android之一篇史上最适合最全面的JNI入门教程
Posted 小钟视野
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android之一篇史上最适合最全面的JNI入门教程相关的知识,希望对你有一定的参考价值。
前言:
一定要下载demo,动手动脑,结合本篇博客来跑demo,否则看了也还是不会;写代码还是要勤动手才能掌握,否则里边的坑也只是想当然 demoNDK的基础知识,强烈推荐小楠总的NDK系列博客,先拜读一遍,照着学习还是很厉害的
一.基础知识
JNI:是java和c/c++交互的桥梁;有必要去弄明白整个开发流程;jni的效率比java要快,所以一些好性能的都会通过走底层来调用java 用途:用的比较多的是视频、美颜、相机、地图等涉及底层以及效率问题 NDK:是一针对jni中的工具包,包括底层c库、编译工具等 1. 编译xxx.c ——> windows .obj ; Linux .o –》 语法检查
2. 链接:将函数之间的关系链接起来,生成一个静态或动态库文件(可执行文件)
.o —–> log.so .dll .exe
静态库:静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分,已经整合进去了
动态库:装入过程中将所有动态链接库载入内存。应用程序在运行时,将所有可能要运行到的模块都全部装入内存(共享内存),
动态链接过程只是把需要调用的函数的路径做个标志(类似于头文件声明),直到运行用到函数时才会从内存载入
jvm 是虚拟机内存,C/C++ 是 native内存,并且这个 so库 是放在 apk 的 lib 下面的
当我们调用 java中native声明的javaDiff()方法 的时候会到 Java虚拟机 的内存当中来处理找这个方法,
而加了 native 关键字的时候他就会去到 C++/c 的堆栈空间找这个 C++/c 的实现。
二.JNI开发注意事项
jni开发中如果不是作为外部库或.c文件的函数不对外,则可以不生成.h头文件,头文件的声明只是想把当前.c/.cpp文件下的功能提供给外部文件使用,如:demo只有一个.c文件,函数定义 都是可以直接调用的,不需要声明;但是要是作为第三方库且库中的函数对外开放,则必须要有对应.c的头文件.h,否则无法调用相应的函数; 即:不管动态还是静态注册,只要不是对外(.c文件之间以及库和库之间)提供的函数功能,则不需要生成头文件; demo将分开来说,只要按照以下说的步骤,就能清楚知道静态注册、动态注册以及so库对外的函数会在当前JNI函数中被调用的情况。 期间会涉及到修改cmakeList.txt脚本,所以每次修改后,都要同步gradle和重新build--->makeProject(先删除build)这里也会分两种方式说明: 一、不验证so库,直接gradle,build之后,运行:主要是验证jni的方法是否正确,算是调试吧 二、验证so库 验证so库的整个过程:
想要只测试so库,需要将验证的so库放到jniLibs指定的目录,然后将build.gradle中相应的所有的cmake都注释掉如下
externalNativeBuild
cmake
然后删除build以及.externalNative目录,再者gradle同步,其次build-->makeproject,最后运行,能正确调用相应的jni 的函数
在c、c++中叫函数 java中叫方法,实际上两个是一样的玩意,这里为了区分jni和native所以有两个叫法
三.JNI开发分类
1、静态注册
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、动态注册
大概就是当java中通System.loadLibrary(name);时会调用JNI_OnLoad()函数,所以在这函数中将java中native方法 声明和jni函数实现进行注册,也就是绑定(声明-实现) 生成动态so库的链接: 动态注册 代码如下: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;
注意gMethod数组和registerNatives(env)函数,重点是在注释;看懂流程才是重点; 这里流程是System.loadLibrary()-->JNI_onLoad()-->registerNatives()--->RegisterNatives(...) 一定要看注释,这个流程中registerNatives(env)中,注意这个参数值,是java中的具体类的路径名,只是将"."换成“/”
//这个是具体的类名,不能写错,写错就无法注册成功,也就无法调用jni函数了 clazz = (*engv) -> FindClass(engv, "com/jni/www/jnidemo/dif/JNIDynamicUtil");生成并验证so库
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方法,通过staticSystem.loadLibrary(name);加载so库,就可以直接通过调动native声明进而调用到jni函数的实现了
注:虽然直接复制过来的so库name会带有前缀,直接将name中不要前缀lib 遵循linux生成so库的标准,会加上前缀,但是在调用此方法时要将前缀lib去掉)
8.将module下的build.gradle中的externalNativeBuild都注释掉(防止执行cmakeLists.txt脚本),这样就不会执行脚本生成so库
9.点击gralde同步
10.如果步骤正确,直接运行应该就成功了
动态注册是需要JNIDynamicUtil类的具体路径才能进行动态注册的,所以如果要封装成第三发so库供给第三方使用,需要写一个工具类统一管理native的注册,然后将这个工具类做成jar包,将jar包和so供给第三方使用;(这只是个人看法,没有具体实施,希望有知道怎么做的,能不吝赐教)注:不管是静态注册还是动态注册:都必须遵循 java的类名包名方法名和jni的函数一一对应
如: 静态注册:不管so库是不是在不同的项目中使用,都必须保证java中使用声明native的方法的类必须和JNI函数命名规则 保持一致:即java_包名_类名_方法名
动态注册:由于动态注册也是要通过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函数有问题,解决办法就是一边注释代码一边运行看看是哪行代码报错,或者通过调试来定位,调试有机会在讲讲
cmakeLists.txt: 生成静态库和动态库:区分于静态注册和动态注册:库是将函数打包成动态库,注册是函数注册,两者概念没关系
#静态注册的动态库---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
以下以下强烈推荐一篇博客:cmake的手册文档、ndk的手册文档在这篇博客都有讲解,会更新cmake的讲解
http://mp.weixin.qq.com/s/QTxEQg4s5ummtFNe8vRIvA
CMake 的官方文档使用。
https://cmake.org/documentation/
同时在这推荐一个中文翻译的简易的 CMake手册
https://www.zybuluo.com/khan-lau/note/254724
以上是关于android之一篇史上最适合最全面的JNI入门教程的主要内容,如果未能解决你的问题,请参考以下文章
史上最简单&最全&最基础&入门到精通的opencv图像处理 第十九课:图像近似