浅析Android NDK开发
本文主要介绍JNI、NDK相关知识,使用两个例子来阐述如何进行JNI和Android NDK开发。
一、 JNI和NDK简介
JNI全称Java Native Interface,Java本地接口,这个接口提供了Java与C/C++互相调用的能力。在Java中用native关键字标识该方法为本地方法,方法具体实现由C/C++代码实现。Java是跨平台的编程语言,而使用JNI会破坏该特性,在实际工作中使用JNI需要针对各系统平台生成不同的动态和静态库。
NDK全称Native Development Kit,原生开发工具包,提供了在Android应用中利用C/C++代码的工具。
二、 Java调用C代码实现时间格式化
1. 编写native方法
```
package com.example.jni;
import java.util.Date;
public class HelloJni {
/**
* 格式化时间
*/
public static native String formatDate(Date date, String format);
}
```
2. native方法生成头文件
进入src目录,编译javac com.example.jni.HelloJni.java,然后执行javah com.example.jni.HelloJni,可以看到在生成了com_example_jni_HelloJni.h,文件内容如下:
```
#include
#ifndef _Included_com_example_jni_HelloJni
#define _Included_com_example_jni_HelloJni
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_jni_HelloJni
* Method: formatDate
* Signature: (Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_jni_HelloJni_formatDate
(JNIEnv *, jclass, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
```
代码解析: 1. 头文件中引入了jni.h、jni_md.h(jni.h内引入)文件,两个文件位与$JAVA_HOME/include和$JAVA_HOME/include/darwin(与操作系统有关)目录下,文件中定义了JNI所支持的类型、接口和方法,在代码中我们可以看到java数据类型与C/C++的映射关系和结构体实的对象, 映射表格可见下表,大部分情况java基础数据类型在前加"j"即为对应的C/C++类型。
字符(方法签名、返回类型用) C类型 JAVA类型
V void void
Z jboolean boolean
I jint int
J jlong long
D jdouble double
F jfloat float
B jbyte byte
C jchar char
S jshort short
[I jintArray int[]
Ljava/lang/String; jstring char*
L${以/连接的全路径类名}; jobject ${类}
2. 方法解析: 方法名由JAVA_${package}_${class}_${method}组成,在Java调用native方法JVM会时会自动定位到该库文件的该方法,
该方法第一个参数类型JNIEnv表示Java运行环境,全局唯一,是JVM有JNI层的体现,该类型实现在jni.h中的struct, 在jni.h中定义了一系列方法操作获取Java的类和方法、创建对象、执行方法等,通过其可以获取到java中的类jclass、方法jmethodID和执行方法等,第二个参数是jclass,表示Java native方法为类方法,指向Java类,若Java方法为对象方法,该参数为jobject, 表示当前调用native方法的对象。
下面将举出JNIEnv该类型常用的方法
1. FindClass(env,${全路径类名}), 该方法获取Java类,返回结果为jclass;
2. GetMethodID(env,jclass,${方法名},${方法签名}) 获取Java类的方法类型jmethodID,其中方法签名生成"$(参数标识列表)"+返回标识字符,在刚生成的头文件可以看到签名为"(Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String",还可使用javap -s com.${全路径类名} 中的descriptor,具体参数签名规则为”L+全限定类名+;”三部分组成,其中全限定类名以”/”分隔,而不是用”.”或”_”分隔。如Java方法:long fun(int n, String str, int[] arr);其方法签名为(ILjava/lang/String;[I)J括号里的内容分成三部分,之间没有空格,即”I”,”Ljava/lang/String;”和”[I”,分别代表int,String,int[]。括号后面是返回值类型签名,J代表long型。特列而言,构造函数也是方法,方法名为init;
3. NewObject,该方法是生成JAVA对象,需传入构造函数的JMethodId和参数;
4. CallObjectMethod、CallStaticObjectMethod、CallStaticBooleanMethod等,这些方法是执行Java的对象方法和类方法。
3. 实现头文件, com_example_jni_HelloJni.c
```
#include "com_example_jni_HelloJni.h"
JNIEXPORT jstring JNICALL
Java_com_example_jni_HelloJni_formatDate(JNIEnv *env, jclass clazz, jobject date, jstring format)
{
jclass formatClazz = (*env)->FindClass(env, "java/text/SimpleDateFormat"); //获取SimpleDateFormat.class
jmethodID constructorMethod =
(*env)->GetMethodID(env, formatClazz, "
jobject simpleDateFormat = (*env)->NewObject(env, formatClazz, constructorMethod, format); //生成对象
jmethodID formatMethod =
(*env)->GetMethodID(env, formatClazz, "format", "(Ljava/util/Date;)Ljava/lang/String;"); //获取format(Date)方法
jstring result = (*env)->CallObjectMethod(env, simpleDateFormat, formatMethod, date); //执行方法
return result;
}
```
4. 编译C代码生成动态链接库
1. 编译生成链接文件 gcc -I $JAVA_HOME/include/ -I $JAVA_HOME/include/darwin/ -c -fPIC com_example_jni_HelloJni.c
2. 生成动态链接库文件 gcc -shared -fPIC -o libhelloJni.dylib com_example_jni_HelloJni.o
注意: C/C++非跨平台,生成的so文件无法在所在系统平台使用,需要单独编译或使用交叉编译,本文件使用gcc编译,也可使用g++等编译。动态链接库文件后缀名windows下为dll文件,Mac为dylib,Linux为so文件。
5. JAVA调用
使用System.loadLibrary(${去除lib前缀和扩展名的库名})或System.load(${绝对路径}),使用前者引入文件必须位与环境变量path目录下。
```
package com.example.jni;
import java.util.Date;
public class Main {
static {
System.load("/xx/xx/libhelloJni.dylib");
//System.loadLibrary("helloJni");
}
public static void main(String[] args) {
System.out.println(HelloJni.formatDate(new Date(), "yyyy-MM-dd HH:mm:ss"));
}
}
```
三、 NDK方式实现BsDiff(Cmake方式)
1. 环境下载
AndroidStudio中配置下载NDK,SDK-Manager里下载CMake编译工具
2. Android Studio NDK环境集成
配置build.gradle
```
android{
defaultConfig {
...
externalNativeBuild {
cmake {
abiFilters "armeabi","arm64-v8a","armeabi-v7a","x86","x86_64" //指定生成的so文件支持的CPU和系统架构
}
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt" // CMakeLists.txt 路径
}
}
...
}
```
3. CMakeLists.txt配置
CMake语法规则详见CMake官网
```
#该指令的主要作用就是将指定的源文件生成链接文件,生右apk会自动添加到libs中
add_library{
bsdiff
SHARED #STATIC静态链接库等
#{源文件1}
#{其它源文件}
}
#引入其它动态、静态库并命名,示例假入了android log库(打印Log.x)
find_library{
log-lib
log
}
#链接库
target_link_libraries{
bsdiff
${log-lib}
${其它find_libary引入的库}
}
```
4. bsdiff源文件下载
下载bsdiff-4.3、bzip2-1.0.6,解压后把.c和.h文件放入项目新建好的cpp目录下,修改bsdiff和bspatch中引入了头文件
5. 配置源文件至CMake
添加bsdiff和bzip下的所有C文件名至Cmake的add_libray下,当文件较多时,可使用Cmake 的File块简写。
6. JNI代码编写
新建源文件main.cpp并加入到cmake下,文件内容如下
```
#include
#ifndef _Included_com_example_jni_HelloJni
#define _Included_com_example_jni_HelloJni
//外部实现,本文件为C++文件,extern C 必须,表示按C语言方式编译,若按C++方法编译则会重命名方法导致查找方法失败
extern "C" {
int bspatch_main(int i, char *pString[4]);
int bsdiff_main(int i, char *pString[4]);
}
//注意实现native方法的方法名为JAVA_${用_全路径类名}_${方法},若想使用自己定义的方法名,则需使用动态注册方法(实现JNI_OnLoad)
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_example_jni_BsDiff_diffNative(JNIEnv *env, jclass clazz, jstring path1, jstring path2,
jstring output) {
//定义参数
const char *argv[4];
argv[0] = "";
argv[1] = env->GetStringUTFChars(path1, 0);
argv[2] = env->GetStringUTFChars(path2, 0);
argv[3] = env->GetStringUTFChars(output, 0);
int success = diff_main(4, const_cast
//释放char*
env->ReleaseStringUTFChars(path1, argv[1]);
env->ReleaseStringUTFChars(path2, argv[2]);
env->ReleaseStringUTFChars(output, argv[3]);
return 0 == success ? JNI_TRUE : JNI_FALSE;
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_example_jni_BsDiff_patchNative(JNIEnv *env, jclass clazz, jstring path1, jstring patch,
jstring output) {
const char *argv[4];
argv[0] = "";
argv[1] = env->GetStringUTFChars(path1, 0);
argv[2] = env->GetStringUTFChars(output, 0);
argv[3] = env->GetStringUTFChars(patch, 0);
int success = patch_main(4, const_cast
env->ReleaseStringUTFChars(path1, argv[1]);
env->ReleaseStringUTFChars(output, argv[2]);
env->ReleaseStringUTFChars(patch, argv[3]);
return 0 == success ? JNI_TRUE : JNI_FALSE;
}
#endif
```
7. 执行
Java直接调用native方法即可。
注: 正常而言,差分包的生成即diff方法在服务端调用,相应的移动代码至服务端项目,根据部署平台和系统编译成相应的动态链接库格式即可。