前言:
NDK的基础知识,强烈推荐小楠总的NDK系列博客,先拜读一遍,照着学习还是很厉害的
一.基础知识
xxx.c ——> windows .obj ; Linux .o –》 语法检查
2. 链接:将函数之间的关系链接起来,生成一个静态或动态库文件(可执行文件)
.o —–> log.so .dll .exe
静态库:静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分,已经整合进去了
动态库:装入过程中将所有动态链接库载入内存。应用程序在运行时,将所有可能要运行到的模块都全部装入内存(共享内存),
动态链接过程只是把需要调用的函数的路径做个标志(类似于头文件声明),直到运行用到函数时才会从内存载入
当我们调用 java中native声明的javaDiff()方法 的时候会到 Java虚拟机 的内存当中来处理找这个方法,
而加了 native 关键字的时候他就会去到 C++/c 的堆栈空间找这个 C++/c 的实现。
二.JNI开发注意事项
想要只测试so库,需要将验证的so库放到jniLibs指定的目录,然后将build.gradle中相应的所有的cmake都注释掉如下
externalNativeBuild {
cmake {
}
在c、c++中叫函数 java中叫方法,实际上两个是一样的玩意,这里为了区分jni和native所以有两个叫法
三.JNI开发分类
1、静态注册
按照链接给的步骤,就可以创建一个JNIdemo,静态注册,不需要导入so库,直接build->make project之后,点击as的运行,安装app 就可以正常使用native了
2).验证so库
第一步build之后,在\app\build\intermediates\cmake目录下就拿到.so库
1、将module下的build.gradle中的externalNativeBuild都注释掉(防止执行cmakeLists.txt脚本),这样就不会执行脚本生成so库
2、将\app\.externalNativeBuild和app\build目录删除,排除一切干扰
3、将so库放到app\src\main\jniLibs目录下,如果没有jniLibs目录就自己创建一个
4、gradle同步一下,运行成功就验证so库是可以的
2、动态注册
JNIEXPORT jstring JNICALL
string_from_JNI(
JNIEnv *env,
jobject instance/* this */) {
/* 默认是以c++的方式
* std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
*/
const char *hello = "Hello from C";
jstring content = (*env)->NewStringUTF(env, hello);
(*env)->ReleaseStringChars(env, content, hello);
return content;
}
/****
*通过jni的方法访问java中方法
*/
JNIEXPORT void JNICALL
access_method(JNIEnv *env, jobject instance,jstring methodName) {
jclass mainClass = (*env)->GetObjectClass(env, instance);
const char* method_n = (*env)->GetStringUTFChars(env,methodName,NULL);
jmethodID rid = (*env)->GetMethodID(env, mainClass, method_n,
"(I)I");//最后一个参数是方法签名:即前一个I表示java方法的参数,最后一个I表示返回值
jint rNum = (*env)->CallIntMethod(env, instance, rid, 20);
printf("output from C : %d", rNum);
}
//}
/***
参数1:name是Java中方法名。
参数2:signature签名,用字符串是描述了Java中函数的参数和返回值
参数3:fnPtr是函数指针,指向native函数。前面都要接 (void *) C/C++中对应函数的函数名(地址):即指针函数变量名必须和c/c++实现的函数名一样
*/
const JNINativeMethod gMethods[] = {
{"stringFromJNI1","()Ljava/lang/String;",(void*)string_from_JNI},
{"accessMethod1","(Ljava/lang/String;)V",(void*)access_method}
};
int registerNatives(JNIEnv* engv) {
LOGI("registerNatives begin");
jclass clazz;
//这个是具体的类名,不能写错,写错就无法注册成功,也就无法调用jni函数了
clazz = (*engv) -> FindClass(engv, "com/jni/www/jnidemo/dif/JNIDynamicUtil");
if (clazz == NULL) {
LOGI("clazz is null");
return JNI_FALSE;
}
if ((*engv) ->RegisterNatives(engv, clazz, gMethods, NELEM(gMethods)) < 0) {
LOGI("RegisterNatives error");
return JNI_FALSE;
}
return JNI_TRUE;
}
/***
当java中通System.loadLibrary(name);时会调用此方法
*/
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){
LOGI("jni_OnLoad begin");
JNIEnv* env = NULL;
jint result = -1;
if ((*vm)->GetEnv(vm,(void**) &env, JNI_VERSION_1_4) != JNI_OK) {
LOGI("ERROR: GetEnv failed\n");
return -1;
}
assert(env != NULL);
registerNatives(env);
return JNI_VERSION_1_4;
}
//这个是具体的类名,不能写错,写错就无法注册成功,也就无法调用jni函数了 clazz = (*engv) -> FindClass(engv, "com/jni/www/jnidemo/dif/JNIDynamicUtil");
1.完成.c或.cpp文件编写(参考链接)
2.需要在cmakeLists.txt中增加和静态库一样的三个方法,将静态注册中的.c/.cpp文件修改成当前编写的动态注册的.c/.cpp文件
3.将\app\.externalNativeBuild和app\build目录删除,排除一切干扰
4.gradle同步后,(要先删除build目录和extenalNativeBuild目录)再build->make project;
5.build之后,在\app\build\intermediates\cmake目录下就拿到.so库(如果还保留着静态注册的代码,则会生成两个so库,参考文末demo)
6.将so库放到app\src\main\jniLibs目录下,如果没有jniLibs目录就自己创建一个(注:确保so库的路径正确,如果存放在其它目录下,需要配置gradle文件的jniLibs.dir具体可以百度)
7.java的代码中声明native方法,通过static{System.loadLibrary(name);}加载so库,就可以直接通过调动native声明进而调用到jni函数的实现了
注:虽然直接复制过来的so库name会带有前缀,直接将name中不要前缀lib 遵循linux生成so库的标准,会加上前缀,但是在调用此方法时要将前缀lib去掉)
8.将module下的build.gradle中的externalNativeBuild都注释掉(防止执行cmakeLists.txt脚本),这样就不会执行脚本生成so库
9.点击gralde同步
10.如果步骤正确,直接运行应该就成功了
如:
动态注册:由于动态注册也是要通过java类中的绝对路径来找到类中的class,才能进行映射;如代码中
3.so库链接
零碎笔记建议:一定要看,不然坑死你
若是要引用第三方so库的话需要将第三方的头文件和当前的so库建立关系
as中写下native方法后,ctrl+1或者alt+enter会在当前目录下生成jni目录,会自动生成.c文件
归根结底都是通过命令 javah -d jni -jni -classpath class的路径生成
如:
javah -d jni -jni -classpath
C:\Users\Administrator\Desktop\JNIDemo\app\build\intermediates\classes\debug com.jni.www.jnidemo.JNIUtil
会在当前目录下创建jni目录(没有的话),并将自动生成.h头文件在jni目录下,注意此时的头文件是整个是:包名_类名.h 一定要改成和.c文件一样的文件名;此外还要.h头文件的函数声明也要和.c对应上,尤其是动态注册的函数一定一定要一一对应上,否则很容易报找不到方法
如果想要jni中调用第三方的so库,那么需要通过书写cmakeLists.txt脚本关联jni与第三方的so库,编写好之后build就可以是掉第三方so库的方法了;
如:在cmakeList.txt中加入,其中jni是存放头文件的目录
set(distribution_DIR ${CMAKE_SOURCE_DIR}/jni)
#加入头文件:第三方的so库的头文件加入编译到native-lib.so中,native-lib.so中才能使用它里边的函数
#参数是头文件所在的目录
target_include_directories(native-lib PRIVATE ${distribution_DIR})
文档地址: https://developer.android.com/ndk/guides/cmake.html
build.gradle脚本中配置externalNativeBuild{}中的信息可以查看:
app\.externalNativeBuild\cmake\debug\arm64-v8a\cmake_build_command.txt,这里有build之后的具体信息
比如:查询文档可以知道 arguments 中 -DANDROID_PLATFORM 代表编译的 android 平台,
文档建议直接设置 minSdkVersion 就行了,所以这个参数可忽略。
另一个参数 -DANDROID_TOOLCHAIN=clang,
CMake 一共有2种编译工具链 - clang 和 gcc,gcc 已经废弃,clang 是默认的。
流程:
第三方的so库和当前的so库建立连接,还是要通过System.loadLibrary("native-dy-lib");加载各个so库
1.获取第三方的so库提供给其它库使用的函数的头文件(也就是so库对外的函数对应的头文件)
2.将头文件加到本地so库以及和第三方so库链接
3.gradle后,build(要先删除build目录和extenalNativeBuild目录)-->make project获取到本地so库
4.将本地so库和第三方的so库导入项目。
5.需要使用的地方调用System.loadLibrary();加载so库
一定要保证加载的第三方的so库和放在jniLibs指定目录中的第三方so库是一个库,否则很可能会报找不到对应的so的函数
可能频繁出现的异常
java.lang.UnsatisfiedLinkError: dlopen failed: could not load library "libnative-dy-lib.so" needed by "libnative-lib.so"; caused by library "libnative-dy-lib.so" not found
libnative-dy-lib这个库也要放到jniLibs指定的目录否则native-lib.so找不到
报错:Fatal signal 11 (SIGSGV) at 0x00002820 (code=1),thread 23696 (xvdy.oa:vitamio) 这个问题都是调用的jni函数有问题,解决办法就是一边注释代码一边运行看看是哪行代码报错,或者通过调试来定位,调试有机会在讲讲
#静态注册的动态库---so库的cmake
add_library( # Sets the name of the library.也是.so库的名,生成的so库是在\app\build\intermediates\cmake
native-lib
# Sets the library as a shared library.SHARED:动态库 STATIC:静态库
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.c )
find_library( log-lib
log )
target_link_libraries( native-lib
${log-lib} )
#-----静态注册的动态库结束-------
#----开始---动态注册的库---生成动态so库的cmake
add_library( # Sets the name of the library.也是.so库的名(可以自己修改,修改后一定要删除build和externalNativeBuild目录,重新build),生成的so库是在\app\build\intermediates\cmake
native-dy-lib1
SHARED
src/main/cpp/native-dynamic-lib.c )
find_library( log-lib
log )
target_link_libraries( native-dy-lib1
${log-lib} )
#------动态库结束--------
#在 .externalNativeBuild/cmake/debug/{abi}/cmake_build_output.txt 中查看 log。
message(STATUS "execute CMakeLists")
set(CMAKE_VERBOSE_MAKEFILE on)
#当前cmakeList.txt的文件路径
set(lib_src_DIR ${CMAKE_CURRENT_SOURCE_DIR})
set(lib_build_DIR ${lib_src_DIR}/tmp)
#创建目录。父目录不存在也会创建
file(MAKE_DIRECTORY ${lib_build_DIR})
#外层的 CMakeLists 里面核心就是 add_subdirectory,查询CMake 官方文档可以知道这条命令的作用是为构建添加一个子路径。
#子路径中的 CMakeLists.txt 也会被执行。即会去分别执行 gmath 、gperf、mtxxJni 中的 CMakeLists.txt
#参数指定了源码路径,参数2指定了当前cmakeList执行结果的输出路径
#add_subdirectory(${lib_src_DIR}/gmath ${lib_build_DIR}/gmath)
#add_subdirectory(${lib_src_DIR}/gperf ${lib_build_DIR}/gperf)
add_subdirectory(${lib_src_DIR}/mtxxJni)#不指定参数2,默认输出路径
#更改库的输出路径为${distribution_DIR}/gperf/lib/${ANDROID_ABI}
set_target_properties(gperf
PROPERTIES
LIBRARY_OUTPUT_DIRECTORY
"${distribution_DIR}/gperf/lib/${ANDROID_ABI}")
set(CMAKE_VERBOSE_MAKEFILE on)
#创建目录
file(MAKE_DIRECTORY ${distribution_DIR}/gperf/include)
验证so库的整个过程:
想要只测试so库,需要将验证的so库放到jniLibs指定的目录,然后将build.gradle中相应的所有的cmake都注释掉如下
externalNativeBuild {
cmake {
}
}
然后删除build以及.externalNative目录,再者gradle同步,其次build-->makeproject,最后运行,能正确调用相应的jni的函数
demo
http://mp.weixin.qq.com/s/QTxEQg4s5ummtFNe8vRIvA
CMake 的官方文档使用。
https://cmake.org/documentation/
同时在这推荐一个中文翻译的简易的 CMake手册