目录介绍
- 01.学习JNI开发流程
- 1.1 JNI开发概念
- 1.2 JNI和NDK的联系
- 1.3 JNI实践进程
- 1.4 NDK运用场景
- 1.5 学习道路阐明
- 02.NDK架构分层
- 2.1 NDK分层构建层
- 2.2 NDK分层Java层
- 2.3 Native层
- 03.JNI根底语法
- 3.1 JNI三种引证
- 3.2 JNI反常处理
- 3.3 C和C++彼此调用
- 3.4 JNI中心原理
- 3.5 注册Native函数
- 3.6 JNI签名是什么
- 04.一些必备操作
- 4.1 so库生成打包
- 4.2 so库查询操作
- 4.3 so库怎样反编译
- 05.实践几个事例
- 5.1 Java静态调用C/C++
- 5.2 C/C++调用Java
- 5.3 Java调三方so中API
- 5.4 Java动态调C++
- 06.一些技能原理
- 6.1 JNIEnv创立和开释
- 6.2 动态注册的原理
- 6.3 注册JNI流程图
- 07.JNI遇到的问题
- 7.1 混杂的bug
- 7.2 留意字符串编译
01.学习JNI开发流程
1.1 JNI开发概念
- .SO库是什么东西
- NDK为了方便运用,供给了一些脚本,使得更简略的编译C/C++代码。在Android程序编译中会将C/C++ 编译成动态库 so 文件,相似java库.jar文件相同,它的生成需求运用NDK东西来打包。
- so是shared object的缩写,见名思义便是共享的目标,机器可以直接运转的二进制代码。实质so文件便是一堆C、C++的头文件和完结文件打包成一个库。
- JNI是什么东西
- JNI的全称是Java Native Interface,即本地Java接口。由于 Java 具备跨平台的特色,所以Java 与 本地代码交互的才能十分弱。
- 选用JNI特性可以增强 Java 与本地代码交互的才能,使Java和其他类型的言语如C++/C可以彼此调用。
1.2 JNI和NDK的联系
- JNI和NDK学习内容太难
- 其实难的不是JNI和NDK,而是C/C++言语,JNI和NDK只是个东西,很简略学习的。
- JNI和NDK有何联系
- 学习JNI之前,首先得先知道JNI、NDK、Java和C/C++之间的联系。
- 在Android开发中,有时为了性能和安全性(反编译),需求运用C/C++言语,可是Android APP层用的是Java言语,怎样才能让这两种言语进行交流呢,由于他们的编码办法是不相同的,这是就需求JNI了。
- JNI可以被看作是代理形式,JNI是java接口,用于Java与C/C++之间的交互,作为两者的桥梁,也便是Java让JNI代其与C/C++交流。
- NDK是Android东西开发包,协助快速开发C/C++动态库,相当于JDK开发java程序相同,一同能帮打包生成.so库
1.3 JNI实践进程
- 操作实践进程
- 第一步,编写native办法。
- 第二步,依据此native办法编写C文件。
- 第三步,运用NDK打包成.so库。
- 第四步,运用.so库然后调用api。
- 怎样运用NDK打包.so库
- 1,编写Android.mk文件,此文件用来告知NDK打包.so库的规矩
- 2,运用ndk-build打包.so库
- 相关学习文档
- NDK学习:developer.android.google.cn/ndk/guides?…
1.4 NDK运用场景
- NDK的运用场景一般在:
- 1.为了进步这些模块的性能,对图形,视频,音频等核算密集型运用,将杂乱模块核算封装在.so或许.a文件中处理。
- 2.运用的是C/C++进行编写的第三方库移植。如ffmppeg,OpenGl等。
- 3.某些状况下为了进步数据安全性,也会封装so来完结。究竟运用纯Java开发的app是有很多逆向东西可以破解的。
1.5 学习道路阐明
- JNI学习道路介绍
- 1.首先要有点C/C++的根底,这个我是在 菜鸟教程 上学习的
- 2.了解NDK和JNI的一些概念,以及NDK的一个大约的架构分层,JNI的开发进程是怎样的
- 3.掌握事例操练,前期先写事例,比方java调用c/c++,或许c/c++调用java。把这个事例写熟,跑通即可
- 4.事例操练之后,然后在考虑NDK是怎样编译的,怎样打包so文件,loadLibrary的流程,CMake作业流程等一些根底的原理
- 5.在实践进程中,先记录遇到的问题。这时分可能不一定懂,先放着,先完结事例或许简略的业务。然后边实践边揣摩问题和背面的原理
- 留意事项介绍
- 避免一开端就研讨原理,或许把C/C++全体学习一遍,那样会比较辛苦。焦点先放在JNI通讯流程上,写事例学习
- 把学习内容,分为几个不同类型:了解(可以扯淡),了解(大约知道什么意思),掌握(可以运用和实践),通晓(能触类旁通和分享讲清楚)
02.NDK架构分层
- 运用NDK开发终究目标是为了将C/C++代码编译生成.so动态库或许静态库文件,并将库文件供给给Java代码调用。
- 所以按架构来分可以分为以下三层:
- 1.构建层
- 2.Java层
- 3.native层
2.1 NDK分层构建层
- 要得到目标的so文件,需求有个构建环境以及进程,将这个进程和环境称为构建层。
- 构建层需求将C/C++代码编译为动态库so,那么这个编译的进程就需求一个构建东西,构建东西依照开发者指定的规矩办法来构建库文件,相似apk的Gradle构建进程。
- 在解说NDK构建东西之前,咱们先来了解一些关于CPU架构的知识点:Android abi
- ABI即Application Binary Interface,界说了二进制接口交互规矩,以习惯不同的CPU,一个ABI对应一种类型的CPU。
- Android目前支撑以下7种ABI:
- 1.armeabi:第5代和6代的ARM处理器,前期手机用的比较多。
- 2.armeabi-v7a:第7代及以上的 ARM 处理器。
- 3.arm64-v8a:第8代,64位ARM处理器
- 4.x86:一般用在平板,模拟器。
- 5.x86_64:64位平板。
- 常规的NDK构建东西有两种:
- 1.ndk-build:
- 2.Cmake
- ndk-build其实便是一个脚本。前期的NDK开发一向都是运用这种形式
- 运转ndk-build相当于运转一下指令:$GNUMAKE -f /build/core/build-local.mk
- $GNUMAKE 指向 GNU Make 3.81 或更高版本, 则指向 NDK 安装目录
- 运用ndk-build需求合作两个mk文件:Android.mk和Application.mk。
- Cmake是一个编译系统的生成器
- 简略了解便是,他是用来生成makefile文件的,Android.mk其实便是一个makefile类文件,cmake运用一个CmakeLists.txt的装备文件来生成对应的makefile文件。
- Cmake构建so的进程其实包含两步:进程1:运用Cmake生成编译的makefiles文件;进程2:运用Make东西对进程1中的makefiles文件进行编译为库或许可履行文件。
- Cmake优势在哪里呢?在生成makefile进程中会主动剖析源代码,创立一个组件之间依靠的联系树,这样就可以大大缩减在make编译阶段的时间。
- Cmake构建项目装备
- 运用Cmake进行构建需求在build.gradle装备文件中声明externalNativeBuild
2.2 NDK分层Java层
-
怎样挑选正确的so库呢
- 通常状况下,咱们在编译so的时分就需求确定自己设备类型,依据设备类型挑选对应abiFilters。
- 留意:运用as编译后的so会主动打包到apk中,假如需求供给给第三方运用,可以到build/intermediates/cmake/debug or release 目录中copy出来。
-
Java层怎样调用so文件中的函数
- 对于Android上层代码来说,在将包正确导入到项目中后,只需求一行代码就可以完结动态库的加载进程。有两种办法:
System.load("/data/local/tmp/native_lib.so"); System.loadLibrary("native_lib");
- 1.加载途径不同:load是加载so的完好途径,而loadLibrary是加载so的称号,然后加上前缀lib和后缀.so去默许目录下查找。
- 2.主动加载库的依靠库的不同:load不会主动加载依靠库;而loadLibrary会主动加载依靠库。
-
不管哪种办法,终究都会调用到LoadNativeLibrary()办法,该办法首要操作:
- 1.经过dlopen翻开动态库文件
- 2.经过dlsym找到JNI_OnLoad符号所对应的办法地址
- 3.经过JNI_OnLoad去注册对应的jni办法
2.3 Native层
- 怎样了解JNI的规划思想
- JNI(全名Java Native Interface)Java native接口,其可以让一个运转在Java虚拟机中的Java代码被调用或许调用native层的用C/C++编写的基于本机硬件和操作系统的程序。简略了解为便是一个连接Java层和Native层的桥梁。
- 开发者可以在native层经过JNI调用到Java层的代码,也可以在Java层声明native办法的调用入口。
- JNI注册办法
- 当Java代码中履行Native的代码的时分,首先是经过一定的办法来找到这些native办法。JNI有静态注册和动态注册两种注册办法。
- 静态注册先由Java得到本地办法的声明,然后再经过JNI完结该声明办法。动态注册先经过JNI重载JNI_OnLoad()完结本地办法,然后直接在Java中调用本地办法。
03.JNI根底语法
3.1 JNI三种引证
- 在JNI标准中界说了三种引证:
- 部分引证(Local Reference)、大局引证(Global Reference)、弱大局引证(Weak Global Reference)。
- Local引证
- JNI中运用 jobject, jclass, and jstring等来标志一个Java目标,然而在JNI办法在运用的进程中会创立很多引证类型,假如运用进程中不留意就会导致内存走漏。
- 直接运用:NewLocalRef来创立。Local引证其实便是Java中的部分引证,在声明这个部分变量的办法结束或许退出其效果域后就会被GC收回。
- Global引证大局引证
- 大局引证可以跨办法、跨线程运用,直到被开发者显式开释。一个大局引证在被开释前保证引证目标不被GC收回。
- 和部分运用不同的是,能创立大局引证的函数只要NewGlobalRef,而开释它需求运用ReleaseGlobalRef函数。
- Weak引证
- 弱引证可以运用大局声明的办法。弱引证在内存不足或许严重的时分会主动收回掉,可能会呈现时间短的内存走漏,可是不会呈现内存溢出的状况。
3.2 JNI反常处理
- native层反常
- 处理办法1:native层自行处理
- 处理办法2:native层抛出给Java层处理
3.4 JNI中心原理
- java运转在jvm,jvm自身便是运用C/C++编写的,因而jni只需求在java代码、jvm、C/C++代码之间做切换即可
- JNIEnv是什么?
- JINEnv是当时Java线程的履行环境,一个JVM对应一个JavaVM结构体,一个JVM中可能创立多个Java线程,每个线程对应一个JNIEnv结构,它们保存在线程本地存储TLS中。
- 因而不同的线程JNIEnv不同,而不能彼此共享运用。 JavaEnv结构也是一个函数表,在本地代码经过JNIEnv函数表来操作Java数据或许调用Java办法。
3.5 注册Native函数
- JNI静态注册:
- 进程1.在Java中声明native办法,比方:public native String stringFromJNI()
- 进程2.在native层新建一个C/C++文件,并创立对应的办法(主张运用AS快捷键主动生成函数名),比方:testjnilib.cpp: Line 8
- JNI动态注册
- 经过RegisterNatives办法把C/C++中的办法映射到Java中的native办法,而无需遵从特定的办法命名格局,这样书写起来会省劲很多。
- 动态注册其实便是运用到了前面剖析的so加载原理:在最终一步的JNI_OnLoad中注册对应的jni办法。这样在类加载的进程中就可以主动注册native函数。比方:
- 与JNI_OnLoad()函数相对应的有JNI_OnUnload()函数,当虚拟机开释该C库的时分,则会调用JNI_OnUnload()函数来进行善后清除作业。
- 那么怎样挑选运用静态注册or动态注册
- 动态注册和静态注册终究都可以将native办法注册到虚拟机中,引荐运用动态注册,更不简略写错,静态注册每次增加一个新的办法都需求检查原函数类的包名。
3.6 JNI签名是什么
- 为什么JNI中突然多出了一个概念叫”签名”:
- 由于Java是支撑函数重载的,也便是说,可以界说相同办法名,可是不同参数的办法,然后Java依据其不同的参数,找到其对应的完结的办法。
- 这样是很好,所以说JNI肯定要支撑的,假如只是是依据函数名,没有办法找到重载的函数的,所以为了处理这个问题,JNI就衍生了一个概念——”签名”,即将参数类型和回来值类型的组合。
- 假如具有一个该函数的签名信息和这个函数的函数名,就可以顺序的找到对应的Java层中的函数。
- 怎样检查签名呢:可以运用javap指令。
- javap -s -p MainActivity.class
04.一些必备操作
4.1 so库生成打包
- 什么是so文件库
- so库,即将C或许C++完结的功用进行打包,将其打包为共享库,让其他程序进行调用,这可以进步代码的复用性。
- 关于.so文件的生成有两种办法
- 可以供给给大家参考,一种是CMake主动生成法,另一种是传统打包法。
- so文件在程序运转时就会加载
- 所以想运用Java调用.so文件,必有某个Java类运转时load了native库,并经过JNI调用了它的办法。
- cmake生成.so计划
- 第一步:创立native C++ Project项目,创立native函数并完结,先测验本地JNI函数调通
- 第二步:获取.so文件。将生成的.apk文件改为.zip文件,然后进行解压缩,就能看到.so文件。假如想支撑多种库架构,则可在module的build.gradle中装备ndk支撑。
- 第三步:so文件测验。新建一个一般的Android程序,将so库放入程序,然后创立类(留意要相同的包名、文件名及办法名)去加载so库。
- 总结一下:Android Studio主动创立的native C++项目默许支撑CMake办法,它支撑JNI函数调用的入口在build.gradle中。
- 传统打包生成.so计划【不引荐这种办法】
- 第一步:在Java类中声明一个本地办法。
- 第二步:履行指令javah获得C声明的.h文件。
- 第三步:获得.c文件并完结本地办法。创立Android.mk和Application.mk,并装备其参数,两个文件如不编写或编写正常会呈现报错。
- 第四步:打包.so库。cd到\app目录下,履行指令 ndk-build即可。生成so库后,最终测验ok即可。
4.2 so库查询操作
- so库怎样查找所对应的方位
- 第一步:在 app 模块的 build.gradle 中,追加以下代码:
- 第二步:履行指令行:./gradlew assembleDebug 【留意假如遇到gradlew找不到,则输入:chmod +x gradlew】
- so文件查询成果后。就可以查询到so文件归于那个lib库的!如下所示:libtestjnilib.so文件归于TestJniLib库的 find so file: /Users/yc/github/YCJniHelper/TestJniLib/build/intermediates/library_jni/debug/jni/armeabi-v7a/libtestjnilib.so find so file: /Users/yc/github/YCJniHelper/SafetyJniLib/build/intermediates/library_jni/debug/jni/armeabi-v7a/libsafetyjnilib.so find so file: /Users/yc/github/YCJniHelper/SignalHooker/build/intermediates/library_jni/debug/jni/armeabi-v7a/libsignal-hooker.so
05.实践几个事例
5.1 Java静态调用C/C++
-
Java调用C/C++函数调用流程
- Java层调用某个函数时,会从对应的JNI层中寻觅该函数。依据java函数的包名、办法名、参数列表等多方面来确定函数是否存在。
- 假如没有就会报错,假如存在就会就会树立一个相关联系,以后再调用时会直接运用这个函数,这部分的操作由虚拟机完结。
-
Java层调用C/C++办法操作进程
- 第一步:创立java类NativeLib,然后界说native办法stringFromJNI()
public native String stringFromJNI();
- 第二步:依据此native办法编写C文件,可以经过指令后或许studio提示生成C++对应的办法函数
//java中stringFromJNI //extern “C” 指定以"C"的办法来完结native函数 extern "C" //JNIEXPORT 宏界说,用于指定该函数是JNI函数。表明此函数可以被外部调用,在Android开发中不行省掉 JNIEXPORT jstring //JNICALL 宏界说,用于指定该函数是JNI函数。,无实际意义,可是不行省掉 JNICALL //以留意到jni的取名规矩,一般都是包名 + 类名,jni办法只是在前面加上了Java_,并把包名和类名之间的.换成了_ Java_com_yc_testjnilib_NativeLib_stringFromJNI(JNIEnv *env, jobject /* this */) { //JNIEnv 代表了JNI的环境,只要在本地代码中拿到了JNIEnv和jobject //JNI层完结的办法都是经过JNIEnv 指针调用JNI层的办法访问Java虚拟机,然后操作Java目标,这样就能调用Java代码。 //jobject thiz //在AS中主动为咱们生成的JNI办法声明都会带一个这样的参数,这个instance就代表Java中native办法声明地点的 std::string hello = "Hello from C++"; //考虑一下,为什么直接回来字符串会呈现错误提示? //return "hello"; return env->NewStringUTF(hello.c_str()); }
-
举一个比如
- 例如在 NativeLib 类的native stringFromJNI()办法,程序会主动在JNI层查找 Java_com_yc_testjnilib_NativeLib_stringFromJNI 函数接口,如未找到则报错。如找到,则会调用native库中的对应函数。
5.2 C/C++调用Java
- Native层调用Java层的类的字段和办法的操作进程
- 第一步:创立一个Native C++的Android项目,创立 Native Lib 项目
- 第二步:在cpp文件夹下创立:calljnilib.cpp文件,calljnilib.h文件(用来声明calljnilib.cpp中的办法)。
- 第三步:开端编写装备文件CmkaeLists.txt文件。运用add_library创立一个新的so库
- 第四步:编写 calljnilib.cpp文件。由于要完结native层调用Java层字段和办法,所以这儿界说了两个办法:callJavaField和callJavaMethod
- 第五步:编写Java层的调用代码此处要留意的是调用的类的类名以及包名都要和c++文件中声明的共同,不然会报错。具体看:CallNativeLib
- 第六步:调用代码进行测验。然后检查测验成果
5.3 Java调三方so中API
- 直接拿前面事例的 calljnilib.so 来测验,可是为了完结三方调用还需求对文件进行改造
- 第一步:要完结三方so库调用,在 calljnilib.h中声明两个和 calljnilib.cpp中对应的办法:callJavaField和callJavaMethod,一般状况下这个头文件是第三方库一同供给的给外部调用的。
- 第二步:对CMakeLists装备文件改造。首要是做一些库的装备操作。
- 第三步:编写 third_call.cpp文件,在这内部调用第三方库。这儿需求将第三方头文件导入进来,假如CmakeLists文件中没有声明头文件,就运用#include “include/calljnilib.h” 这种办法导入
- 第四步:最终测验下:callThirdSoMethod(“com/yc/testjnilib/HelloCallBack”,”updateName”);
5.4 Java动态调C++
- 先说一下静态调C++的问题:
- 在完结stringFromJNI()时,可以看到c++里边的办法名很长 Java_com_yc_testjnilib_NativeLib_stringFromJNI。
- 这是jni静态注册的办法,依照jni标准的命名规矩进行查找,格局为Java_类途径_办法名。Studio默许这种办法名字太长了,能否设置短一点。
- 程序运转功率低,由于初度调用native函数时需求依据依据函数名在JNI层中查找对应的本地函数,然后树立对应联系,这个进程比较耗时。
- 动态注册办法处理上面问题
- 当程序在Java层运转System.loadLibrary(“testjnilib”);这行代码后,程序会去载入testjnilib.so文件。
- 于此一同,发生一个Load事件,这个事件触发后,程序默许会在载入的.so文件的函数列表中查找JNI_OnLoad函数并履行。与Load事件相对,在载入的.so文件被卸载时,Unload事件被触发。
- 此刻,程序默许会去载入的.so文件的函数列表中查找JNI_OnLoad函数并履行,然后卸载.so文件。
- 因而开发者常常会在JNI_OnLoad中做一些初始化操作,动态注册便是在这儿进行的,运用env->RegisterNatives(clazz, gMethods, numMethods)。
- 动态注册操作进程:
- 第一步:由于System.loadLibrary()履行时会调用此办法,完结JNI_OnLoad办法。
- 第二步:调用FindClass找到需求动态注册的java类【界说要相关的对应Java类】,留意这个是native办法那个类的途径字符串
- 第三步:界说一个静态数据(JNINativeMethod类型),里边存放需求动态注册的native办法,以及参数称号
- 第四步:经过调用jni中的RegisterNatives函数将注册函数的Java类,以及注册函数的数组,以及个数注册在一同,这样就完结了绑定。
- 动态注册优势剖析
- 比较静态注册,动态注册的灵活性更高,假如修改了native函数地点类的包名或类名,仅调整native函数的签名信息即可。
- 还有一个优势:动态注册,java代码不需求更改,只需求更改native代码。
- 功率更高:经过在.so文件载入初始化时,即JNI_OnLoad函数中,先即将native函数注册到VM的native函数链表中去,后续每次java调用native函数时都会在VM中的native函数链表中找到对应的函数,然后加快速度。
06.一些技能原理
6.1 JNIEnv创立和开释
- JNIEnv的创立办法
- C 中——JNIInvokeInterface:JNIInvokeInterface是C言语环境中的JavaVM结构体,调用 (AttachCurrentThread)(JavaVM, JNIEnv*, void) 办法,可以获得JNIEnv结构体;
- C++中 ——_JavaVM:_JavaVM是C++中JavaVM结构体,调用jint AttachCurrentThread(JNIEnv** p_env, void* thr_args) 办法,可以获取JNIEnv结构体;
- JNIEnv的开释:
- C 中开释:调用JavaVM结构体JNIInvokeInterface中的(DetachCurrentThread)(JavaVM)办法,可以开释本线程的JNIEnv
- C++ 中开释:调用JavaVM结构体_JavaVM中的jint DetachCurrentThread(){ return functions->DetachCurrentThread(this); } 办法,就可以开释 本线程的JNIEnv
- JNIEnv和线程的联系
- JNIEnv只在当时线程有用:JNIEnv只是在当时线程有用,JNIEnv不能在线程之间进行传递,在同一个线程中,多次调用JNI层方便,传入的JNIEnv是同样的
- 本地办法匹配多个JNIEnv:在Java层界说的本地办法,可以在不同的线程调用,因而可以接受不同的JNIEnv
6.2 动态注册的原理
- 在Android源码开发环境下,大多选用动态注册native办法。
- 利用结构体JNINativeMethod保存Java Native函数和JNI函数的对应联系;
- 在一个JNINativeMethod数组中保存一切native函数和JNI函数的对应联系;
- 在Java中经过System.loadLibrary加载完JNI动态库之后,调用JNI_OnLoad函数,开端动态注册;
- JNI_OnLoad中会调用AndroidRuntime::registerNativeMethods函数进行函数注册;
- AndroidRuntime::registerNativeMethods中终究调用jni RegisterNativeMethods完结注册。
- 动态注册原理剖析
- RegisterNatives 办法的实质是直接经过结构体指定映射联系,而不是比及调用 native 办法时查找 JNI 函数指针,因而动态注册的 native 办法调用功率更高。
- 此外,还能削减生成 so 库文件中导出符号的数量,则可以优化 so 库文件的体积。
6.3 注册JNI流程图
- 提到了注册 JNI 函数(树立 Java native 办法和 JNI 函数的映射联系)有两种办法:静态注册和动态注册。
- 剖析下静态注册匹配 JNI 函数的履行进程
- 第一步:以 loadLibrary() 加载 so 库的履行流程为头绪进行剖析的,终究定位到 FindNativeMethod() 这个办法。
- 第二步:检查
java_vm_ext.cc
中FindNativeMethod办法,然后看到jni_short_name和jni_long_name,获取native办法对应的短称号和长称号。 - 第三步:在
java_vm_ext.cc
,经过FindNativeMethodInternal查找现已加载的so库中查找,先查找短称号,然后再查找长称号 - 第四步:树立内部数据结构,树立 Java native 办法与 JNI 函数的函数指针的映射联系,调用 native 办法,则直接调用已记录的函数指针。
07.JNI遇到的问题
7.1 混杂的bug
- 在Android工程中要排除对native办法以及地点类的混杂(java工程不需求),不然要注册的java类和java函数会找不到。proguard-rules.pro中增加。 # 设置一切 native 办法不被混杂 -keepclasseswithmembernames class * { native ; } # 不混杂类 -keep class com.yc.testjnilib.** { *; }
7.2 留意字符串编译
- 比方:对于JNI办法来说,运用如下办法回来或许调用直接崩溃了,有点搞不懂原理? env->CallMethod(objCallBack,_methodName,”123″);
- 这段代码编译没问题,可是在运转的时分就报错了: JNI DETECTED ERROR IN APPLICATION: use of deleted global reference
- 终究定位到是最终一个参数需求运用jstring而不能直接运用字符串表明。如下所示: //考虑一下,为什么直接回来字符串会呈现错误提示?为何这样规划…… //return “hello”; return env->NewStringUTF(hello.c_str());