深入浅出Android NDK之使用RegisterNatives函数动态注册native函数

目录
上一篇 深入浅出Android NDK之java.lang.UnsatisfiedLinkError

在上一章中我们所使用的注册native函数的方式叫做静态注册,这种方式的原理是,当调用native函数时,如果native函数还没有被链接到C/C++函数,虚拟机机会去SO的符号表中寻找名称为(Java_包名_类名_函数名)的函数,找到后将C函数地址链接到java的native函数,之后就可以调用native函数了。
所以静态注册的关键在于,java层和C层对函数名称有一个共同的约定,在java层的native函数没有重载的情况下,C层的函数名总应该具有以下格式:

Java_包名_类名_函数名

掌握了这一点,下一次你就不用再辛苦的使用javah生成头文件了。
对于java层没有重载过的native实例方法,我们只需要在C++代码中定义如下格式函数即可:

extern "C" JNIEXPORT [返回值类型] Java_包名_类名_函数名(JNIEnv *env, jobject thiz,[函数参数列表...])

对于java层没有重载过的native静态方法,我们只需要在C++代码中定义如下格式函数即可:

extern "C" JNIEXPORT [返回值类型] Java_包名_类名_函数名(JNIEnv *env, jclass cls,[函数参数列表...])

除了静态注册外,我们还可以使用动态注册的方式来链接native函数。
使用动态方式注册native函数时,函数名称可以不必和java层约定,可以随便起。
下面我们将上一章的例子修改为动态注册的方式,首先新建一个Test.cpp,内容如下:

#include 

jint cv1(JNIEnv *env, jobject thiz, jbyte p1, jchar p2, jshort p3, jint p4, jlong p5, jstring p6, jobject p7) {
    return 3;
}

jint cv2(JNIEnv *env, jclass cls, jbyte p1, jchar p2, jshort p3, jint p4, jlong p5, jstring p6, jobject p7) {
    return 4;
}

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env;
    vm->GetEnv((void**)&env,JNI_VERSION_1_4);

    jclass classTest = env->FindClass("com/example/hello_jni/Test");
    JNINativeMethod methods[]= {
            {"connectV1", "(BCSIJLjava/lang/String;Landroid/graphics/Bitmap;)I", (void*)cv1},
            {"connectV2", "(BCSIJLjava/lang/String;Landroid/graphics/Bitmap;)I", (void*)cv2},
    };
    env->RegisterNatives(classTest, methods, sizeof(methods)/sizeof(JNINativeMethod));
    return JNI_VERSION_1_4;
}

然后在Android.mk中将Test.cpp文件也加入编译:

LOCAL_SRC_FILES := com_example_hello_jni_Test.cpp test.cpp

编译运行得到如下输出:
深入浅出Android NDK之使用RegisterNatives函数动态注册native函数_第1张图片
logcat中输出的n1的值是3,n2的值是4说明java的native函数connectV1和connectV2函数链接的是cv1和cv2函数,而不是之前的静态注册函数Java_com_example_hello_1jni_Test_connectV1和Java_com_example_hello_1jni_Test_connectV2。
当java层调用test.connectV1和test.connectV2函数时,发现函数已经被动态注册过了,所以这时候虚拟机就不必再去尝试链接Java_com_example_hello_1jni_Test_connectV1和Java_com_example_hello_1jni_Test_connectV2,直接调用cv1和cv2即可。

下面我们详细讲解一下动态注册的代码:
首先我们要讲的是JNI_OnLoad函数,当虚拟机加载SO后,会首先到符号表中找一个名为JNI_OnLoad的函数,如果找到了,会立刻调用它。
简单来说当我们在java层调用System.loadLibrary时,如果SO中存在JNI_OnLoad函数会立刻调用他。
所以JNI_OnLoad是SO的入口函数,是SO中第一个调用的函数,并且只会被调用一次。

JNI_OnLoad函数的第一个参数的类型是JavaVM,JavaVM代表java虚拟机,我们调用

	JNIEnv *env;
    vm->GetEnv((void**)&env,JNI_VERSION_1_4);

可以得到当前线程的JNIEnv对像,得到JNIEnv对像后,我们调用

env->RegisterNatives(classTest, methods, sizeof(methods)/sizeof(JNINativeMethod));

来注册JNI函数。
RegisterNatives的声明如下:

 jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,
        jint nMethods)

clazz代表要注册哪个类的native函数。
methods是一个native函数的数组。
nMethods是代表数组的长度。
所以总的意思就是向类clazz注册nMethods个native函数。
clazz可以通过env->FindClass函数来得到,参数是该类的全类名,即类名+包名,注意要将.号替换为/。
JNINativeMethod有如下定义:

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

fnPtr代表C/C++层函数的地址。
name代表java层native函数的方法名。
signature代表native函数的签名,签名其实就是函数参数类型列表和返回值的编码。将函数的参数类型和返回值类型按照固定规则的编码,编码为一个字符串,就是函数的签名。
我们查看昨天用javah生成的com_example_hello_jni_Test.h文件,会发殃函数前面都有一段这样的注释:

/*
 * Class:     com_example_hello_jni_Test
 * Method:    connectV1
 * Signature: (BCSIJLjava/lang/String;Landroid/graphics/Bitmap;)I
 */

Signature后面的那一段字符串就是函数的签名。
对于初学者,除了javah查看签名外,还可以使用javap查看签名:

C:\Users\zy>d:

D:\>cd D:\ndk-demo\hello-jni\app\build\intermediates\javac\debug\classes

D:\ndk-demo\hello-jni\app\build\intermediates\javac\debug\classes>javap  -s com.example.hello_jni.Test
Compiled from "Test.java"
public class com.example.hello_jni.Test {
  public com.example.hello_jni.Test();
    Signature: ()V

  public native int connectV1(byte, char, short, int, long, java.lang.String, android.graphics.Bitmap);
    Signature: (BCSIJLjava/lang/String;Landroid/graphics/Bitmap;)I

  public static native int connectV2(byte, char, short, int, long, java.lang.String, android.graphics.Bitmap);
    Signature: (BCSIJLjava/lang/String;Landroid/graphics/Bitmap;)I

  static {};
    Signature: ()V
}

对于有经验的程序员我们一般自己写签名,其实规则很简单:

(参数1类型的签名+参数2类型的签名...)返回值类型的签名

括号里面是每个参数类型的签名,括号后面是返回值类型的签名。
对于基本数据类型来说,对应规则如下:

Java类型 签名
boolean Z
byte B
char C
short S
int I
long J
float F
double D

对于基本类型除了boolean是Z,long是J外,其他的都是类型的首字母的大写。
为什么boolean是Z不是B呢?因为byte也是B,冲突了。
为什么long是J不是L呢?因为对于类的签名以L开头。下面列举几个类的签名:

类名 签名
java.lang.String Ljava/lang/String;
android.graphics.Bitmap Landroid/graphics/Bitmap;
com.example.hello_jni.Test Lcom/example/hello_jni/Test;

规则也很简单,以L开头,分号结尾,中间也上全类名。把.替换为/就可以了。
对于数组类型,只需要在前面加上[即可,例如:

类型 签名
int[] [I
short[] [S
boolean[] [Z
java.lang.String[] [Ljava/lang/String;

静态注册和动态注册其实是两个相反的过程。
静态注册是java函数找C函数。
动态注册是C函数找java函数。

使用RegisterNatives进行动态注册时,并不要求将java类中的所有native方法一次性全部注册,我们可以一个一个的注册。对于已经注册过的也可以重新注册到一个新的函数。
静态注册和动态注册并不冲突,可以一个函数使用静态注册,另一个函数使用动态注册。对于静态注册过的函数也可以重新进行动态注册。

使用动态注册时,C/C++层的jni函数不需要使用extern "C"和JNIEXPORT来声明,只需要将JNI_OnLoad函数使用extern "C"和JNIEXPORT声明即可,再配合fvisibility=hidden标志进行编译。这样可以隐藏我们的jni入口函数,给别人破解我们的SO带来一定的阻力。

有些细心的同学可能会发现我的例子中的JNI_OnLoad函数并没有使用extern "C"和JNIEXPORT来声明,那是因为我包含了jni.h,而jni.h对JNI_OnLoad进行了extern "C"和JNIEXPORT声明,所以我这里只用包含jni.h就可以了,不需要再额外的声明了。

下一篇 深入浅出Android NDK之往logcat输出日志

你可能感兴趣的:(深入浅出Android,NDK开发,ndk,android,jni)