目录
上一篇 深入浅出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
编译运行得到如下输出:
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输出日志