使用C开发Android的一种容易实现的方式
kelvin 发布于 2021-02-21
将C结构和函数绑定到Kotlin比您想象的要简单!

android NDK(native development kit)不被认为是对开发者最友好的工具之一。在本文中,我提出了一个解决方案,使使用它更容易。

介绍

为什么要在Android项目中使用C?我可以给你两个很好的理由:
性能。如果你正在开发一个计算量很大的应用程序(游戏、CAD、图像处理、密码学等),你可能会考虑在C库中实现其中的一些计算。
跨平台开发。你可以在Android和iOS应用程序中集成C库。
实际上,将C代码集成到用Swift编写的iOS应用程序中非常简单。关于这一点,您可以在这里阅读更多:
导入C和Objective-C API
另一方面,在Android中,集成C库是一项更为复杂的任务。在本文中,我将向您展示如何简化此任务。


背景

这不是对Android NDK的介绍。如果您刚刚开始熟悉它,最好从官方文档开始:

官方代码生成

幸运的是,Java编译器中有一个内置工具,可以从Java类生成本机绑定。在撰写本文时,Kotlin还没有得到这个工具的正式支持,但是由于Java和Kotlin具有极好的互操作性,因此您也可以在Kotlin项目中使用它。让我们看看这是怎么回事。下面是一个小示例,其中有一个消息类:

 public class Message {
    public String subject;
    public String text;
}
以及发送消息函数,该函数应绑定到本机C函数:

 public class HelloJNI {
   static {
      System.loadLibrary("hello");
   }

   private native boolean sendMessage(Message message);
}
让我们运行以下命令:

 javac -h . HelloJNI.java
这将为Java本机接口(JNI)生成以下C绑定:

 /* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */

#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    sendMessage
 * Signature: (LMessage;)Z
 */
JNIEXPORT jboolean JNICALL Java_HelloJNI_sendMessage(JNIEnv *, jobject, jobject);

#ifdef __cplusplus
}
#endif
#endif
生成的代码的重要部分是以下函数定义:

 JNIEXPORT jboolean JNICALL Java_HelloJNI_sendMessage(JNIEnv *, jobject, jobject);
第一个参数是JNI环境,第二个是HelloJNI实例,第三个是消息实例。请注意,这些类型根本没有映射,因此对于第三个参数,您需要知道它是一个消息实例,并且它有一个名为“subject”的成员,这是一个字符串。如果你想读subject的值,你需要这样写:

 jclass messageClass = (*jenv)->FindClass((JNIEnv *) jenv, "com/jnigen/model/Message");
jfieldID fieldId =  (*jenv)->GetFieldID((JNIEnv *) jenv, 
                    messageClass, "subject", "Ljava/lang/String;");
char* value = (*jenv)->GetStringUTFChars((JNIEnv *) jenv, 
              (*jenv)->GetObjectField((JNIEnv *) jenv, obj, fieldId), 0);
是的,您理解正确,此代码相当于:

 String value = message.subject;
一定有更好的办法吧?
在我向您展示我对这个问题的解决方案之前,我想指出这种代码生成的另一个不便之处。我们这里有一个Android应用程序,它使用一个C库,但是它是定义接口的应用程序,C库应该提供这个接口。如果团队中的一个成员编写C库,另一个成员编写Kotlin/Java代码,这将是一个大问题。即使你一个人工作,这也会妨碍你按逻辑顺序做事,这有点烦人。


生成代码的更好方法

我编写了一个代码生成器,它的工作方式正好相反:它生成Kotlin代码,并从C代码生成C绑定。此项目可在以下位置找到:

让我们看看同样的例子,但现在有了新的发电机。使用此生成器,我们将从编写C头开始:

 #ifndef MESSAGE_H
#define MESSAGE_H

struct Message {
    char* subject;
    char* text;
};

int sendMessage(struct Message message);

#endif
从这个头文件,我的生成器将创建三个文件:
消息结构将有一个Kotlin对应项:
//Generated code. Do not edit!

package com.jnigen.model

data class Message (var subject: String = String(), var text: String = String())
本机接口:

//Generated code. Do not edit!

package com.jnigen

import com.jnigen.model.*

class JniApi {

    external fun sendMessage(message: Message): Int
    companion object {
        init {
            System.loadLibrary("native-lib")
        }
    }
}
以及JNI的绑定代码:

 //  Generated code. Do not edit!

#include <stdlib.h>
#include "../example/example.h"
#include "jni.h"

jclass getMessageClass(JNIEnv const *jenv) {
    return (*jenv)->FindClass((JNIEnv *) jenv, "com/jnigen/model/Message");
}

jmethodID getMessageInitMethodId(JNIEnv const *jenv) {
    return (*jenv)->GetMethodID((JNIEnv *) jenv, getMessageClass(jenv), "<init>", "()V");
}

jobject createMessage(JNIEnv const *jenv) {
    return (*jenv)->NewObject((JNIEnv *) jenv, getMessageClass(jenv), 
            getMessageInitMethodId(jenv));
}

jfieldID getMessageFieldID(JNIEnv const *jenv, char *field, char *type) {
    return (*jenv)->GetFieldID((JNIEnv *) jenv,
                               getMessageClass(
                                       jenv),
                               field, type);
}

jobject convertMessageToJobject(JNIEnv const *jenv, struct Message value) {
    jobject obj = createMessage(jenv);
    (*jenv)->SetObjectField((JNIEnv*)jenv, obj, 
    getMessageFieldID(jenv, "subject", "Ljava/lang/String;"), 
    (*jenv)->NewStringUTF((JNIEnv *) jenv, value.subject));
    (*jenv)->SetObjectField((JNIEnv*)jenv, obj, 
    getMessageFieldID(jenv, "text", "Ljava/lang/String;"), 
    (*jenv)->NewStringUTF((JNIEnv *) jenv, value.text));
    return obj;
}

struct Message convertJobjectToMessage(JNIEnv const *jenv, jobject obj) {
    struct Message result;
    result.subject = (*jenv)->GetStringUTFChars((JNIEnv *) jenv, 
    (*jenv)->GetObjectField((JNIEnv *) jenv, obj, 
    getMessageFieldID(jenv, "subject", "Ljava/lang/String;")), 0);
    result.text = (*jenv)->GetStringUTFChars((JNIEnv *) jenv, 
    (*jenv)->GetObjectField((JNIEnv *) jenv, obj, 
    getMessageFieldID(jenv, "text", "Ljava/lang/String;")), 0);
    return result;
}

JNIEXPORT
jint JNICALL
Java_com_jnigen_JniApi_sendMessage(JNIEnv *jenv, jobject instance,
                                              jobject message) {
  int result = sendMessage(
      convertJobjectToMessage(jenv, message)
  );
  return result;
}
请注意,这个代码生成过程的结果要好得多。由于C结构正确地映射到Kotlin数据类,因此基本上不需要手工编写任何JNI代码。
只需编写一个C库,运行生成器,然后从Kotlin调用它。就这么简单!;)

出处:https://www.codeproject.com/Tips/5295028/Using-C-for-Android-Development-the-Easy-Way

原作者:Gábor Angyal

kelvin
关注 私信
文章
92
关注
0
粉丝
0