基本概念介绍
JNI 全称 Java Native Interface,Java 本地化接口,可以通过 JNI 调用系统提供的 API。操作系统,无论是 Linux,Windows 还是 Mac OS,或者一些汇编语言写的底层硬件驱动都是 C/C++ 写的。Java和C/C++不同 ,它不会直接编译成平台机器码,而是编译成虚拟机可以运行的Java字节码的.class文件,通过JIT技术即时编译成本地机器码,所以有效率就比不上C/C++代码,JNI技术就解决了这一痛点,JNI 可以说是 C 语言和 Java 语言交流的适配器、中间件。
NDK(Native Development Kit) : 原生开发工具包,即帮助开发原生代码的一系列工具,包括但不限于编译工具、一些公共库、开发IDE等。
NDK 工具包中提供了完整的一套将 c/c++ 代码编译成静态/动态库的工具,而 Android.mk 和 Application.mk 你可以认为是描述编译参数和一些配置的文件。比如指定使用c++11还是c++14编译,会引用哪些共享库,并描述关系等,还会指定编译的 abi。只有有了这些 NDK 中的编译工具才能准确的编译 c/c++ 代码。
CMake 则是一个跨平台的编译工具,它并不会直接编译出对象,而是根据自定义的语言规则(CMakeLists.txt)生成 对应 makefile 或 project 文件,然后再调用底层的编译。
JNI 与 NDK 区别
JNI 作用
JNI的简要流程
最近项目中用到数据验签加密功能,就是对指定请求的数据进行加密处理。
数据加密过程是增长部门实现的一个c项目,接下来的工作就是使用增长部门开发人员提供的c项目进行JNI封装,期间遇到一些坑。
接下来讲解的知识点:
目录文件介绍
新建一个Native C++ 项目
默认moduel下的build.gradle文件部分内容
android {
...
defaultConfig {
...
// CMake的命令参数 可以在 .externalNativeBuild/cmake/debug/{abi}/cmake_build_command.txt 中查到
externalNativeBuild {
cmake {
cppFlags "-fvisibility=hidden"
}
}
}
// 指明CMakeList.txt的路径
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
ndk {
// Specifies the ABI configurations of your native
// libraries Gradle should build and package with your APK.
abiFilters 'armeabi-v7a' // 指定NDK需要兼容的架构
}
}
文件目录结构
业务方只需要将EncryptUtils.java文件和build/intermediates/cmake/debug/obj/armeabi-v7a下的.so文件拷贝到项目中就可以使用.so中提供的功能。
JNI开发流程的步骤
JNI的命名规则
extern "C" JNIEXPORT jstring JNICALL Java_com_ke_inspectionsign_EncryptUtils_encrypt
(JNIEnv *jniEnv, jclass clazz)
jstring 是返回值类型
Java_com_ke_inspectionsign 是包名
EncryptUtils 是类名
encrypt 是方法名
其中JNIExport
和JNICALL
是不固定保留的关键字不要修改
PS:javah 是JDK自带的一个命令,-jni参数表示将class 中用到native 声明的函数生成JNI 规则的函数
将jni文件所在的包拖到Terminal中,执行javac EncryptUtils.java
将java文件拖到Terminal中,执行javah -jni com.xxx.xxx.EncryptUtils
获取class文件的方法属性签名 javap -s EncryptUtils
CMakeLists.txt文件的分析
# 设置头文件搜索路径
include_directories(src/main/cpp/thirdparty/sds)
include_directories(src/main/cpp/thirdparty/cJSON)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
encryptsign # 指定产物.so的名称
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
# 资源路径是相对路径,相对于本CMakeLists.txt所在目录
src/main/cpp/encrypt.c
src/main/cpp/native-lib.cpp
src/main/cpp/thirdparty/sds/sds.c
src/main/cpp/thirdparty/cJSON/cJSON.c
src/main/cpp/thirdparty/cJSON/cJSON_Utils.c
)
add_library:创建一个静态或者动态库,并提供其关联的源文件路径,开发者可以定义多个库,CMake会自动去构建它们。
Gradle可以自动将它们打包进APK中。
第一个参数——encryptsign:是产物.so的的名称
第二个参数——SHARED:是库的类别,是动态的还是静态的
第三个参数——src/main/cpp/native-lib.cpp ...:是库的源文件的路径
参数二的取值
STATIC:静态链接库,当生成可执行程序时进行链接。在Linux下为.a文件
SHARED:动态链接库,可执行程序运行时动态加载并链接,源文件由.c(pp)提供。在Linux下为.so文件
MODULE:模块,可执行程序运行时动态加载,但不链接到可执行程序中。
BUILD_SHARED_LIBS变量决定了默认值,如果为on则为动态的,否则为静态的
可以通过设置CMAKE_LIBRARY_OUTPUT_DIRECTORY变量指定输出的路径
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
# 依赖NDK中的库 使用CMake默认的搜索路径中的本地系统库
find_library( # Sets the name of the path variable
# android系统每个类型的库会存放一个特定的位置,而log库存放在log-lib中
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
# android系统在c环境下打log到logcat的库
log)
find_library( # Sets the name of the path variable.
# 在C语言中使用了AndroidBitmap,需要依赖Bitmap库
jnigraphics-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
jnigraphics
)
find_library:找到一个预编译的库,并作为一个变量保存起来。由于CMake在搜索库路径的时候会包含系统库,并且CMake会检查它自己之前编译的库的名字,所以开发者需要保证开发者自行添加的库的名字的独特性。
第一个参数——log-lib:设置路径变量的名称
第一个参数——log:指定NDK库的名称,这样CMake就可以找到这个库
# 加载自定义库或者第三方库或者.a静态库等目标
# 如果是.a静态库 STATIC 如果是.so库 SHARED
# add_library(crypto STATIC IMPORTED)
# 指定该库文件的路径
#set_target_properties(crypto
PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/lib/libcrypto.a)
# add_library(ssl STATIC IMPORTED)
# set_target_properties(ssl
PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/lib/libssl.a)
# add_library(curl STATIC IMPORTED)
# set_target_properties(curl
PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/lib/libcurl.a)
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
# 为该动态库添加要链接的库 库文件的顺序符合gcc链接顺序的规则,即被依赖的库放在依赖它的库的后面
target_link_libraries( # Specifies the target library.
encryptsign # 输出.so名称
# Links the target library to the log library
# included in the NDK.
ssl
crypto
${log-lib}
z)
target_link_libraries(
[[debug|optimized|general]
target_link_libraries:指定CMake链接到目标库。开发者可以链接多个库,比如开发者可以在此定义库的构建脚本,并且预编译第三方库或者系统库。
第一个参数——encryptsign:指定的目标库
第一个参数——${log-lib}:将目标库链接到NDK中的日志库,
package com.ke.inspectionsign;
// 业务方需要拷贝的.java文件
public class EncryptUtils {
// Used to load the 'encryptsign' library on application startup.
static {
System.loadLibrary("encryptsign"); // encryptsigin
}
/**
* A native method that is implemented by the ' encrypt_lib' native library,
* which is packaged with this application.
*/
public static native String encrypt(String app_version);
}
这里生成jni文件的步骤:
1) 将jni文件所在的包拖到Terminal中,执行javac EncryptUtils.java
2) 将java文件拖到Terminal中,执行javah -jni com.xxx.xxx.EncryptUtils
3) 获取class文件的方法属性签名 javap -s EncryptUtils
4) 由于采用的是静态注册JNI,要求JNI中的java文件路径和业务方调用的java文件路径一致。
JNI 的两种注册方式
静态注册JNI的原理:根据函数名建立 Java 方法和 JNI 函数的一一对应关系。流程如下:
javah packagename.classname
可以生成由包名加类名命名的 jni 层头文件,或执行命名javah -o custom.h packagename.classname
,其中 custom.h 为自定义的文件名;静态注册的方式有两个重要的关键词 JNIEXPORT 和 JNICALL,这两个关键词是宏定义,主要是注明该函数式 JNI 函数,当虚拟机加载 so 库时,如果发现函数含有这两个宏定义时,就会链接到对应的 Java 层的 native 方法。
需要注意几点特殊规则:
优点:
实现比较简单,可以通过 javah 工具将 Java代码的 native 方法直接转化为对应的native层代码的函数;
缺点:
动态注册 JNI 的原理:直接告诉 native 方法其在JNI 中对应函数的指针。通过使用 JNINativeMethod 结构来保存 Java native 方法和 JNI 函数关联关系,步骤:
registerNatives(JNIEnv* env)
注册类的所有本地方法; 当我们使用System.loadLibarary()方法加载so库的时候,Java虚拟机就会找到这个JNI_OnLoad
函数并调用该函数,这个函数的作用是告诉Dalvik虚拟机此C库使用的是哪一个JNI版本,如果你的库里面没有写明JNI_OnLoad()函数,VM会默认该库使用最老的JNI 1.1版本。由于最新版本的JNI做了很多扩充,也优化了一些内容,如果需要使用JNI新版本的功能,就必须在JNI_OnLoad()函数声明JNI的版本。同时也可以在该函数中做一些初始化的动作,其实这个函数有点类似于Android
中的Activity
中的onCreate()
方法。该函数前面也有三个关键字分别是JNIEXPORT
,JNICALL
,jint
。其中JNIEXPORT
和JNICALL
是两个宏定义,用于指定该函数时JNI函数。jint是JNI定义的数据类型,因为Java层和C/C++的数据类型或者对象不能直接相互的引用或者使用,JNI层定义了自己的数据类型,用于衔接Java层和JNI层。
PS:与JNI_OnLoad()函数相对应的有JNI_OnUnload()函数,当虚拟机释放的该C库的时候,则会调用JNI_OnUnload()函数来进行善后清除工作。
动态注册实现步骤:
1. 声明的本地方法
public static native List
2. 编写 JNI 函数的实现
JNIEXPORT jobject JNICALL crash_parse(JNIEnv *env, jclass jcl, jobject jobj) {
ljcrash_address_info info;
//获取ArrayList 对象
jclass jcs_alist = env->GetObjectClass(jobj);
//获取Arraylist的methodid
jmethodID list_get = env->GetMethodID(jcs_alist, "get", "(I)Ljava/lang/Object;");
jmethodID list_size = env->GetMethodID(jcs_alist, "size", "()I");
jint len = env->CallIntMethod(jobj, list_size);
for (int i = 0; i < len; i++) {
jobject addressObj = env->CallObjectMethod(jobj, list_get, i);
//获取Address类
jclass addressClazz = env->GetObjectClass(addressObj);
jmethodID archId = env->GetMethodID(addressClazz, "getArch", "()Ljava/lang/String;");
jstring archValue = (jstring) env->CallObjectMethod(addressObj, archId);
const char * arch = env->GetStringUTFChars(archValue, 0);
LOGV("arch = %s", arch);
// ArrayList addresses; 打印该属性集合的数值
jmethodID addressesMethodID = env->GetMethodID(addressClazz, "getAddresses", "()Ljava/util/ArrayList;");
jobject addressesObj = env->CallObjectMethod(addressObj, addressesMethodID);
jclass addressesClazz = env->GetObjectClass(addressesObj);
//method in class ArrayList 获取集合大小
jmethodID addresses_get = env->GetMethodID(addressesClazz,"get","(I)Ljava/lang/Object;");
jmethodID addresses_size = env->GetMethodID(addressesClazz,"size","()I");
len = env->CallIntMethod(addressesObj, addresses_size);
for(int j = 0; j < len; j++){
jstring addressStr = (jstring) (env->CallObjectMethod(addressesObj, addresses_get, j));
const char * address = env->GetStringUTFChars(addressStr, 0);
LOGV("address = %s", address);
}
}
//获取ArrayList类引用
jclass list_jcs = env->FindClass("java/util/ArrayList");
if (list_jcs == NULL) {
LOGV("ArrayList no find!");
return NULL;
}
//获取ArrayList构造函数id
jmethodID list_init = env->GetMethodID(list_jcs, "", "()V");
//创建一个ArrayList对象
jobject list_obj = env->NewObject(list_jcs, list_init, "");
//获取ArrayList对象的add()的methodID
jmethodID list_add = env->GetMethodID(list_jcs, "add", "(Ljava/lang/Object;)Z");
//获取Symbol类
jclass symbol_cls = env->FindClass("com/ke/crash/ios/jni/Symbol");
//获取Symbol的构造函数
jmethodID symbol_init = env->GetMethodID(symbol_cls, "",
"(Ljava/lang/String;Ljava/lang/String;)V");
// 初始化结构体
// Fatal signal 11 (SIGSEGV), code 2, fault addr 0x72df900500 in tid 19394 (om.ke.crash.ios)
ljcrash_symbol_info symbol_info = {};
symbol_info.address = "上地";
symbol_info.symbol = "com.ke.crash.ios";
// 填充ArrayList
for (int i = 0; i < len; i++) {
jobject symbol_obj = env->NewObject(symbol_cls, symbol_init, env->NewStringUTF(symbol_info.address),
env->NewStringUTF(symbol_info.symbol));
env->CallBooleanMethod(list_obj, list_add, symbol_obj);
}
return list_obj;
}
3. 利用结构体 JNINativeMethod 保存Java native方法和 JNI函数的对应关系
static JNINativeMethod g_methods[] = {
{ "parse", "(Ljava/util/ArrayList;)Ljava/util/List;", (jobject)crash_parse}
};
4. 利用registerNatives(JNIEnv* env)
注册类的所有本地方法并在 JNI_OnLoad 方法中调用注册方法;
JNIEXPORT int JNICALL JNI_OnLoad(JavaVM *vm,void *reserved) {
JNIEnv *env;
if (vm->GetEnv((void **) &env,JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jclass javaClass = env->FindClass(JNI_REG_CLASS);
if (javaClass == NULL){
return JNI_ERR;
}
int method_count = sizeof(g_methods) / sizeof(g_methods[0]);
if (env->RegisterNatives(javaClass, g_methods, method_count) < 0) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
遇到的坑
SIMPLE: Error configuring
解决办法: 在工程目录下, 修改build.gradle
classpath 'com.android.tools.build:gradle:3.1.3' 版本号改为3.2.1
默认生成的是.cpp文件,c项目提供的是c语言的.h文件,为了让c++兼容c语言的全局函数, 需要在.h文件的方法加上
extern "C" {
unsigned char *app_encrypt(security_data data);
}
早期版本中的编译错误 undefined reference to `__android_log_print'
解决方法: target_link_libraries(
${log-lib}
z )
z表示linux下的{log-lib}库, 最近写该文档时发现没有这个z配置也能生成so库。