上次面试,腾讯小哥听说我写过jni代码,问了一个问题,JNI代码里,Java上层声明的一个native函数,在cpp文件中实现它的时候,函数名过长,有没有什么办法来改进?
一般我们写JNI中cpp的代码,都是Java_包名_类名_native方法名(Java_com_jni_ndkdemo_JniUtils_javaGetString),遇到包名中本身带有下划线的,它会添加上去数字(Java_com_my_1project_test_1jni_JNIUtils_getName)都是以这样的方式去实现c代码,cpp文件中这个函数确实是有点长,一番学习后发现可以有其他的实现方式,核心是registerNative函数,JNI实际有两种注册方式:
- 静态注册,就是Java_包名_类名_native方法名这种定位注册方式
- 动态注册,registerNative方式注册
言归正传,我们开始registerNative类型的动态注册的方式来写一个JNI的demo,
1、首先依然是创建Java的native方法,
package com.jni.ndkdemo;
public class JniUtils {
public native int javaAdd(int x,int y);
public native String javaGetString();
static {
System.loadLibrary("testLib");
}
}
2、创建cpp目录以及文件,
3、创建CMakeLists.txt文件
cmake_minimum_required(VERSION 3.4.1)
add_library(
testJNI
SHARED
src/main/cpp/arraysAdd-lib.cpp)
add_library(
registerNative
SHARED
src/main/cpp/registerNative.cpp)
add_library(
testLib
SHARED
src/main/cpp/test.cpp)
find_library(
log-lib
log
)
target_link_libraries(testJNI ${log-lib})
这里我们能看到,我们是可以无限编译添加cpp文件的,只要你不停的调用add_library就好,但是有一点要注意,生成so库的名称,不能相同否则编译会失败的,能看到我这里的命名,testJNI、registerNative、testLib都是不相同的
4、在APP的build.gradle文件中添加关于CMake编译方式的配置
android {
compileSdkVersion 26
defaultConfig {
applicationId "com.jni.ndkdemo"
minSdkVersion 15
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
主要就是上述两个externalNativeBuild 函数的task任务,其实这些和JNI全类名静态注册的方式没有任何区别,静态注册和动态注册的区别只在cpp文件的实现中,下面我们来看registerNative动态注册方式到底是怎么实现的,当jvm启动,执行System.loadLibrary(native-lib),链接到由CMakeLists.txt–Jni中cpp文件生成的so库的时候,系统就会主动的调用cpp文件中的JNI_OnLoad(JavaVM* vm,void* reserve)这个函数名不能乱改,否则系统是无法正常工作的,
jint JNI_OnLoad(JavaVM* vm,void* reserved){
printf("jni onload called");
//注册时在JNIEnv中实现的,所以必须首先获取它
JNIEnv* env =NULL;
jint result = -1;
//从JavaVM中获取JNIEnv,一般使用1.4的版本
if(vm->GetEnv((void **)&env,JNI_VERSION_1_4)!=JNI_OK){
return -1;
}
result = RegisterNative(env);
printf("RegisterNatives result: %d", result);
printf("jni onload called end...");
return JNI_VERSION_1_4; //这里很重要,必须返回版本,否则加载会失败。
}
这个没什么好说的,JNI_OnLoad是系统函数,JavaVM代表的就是java的虚拟机环境,拿到参数JNIEnv以后,传递给registerNative函数,
jint RegisterNative(JNIEnv* env){
//获取映射的Java类
jclass javaClass=env->FindClass(className);//1
if(javaClass == NULL){
printf("cannot get class:%s\n", className);
return -1;
}
return env->RegisterNatives(javaClass,getMethods,sizeof(getMethods)/sizeof(getMethods[0]));//2
}
关注点1、FindClass函数是系统函数,直接调用就好,className是java层的native函数所在的全类名字符串,根据java层native函数的全类名找到带有native函数的java类,
//java代码中native方法的全类名
const char* className="com/jni/ndkdemo/JniUtils";
关注点2、RegisterNatives函数也是系统函数,直接调用传参就好,而getMethods是一个数组,这个数组存放的是native函数相关的信息
const JNINativeMethod getMethods[] = {
{"javaAdd","(II)I",(void *) cAdd},
{"javaGetString","()Ljava/lang/String;",(void *) getString}
};
我们仔细查看这个数组,如果你的demo跑不起来,绝大多数原因都是在这里:单拎出来一个下标的元素,{“javaAdd”,"(II)I",(void *) cAdd},
- 参数一:“javaAdd” ----- java层native函数命名是什么,这里就传什么,但要是字符串
- 参数二: “(II)I” ------ java层native函数的签名,这个签名是什么,就是能够唯一表示某一个函数的签名,因为java函数是可以重载的,签名是由参数类型列表和返回值共同组成,
- 参数三:"(void *) cAdd" --------- cAdds是cpp文件中实现java层native函数的实现方法
jint cAdd(JNIEnv * env,jobject jobj,jint x,jint y){
printf("cAdd x is :%d ,y is :%d",x,y);
return x+y;
}
最后使用env这个指针 调用RegisterNatives函数,传入参数,native函数就在底层动态注册好了,根据我的踩坑之路,cpp文件动态注册的代码没有什么问题,大家主要是弄不懂这个JNI中函数的签名是怎么回事,下篇文章我们重点分析一下,https://www.cnblogs.com/CCBB/p/3978847.html