【Android JNI】从Java中调用C/C++

时间:2021-12-08 15:50:43

Android系统加载JNI Lib的方式

要想在Java中调用C的函数,必然要有一定的规则去映射二者的函数名,也就是加载JNI库的方式,下面介绍这两种方式。

JNI_OnLoad

当Android的VM(Virtual Machine)执行到C组件(即*so)里的System.loadLibrary()函数时,
首先会去执行C组件里的JNI_OnLoad()函数。这种方法有两个优点,1. 可以通知VM此时native使用哪个版本的JNI,默认是最低版本;2. 使得native层在被加载的时候做一些初始化工作。
JNI_OnLoad()中声明的JNI函数在进程空间中的起始地址被保存在ClassObject->directMethods中,也就是说在加载so的时候就记录下来了JNI函数的地址,等到使用的时候直接从特定地址执行相关函数就可以了。
JNI_OnLoad函数中最重要的事就是调用RegisterNatives函数完成动态库中JNI函数的注册,jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)这个函数的第二个参数JNINativeMethod是如下的结构体:

typedef struct {
    const char* name;
    const char* signature;
    void*       fnPtr;
} JNINativeMethod;

name为java中定义的native函数的函数名,signature为函数的签名,fnPtr为对应的native C函数的函数指针。
函数签名查看方法,比如有Example类,先编译javac Example.java,然后使用命令javap -s -p Example.class可以查看函数和成员变量的签名。

示例:JNI_OnLoad

假设Java中有 public class JniManager { public native String nGetStudentInfo(); }方法,native C中定义的对应的函数为jstring jniGetStudentInfo(JNIEnv *env, jobject object),具体的JNI_OnLoad代码如下所示:

“`cpp
static const JNINativeMethod gMethods[] = {
{“nGetStudentInfo”, “()Ljava/lang/String”, (void *) jniGetStudentInfo},
{“nHello”, “()V”, (void*) jniHello}
};

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {

JNIEnv *env = NULL;
jint result = -1;

if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
    return result;
}

jclass clazz = env->FindClass("com/lmshao/jniexample/JniManager");
if (clazz == NULL) {
    return result;
}

jint count = sizeof(gMethods) / sizeof(gMethods[0]);

if (env->RegisterNatives(clazz, gMethods, count) != JNI_OK) {
    return result;
}

result = JNI_VERSION_1_6;

return result;

}
“`

JNIEXPORTJNICALL为jni.h中的宏定义,
#define JNIEXPORT __attribute__ ((visibility ("default")))JNIEXPORT用来导出动态库的函数符号,JNICALL暂时定义为空。

dvmResolveNativeMethod延迟解析机制

如果JNI Lib中没有JNI_OnLoad函数,即在执行System.loadLibrary时,
无法把此JNI Lib实现的函数在进程中的地址增加到ClassObject->directMethods。则直到需要调用的时候才会解析这些javah风格(包名+类名+方法名)的函数。这种方法是Android Studio默认的native工程使用的方法。
示例:
比如Java里面MainActivity类中有个方法声明为public native String stringFromJNI();,则Native中对应的函数名字应该是jstring Java_com_example_demo_MainActivity_stringFromJNI(){}
虽然这种方法是Android Studio默认Native工程使用的方法,但是因为种种原因在实际Native开发中很少使用。

示例

Android Studio 3新建一个Native工程,默认有如下示例函数。

MainActivity.java

public class MainActivity extends AppCompatActivity {

    // 加载动态库
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    // Native方法
    public native String stringFromJNI();
}

native-lib.cpp

#include <jni.h>
#include <string>

extern "C" 
JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(JNIEnv *env, jobject) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

参考:
- https://blog.csdn.net/fireroll/article/details/50102009

签名

JNI签名是使用一些缩写符号来代表参数类型,这些符号由Java语言规定的。

数据类型的签名

Java基本数据类型的签名如下表所示:

签名 Java类型 JNI类型
Z boolean jboolean
C char jcahr
B byte jbyte
S short jshort
I int jint
J long jlong
F float jfloat
D double jdouble
[B byte[ ] jbyteArray
[I int[ ] jintArray

Java复杂类型签名格式是:“L”+“全限定类名”+“;” 。例如String类型签名为Ljava/lang/String;,对应的JNI类型为jstring。其余的复杂数据类型对应的JNI类型都是jobject,jstring本质上也是jobejct类型,因为使用频率高,所以又单独定义方便使用。

函数参数签名

函数的参数签名由参数和返回值组成,参数用一对小括号包起来,即使参数为空也要使用空括号,括号后面是返回值类型,如果没有返回值则用字母V表示。
如:(I)V表示参数为int,无返回值。()I表示参数为空,返回值为int。([IZ)I表示参数为int[]和boolean,返回值为int。
参考:
刘超. 深入理解Android 5.0系统[M]. 人民邮电出版社, 2015.