JNI 与 NDK
JNI
定义:Java Native Interface
,即 Java
本地接口
作用: 使得Java
与 本地其他类型语言(如C、C++
)交互,Java代码与C,C++语言代码相互调用
注意:
JNI
是Java
调用Native
语言的一种特性JNI
是属于Java
的,与Android
无直接关系
背景:Java
具备跨平台的特点,所以Java
与 本地代码交互的能力非常弱
解决方案: 采用 JNI
特性 增强 Java
与 本地代码交互的能力
实现步骤
1、在Java中声明Native方法(即需要调用的本地方法)
2、编译上述Java源文件javac(得到.class文件)
3、通过javah命令导出JNI的头文件(.h文件)
4、使用Java需要交互的本地代码,实现在Java中声明的Native方法
如Java需要与C++交互,那么就用C++实现 Java的Native方法
5、编译.so库文件
6、通过Java命令执行 Java程序,最终实现Java调用本地代码
NDK
定义:Native Development Kit,是 Android的一个工具开发包。NDK是属于 Android
的,与Java
并无直接关系
作用:快速开发C
、 C++
的动态库,并自动将so
和应用一起打包成 APK
即可通过 NDK
在 Android
中 使用 JNI
与本地代码(如C、C++)交互
应用场景:在Android的场景下 使用JNI
特点
1.运行效率高
在开发要求高性能的需求中,采用C/C++更加有效率
如果使用本地代码(C/C++)执行算法,能大大提高算法的执行效率
2.代码安全性高
java是半解释型语言,容易被反汇编后得到源码,而本地有些代码语言则不会,如C/C++
3.功能扩展性好
可方便地使用其他开发语言的开源库
4.易于代码复用和移植
用本地代码开发的代码不仅可在Android中使用,还可嵌入到其他类型平台上使用
注意
提供了把.so和.apk打包的工具,而JNI开发则没有,只要把.so文件放到文件系统的特定位置
NDK提供的库有限,仅用于处理算法效率和敏感的问题
提供了交叉编译器,用于生成特定的cpu平台动态库
使用步骤
1.配置 Android NDK环境
2.创建 Android 项目,并与 NDK进行关联
3.在 Android 项目中声明所需要调用的 Native方法
4.使用 Android需要交互的本地代码 实现在Android中声明的Native方法
比如 Android 需要与 C++ 交互,那么就用C++ 实现 Java的Native方法
5.通过 ndk - bulid 命令编译产生.so库文件
6.编译 Android Studio 工程,从而实现 Android 调用本地代码
具体使用——AS 2.2 以下
1、配置 Android NDK环境
下载Android NDK工具包,如android-ndk-r14b-darwin-x86_64.zip
官网下载:https://developer.android.com/ndk/downloads/index.html
解压 NDK包
解压路径不要出现空格和中文
建议将解压路径设置为Android Studio的SDK根目录中(与build-tools同级),并命名为ndk-bundle。此举的好处:启动Android Studio时会自动检查NDK,并直接将配置添加到ndk.dir中,使用时不用配置与NDK相关连的配置
安装 & 配置NDK
NDK环境变量配置类似JDK
https://blog.csdn.net/yuanzhihua126/article/details/78732371
2、关联AS项目与NDK
在Gradle的local.properties中添加配置
ndk.dir=ndk-bundle路径,如:ndk.dir=E:/AS/SDK/ndk-bundle
如NDK目录放在SDK目录中,并命名为ndk-bundle,则此配置会自动添加
在Gradle的gradle.properties中添加配置
android.useDeprecatedNdk=true,用于对旧版本的支持
在Gradle的build.gradle的android的defaultConfig节点中添加ndk节点
ndk{
//.so文件名
moduleName "hello_jni"
//使用STL标准库,默认无法使用
stl "stlport_static"
//log表示加入Android的调试日志,只要再导入#include <android/log.h>就可使用_android_log_print方法打印日志到logcat中
ldLibs "log"
}
3、创建本地代码文件
将创建好的test.cpp文件放到工程目录中的src/main/jni文件夹中,没有则手动创建
JNI与java数据类型对应表见本文件夹
//如果本地代码是C++(.cpp或者.cc),要使用extern "C" {}把本地方法包裹
extern "C"{
//JNIEXPORT 和 JNICALL不能省
JNIEXPORT jstring JNICALL Java_scut_carson_1ho_ndk_1demo_MainActivity_getFromJNI(JNIEnv *env, jobject obj ){
// 参数说明
// 1. JNIEnv:代表了VM里面的环境,本地的代码可以通过该参数与Java代码进行操作
// 2. obj:定义JNI方法的类的一个本地引用(this)
//方法名定义说明:
//格式:Java_包名_类名_Java需要调用的方法名,如此处包名为: scut.carson_ho.ndk_demo
//Java首字母必须大写
//对于包名,符号.改成_;_改成_1
return env -> NewStringUTF("Hello, I am JNI.");
// 上述代码是返回一个String类型的字符串
}
}
4、创建Android.mk文件
将创建好的Android.mk文件放在src/main/jn文件夹下,没有则手动创建
LOCAL_PATH:=$(call my-dir)
// 设置工作目录,而my-dir则会返回Android.mk文件所在的目录
include $(CLEAR_VARS)
// 清除几乎所有以LOCAL—PATH开头的变量(不包括LOCAL_PATH)
LOCAL_MODULE:=hello_jni
// 设置模块的名称,即编译出来.so文件名
// 注,要和上述步骤中build.gradle中NDK节点设置的名字相同
LOCAL_SRC_FILES:=test.cpp
// 指定参与模块编译的C/C++源文件名
include $(BUILD_SHARED_LIBRARY)
// 指定生成的静态库或者共享库在运行时依赖的共享库模块列表。
5、创建Application.mk文件
将创建好的Application.mk文件放在src/main/jn文件夹下,没有则手动创建
*Application.mk*
APP_ABI := armeabi
// 最常用的APP_ABI字段:指定需要基于哪些CPU平台的.so文件
// 常见的平台有armeabi x86 mips,其中移动设备主要是armeabi平台
// 默认情况下,Android平台会生成所有平台的.so文件,即同APP_ABI := armeabi x86 mips
// 指定CPU平台类型后,就只会生成该平台的.so文件,即上述语句只会生成armeabi平台的.so文件
6、编译上述文件,生成.so库文件
编译成功后,在src/main/
会多了两个文件夹libs
& obj
,其中libs
下存放的是.so
库文件
在src/main/
中创建一个名为jniLibs
的文件夹,并将上述生成的so文件夹放到该目录下
.so
文件必须放在对应的cpu平台的文件夹下
cd .../src/main/jni //进入到上述三个文件的目录下
ndk-build //运行NDK编译命令
7、在AS项目中使用NDK实现的JNI功能
public class MainActivity extends Activity {
//加载生成的.so库文件,注意.so文件名
static {
System.loadLibrary("hello_jni");
}
//定义在JNI中实现的方法,固定写法,必须加native关键字
public native String getFromJNI();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//触发JNI方法
getFromJNI();
...
具体使用——AS 2.2以上
AS 2.2 以上内部已经集成了NDK,因此只需在AS内部进行配置
1、创建含有NDK功能的工程
2、工程中根目录中创建cpp文件夹,放入.cpp文件
3、在AS项目中使用NDK实现的JNI功能
与上述的第七步一样,直接调用
JNI与NDK关系
JNI是实现的目的,NDK是Android中实现JNI的手段,即在Android开发中,通过NDK实现JNI的功能
应用实例:APP监听自身被卸载
方案
- 监听系统卸载广播:只能监听到其他应用的卸载广播,无法监听到自己是否被卸载。
- 读取系统 log:第三方软件卸载无法得知。
- 静默安装另一个程序,监听自己是否被卸载:需要 root 权限。
- Java 线程轮询,监听/data/data/{package-name}目录是否存在:卸载 app,进程退出,线程也被销毁。
- C 进程轮询,监听/data/data/{package-name}目录是否存在:目前业界普遍采用的方案
原理
单纯的 Java 层代码是无法监听自身卸载的,使用 C 语言在底层实现
借助 Linux 进程 fork 出来的 C 进程在应用被卸载后不会被销毁,监听/data/data/{package-name}目录是否存在,如果不存在,就证明应用被卸载了
实现
- fork()子进程
- 创建监听文件
- 初始化 inotify 实例
- 注册监听事件
- 调用 read 函数开始监听
- 卸载反馈统计
测试场景
- 正常卸载
- 断网卸载
- 清除数据(5.0 以上不支持)
- kill 进程(5.0 以上不支持)
- 插拔 USB 线
- 覆盖安装
- 内部存储移到 SD 卡
- 开机监听(官方不推荐)
- 打开浏览器(5.0 以上部分机型无法开启)