Android JNI 开发入门指南
1. 【JNI介绍】
-
What (是什么): JNI (JAVA Native Interface)java本地接口。
-
通过JNI可以实现Java和本地代码(C/C++)之间相互调用。
-
JNI可以看做是翻译官,实际上就是一套协议。
-
因此类似于AIDL(安卓接口定义语言),aidl是用于进程间通信的一套协议(接口)。
-
-
Why (为什么要用):
-
由于java的跨平台能力,导致了一些弊端,运行在虚拟机上,无法真正直接的操作系统硬件。
-
JNI则可以扩展JAVA的能力,让java也能通过它来操作硬件驱动。(智能家居,物联网…)
-
java是解释型语言,运行效率相对较低,而C/C++的效率要高很多,通过JNI可以把耗时操作放在C/C++中实现可以提高Java程序的运行效率。
-
java代码编译的字节码文件安全性较差, 可以通过jni 把重要的敏感的业务逻辑放到c/c++去实现,由于c/c++反编译比较困难,因此安全性较高。
-
代码移植,复用已经存在的c代码(opencv,ffmpeg…)
-
C历史悠久,通过JNI可以调用优秀的C开源类库。
-
-
How (怎么用):
- 熟练Java
- C/C++ 能看懂 会调用。
- JNI开发流程 NDK native develop kit
2. 【NDK(JNI)开发介绍】
-
[交叉编译]
-
在一个平台上去编译另一个平台上可以执行的本地代码。
-
交叉编译的实现原理: 模拟不同平台的特性去编译代码。
-
Android系统运行的 CPU 架构 :ARM,X86,mips
-
-
[NDK (native Develop Kit),谷歌提供的: 用作JNI开发的交叉编译工具集。]
△安装介绍;如果电脑与JDK都是32位的则选择 android-ndk-r9d-windows-x86 的开发工具包。
64为的则选择 android-ndk-r9d-windows-x86_64 的开发工具包。
△使用介绍:解压就能用
△NDK解压后的:常用目录,文件介绍:
* docs: 帮助文档 * platforms : 平台版本文件夹,使用时选择项目支持的最小版本号所对应的文件夹,每一个版本号的文件夹中包含了不同cpu架构的资源文件 [例如]:我的安卓项目设置的最低支持SDK版本为 api-14,则应该选择使用: platforms/android-14下的头文件,与.so文件。 * include : 存储jni开发中常用的 .h头文件 * lib : google打包好的 提供给开发者使用的:.so文件,.o文件。 * samples : google官方提供的样例工程 可以参考进行开发 * android-ndk-r9d\build\tools : linux系统下的批处理文件 在交叉编译时会自动调用 * ndk-build.cmd :交叉编译的命令。(执行它就可以吧c代表编译成安卓应用可以支持的 .so文件) * cdt : eclipse的插件 高亮C代码 C的代码提示。(一般在eclipse中会自带此插件)
-
【[JNI-Hello World】
△需求:在Android程序中点击一个Button,实现调用C语言的一个函数,该函数返回一个字符串(Hello World)!
△核心原理:在java中声明一个native方法,然后在C语言代码中实现这个native方法,然后在java代码中调用这个方法。
☆实现步骤: —可以参考:android-ndk-r9d-windows-x86.zip\android-ndk-r9d\samples\hello-jni 这个示例工程。
-
简要步骤如下:
- 创建一个android工程
- java代码中写声明native方法
- 创建jni目录,编写c代码,方法名字要对应(有一定的命名格式)。
- 编写Android.mk文件,如果需要还需Application.mk
- NDK编译生成动态链接库
- java代码load动态库.调用native代码
-
[详细步骤如下]:
-
[1]:首先写java代码, 声明本地方法 用到native关键字 本地方法不用去实现。
例: /** *1. 声明一个java本地方法,利用native关键字,不需要方法体。 */ public native String sayHelloFromC();
-
2:然后在安卓项目根目录下创建jni文件夹, 在jni文件夹下创建 hello.c文件。需要包含进两个头文件:include<stdio.h> 与 include<jni.h>
△在hello.c中:创建本地函数,本地函数命名规则为: Java_包名_类名_本地方法名
△按照JNI本地函数命名规则,定义我们自己需要的函数。函数的返回值类型可以参照JNI协议。
在jni.h头文件中:
typedef const struct JniNativeInterface* JNIENV ;//因此JNIENV 类型,实际上就是JniNativeInterface这个结构体的指针的别名。-
因此JNIEnv 是JniNativeInterface这个结构体的一级指针
-
参数:JNIENV* env 就是结构体JniNativeInterface这个结构体的二级指针(因为它接收的就是一级指针的地址)
-
JniNativeInterface 这个结构体定义了大量的函数指针,因此就可以通过结构体指针来访问结构体中定义的函数指针所指向的函数了。
-
(env)-> 即可调用结构体中的函数指针。(因为env为二级指针,因此需要加,取出一级指针,然后对一级指针使用 -> )
-
第二个参数jobject thiz: 调用本地函数的java对象就是这个jobject,在本例中也就是MainActivity.this
△:注意:我们调用的所有的JNI本地代码函数,都需要带这两个参数。
例:hello.c 如下: #include<stdio.h> #include<jni.h> //由于程序的入口是MainActivity,因此无需再此处创建 main()函数 /** * 添加一个C语言本地函数:命名规则为: Java_包名_类名_本地方法名 。(本地方法名就是调用这个c函数的java native 方法名) */ jstring Java_com_hzy_jnihelloworld_MainActivity_sayHelloFromC(JNIEnv* env,jobject thiz){ //定义一个C语言的字符串 char* hello_str = "helloWorld!"; //通过:结构体(JniNativeInterface)的二级指针 env 调用结构体中声明的函数指针所对应的函数 (jstring NewStringUTF(JNIEnv*, const char*);) return (*env)->NewStringUTF(env,hello_str); }
-
-
[3]:在项目的 jni目录下 创建Android.mk (makefile) 告诉编译器.c的源文件在什么地方,要生成的编译对象的名字是什么
例:Android.mk 如下: LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := hello #指定了生成的动态链接库的名字 LOCAL_SRC_FILES := hello.c #指定了C的源文件叫什么名字 include $(BUILD_SHARED_LIBRARY)
-
4:用cmd进入项目的根目录,然后调用命令 ndk-build 编译c代码,编译成功生成的动态链接库.so文件,
该文件的位置 lib->armeabi->libhello.so 。(右键项目–>refresh即可看到该文件出现。)注意:命令使用前需要将 ndk 解压后的目录,添加到系统的path环境变量中。
如果运行的设备平台为x86,则需新建一个 Application.mk文件,在里面进行配置,实现让ndk编译出多个平台的so文件。--提高兼容性
-
[5]:在java代码中加载动态链接库 System.loadlibrary(“动态链接库的名字”); 也就是 jni/Android.mk 中 LOCAL_MODULE所指定的名字。
//使用前需要加载编译成功后生成的,动态链接库文件 static{ System.loadLibrary("hello"); }
-
例: //调用本地方法 String result = sayHelloFromC(); //实际上会调用 .so文件中所定义的对应的本地代码函数。 Toast.makeText(getApplicationContext(), result, 0).show();
-
-
3. 【jni开发中的常见错误 】
-
△ [java.lang.UnsatisfiedLinkError: Native method not found: 本地方法没有找到 ]
- 本地函数名写错
- 忘记加载.so文件 没有调用System.loadlibrary(); //一般在静态代码块中加载
-
△[findLibrary returned null ]
-
System.loadLibrary(“hello”); //注意:加载动态链接库时,name为 :jni/Android.mk 中 LOCAL_MODULE所指定的名字。
-
平台类型错误:例如:把只支持arm平台的.so文件部署到了 x86cpu的设备上。 —因为默认情况下会生成arm平台的so文件。
解决方案:
* 在jni目录下创建 Application.mk 文件,并在里面指定: APP_ABI := x86 #则只支持x86平台 APP_ABI := armeabi x86 #则只支持arm平台与x86平台 APP_ABI := all #全部支持 APP_PLATFORM := android-14 #此处配置成自己项目所支持的最低的AndroidSDK版本,交叉编译时就不会 Warning! 完成配置后,cmd进入项目根目录,然后ndk-build重新编译。即可!
-
-
△[用javah 命令:来生成头文件,解决自己写本地代码函数时,函数名格式写错的问题。]
-
如果是jdk 1.7以上,则到项目 src目录下运行 javah命令
-
如果是jdk 1.6,则到项目的bin目录下 classes文件夹下运行 javah命令
-
写法:javah (native方法声明的java类的全类名)
例:首先在mainActivity中编写一个native方法:public native String sayHelloFromC(); //native方法的方法体位于本地代码中
然后到cmd中找到项目的...\src> javah com.hzy.jnihelloworld.MainActivity, 就会将该类中所有的 native方法,生成对应的本地代码函数到头文件中。 刷新项目即可看到:src/com_hzy_jnihelloworld_MainActivity.h :方法名就生成成功了。 /* * Class: com_hzy_jnihelloworld_MainActivity * Method: sayHelloFromC * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_hzy_jnihelloworld_MainActivity_sayHelloFromC(JNIEnv *, jobject);
-
-
△:解决CDT插件报错的问题(在.c文件中使用 JNIEnv, jstring这些关键字时,发现报错!)
右键单击项目选择 properties :
选中:c/c++ general->paths and symbols->include选项卡下->点击add…->file system
选择ndk目录下 的platforms文件夹 对应平台下(项目支持的最小版本)usr 目录下 arch-arm -> include
(apply)确定后 会解决代码提示和报错的问题。
4. 【JNI简便开发流程】
-
前言:一个标准的JNI程序开发步骤就如上文所示,但是过于繁琐,因此安卓也提供了简便的开发流程。
-
[实现步骤]:
-
[1]:【首先编写java代码, 用native 声明本地方法。】
-
2:【添加本地支持 右键单击项目->andorid tools->add native surport,然后指定生成的so文件的名字即可。】
* 如果发现 finish不能点击,则需要给工作空间指定 ndk目录的位置(工具包解压后
-