1.2 实现步骤Java Nativie interface
Java 本地接口,JNI是Java调用本地语言的一种特性。通过Jni 可以使java与本地语言之间相互调用
如java 与c/c++ 相互调用
在java中声明Native方法
public native String stringFromJNI();
javac 命令编译1中的java源文件得到class文件
javah -jni命令导出JNI的头(.h)文件
使用java 需要交互的本地代码,实现在java中声明的native方法
extern "C"JNIEXPORT jstring JNICALLJava_com_example_ndktest_NdkManager_stringFromJNI(JNIEnv *env, jobject thiz) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str());}
将本地代码编译成动态库
windows下是.dll,linux下是.so ,Mac系统是.jnilib通过java 命令执行java程序,实现java调用本地代码
1.3 native层通过jni 可以干什么创建,更新java对象(包括数组字符串)调用java方法加载类并获取类信息 1.3.1 对类的操作
GetObjectClass获取类class文件 一般获取参数中传递的自定义类型的class文件
FindClass(类的全路径) 获取class 文件 一般获取没在参数列表中 但需要使用的类的class 文件 参数是 目标类的路径
GetMethodID获取方法名
GetFieldID获取字段
具体请看如下示例
extern "C"JNIEXPORT jobject JNICALLJava_com_example_ndktest_NdkManager_changName(JNIEnv *env, jobject thiz, jobject st, jstring name) { //反射java方法 //1 获取java 对应的class 文件 jclass stClass = env->GetObjectClass(st); // 2 找到要调用的方法 getName jmethodID setAge = env->GetMethodID(stClass, "setAge", "(I)V"); jmethodID printInfo = env->GetStaticMethodID(stClass, "printInfo", "(Ljava/lang/String;)V"); jmethodID getName = env->GetMethodID(stClass, "getName", "()Ljava/lang/String;"); //3 调用 getName jstring sName = static_cast
参数为数组
引用类型数组都为jobjectArray
java
// 引用数据类型为参数 public native int Test(int[] list, String[] str);
c++
extern "C"JNIEXPORT jint JNICALLJava_com_example_ndktest_NdkManager_Test(JNIEnv *env, jobject thiz, jintArray i_Array, jobjectArray str) { // 获取数组首元素地址 // 第二个参数 // jboolean* isCopy // true 表示新申请内存 拷贝一个新数组 // false 就是使用java数组 jint *p = env->GetIntArrayElements(i_Array, NULL); // 如果是c环境 就需要如下使用 env-> 替换成(*env)-> //jint *p1 =(*env).GetIntArrayElements(i_Array, NULL); // 因为 c环境下 JNIEnv 本身就是个指针,这里再次声明为指针就成了二级指针,所以要解一次引用拿到一级指针来操作 //获取数组的长度 jint length = env->GetArrayLength(i_Array); for (int i = 0; i < length; i++) { if (i == 3) { *(p + i) = 10; } LOGE("获取的java 数组值为: %d", *(p + i)); } // 释放数组 // 参数1 // 参数2 // 参数3: mode 模式 有三个值 // 0 刷新java数组 并释放c/c++ 数组 // 1 = JNI_COMMIT:只刷新JAVA数组 // 2 = JNI_ABORT : 只释放c/c++ 数组 env->ReleaseIntArrayElements(i_Array, p, 0);//类型数组通过如下方式遍历 首先获取数组长度 遍历 按下下标获取 int count = env->GetArrayLength(str); for (int i = 0; i < count; i++) { // 获取jstring jstring s1 = static_cast
参数为基本数据类型
则可直接使用
java
// 基本数据类型为参数 public native int Test1(int i);
c++
extern "C"JNIEXPORT jint JNICALLJava_com_example_ndktest_NdkManager_Test1(JNIEnv *env, jobject thiz, jint i) { return i + 1;}
参数为自定义类型
则需通过操作类的方法操作请参考上面1.3.1中的例子 1.4 JNI 语法
java中声明 native方法
public native String stringFromJNI();
Native中实现被声明的native方法
extern "C"JNIEXPORT jstring JNICALLJava_com_example_ndktest_NdkManager_stringFromJNI(JNIEnv *env, jobject thiz) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str());}
1.4.1 Native中代码解释 1.4.1.1 extern “C”Extern “C”是由C++提供的一个连接交换指定符号,用于告诉C++这段代码是C函数。这是因为C++编译后库中函数名会变得很长,与C生成的不一致,造成C++不能直接调用C函数,加上extren “c”后,C++就能直接调用C函数了
1.4.1.2 JNIEXPORT宏定义:#define JNIEXPORT __attribute__ ((visibility ("default"))) 在 Linux/Unix/Mac os/Android 这种类 Unix 系统中,定义为__attribute__ ((visibility ("default")))
表示函数对外界可见
GCC 有个visibility属性, 该属性是说, 启用这个属性:
当-fvisibility=hidden时,动态库中的函数默认是被隐藏的即 hidden。当-fvisibility=default时,动态库中的函数默认是可见的。 1.4.1.3 JNICALL
宏定义,在 Linux/Unix/Mac os/Android 这种类 Unix 系统中,它是个空的宏定义: #define JNICALL,所以在 android 上删除它也可以
1.4.1.4 thizthiz 就是java中声明native方法的类 即调用native方法的类 1.4.1.5JNIEnv
JNIEnv类型实际上代表了Java环境,通过这个 JNIEnv* 指针,就可以对 Java 端的代码进行操作:
调用 Java 函数操作 Java 对象 JNIEnv 的本质是一个与线程相关的结构体,每个线程存在一个JNIEnv 1.4.1.6 JavaVM
JavaVM : JavaVM 是 Java虚拟机在 JNI 层的代表, JNI 全局只有一个JNIEnv : JavaVM 在线程中的代表, 每个线程都有一个, JNI 中可能有很多个 JNIEnv,同时 JNIEnv 具有线程相关性,也就是 B 线程无法使用 A 线程的 JNIEnv 1.4.1.7 如何在native线程中使用JNIenv
如果想在 native 线程中使用 JNIEnv* 需要使用 JVM 的 AttachCurrentThread 方法进行绑定,在子线程退出时,要调用JavaVM的DetachCurrentThread函数来释放对应的资源,否则会出错。
JavaVM *_vm;jint JNI_OnLoad(JavaVM* vm, void* reserved){ _vm = vm; return JNI_VERSION_1_6; }void* threadTask(void* args){ JNIEnv *env; // 通过 javavm 绑定当前线程赋值给env jint result = _vm->AttachCurrentThread(&env,0); if (result != JNI_OK){ return 0; } // ... // 线程 task 执行完后不要忘记分离 _vm->DetachCurrentThread();}extern "C"JNIEXPORT void JNICALLJava_com_example_ndktest_NdkManager_nativeThreadTest(JNIEnv *env, jobject thiz) { pthread_t pid; pthread_create(&pid,0,threadTask,0);}
这里通过在JNI_OLoad中 保存的全局变量来实现,这个方式涉及到jni的注册方式 后面会详细说
1.5 JNI 注册类型 1.5.1 静态注册
当Java层调用navtie函数时,会在JNI库中根据函数名查找对应的JNI函数。如果没找到,会报错。如果找到了,则会在native函数与JNI函数之间建立关联关系,其实就是保存JNI函数的函数指针。下次再调用native函数,就可以直接使用这个函数指针。
语法
JNI函数名格式(包名里面的”.”需要改为”_”)Java_ + 包名(com_example_ndktest)+ 类名(_MainActivity) + 函数名(_stringFromJNI)
静态注册的缺点
要求JNI函数的名字必须遵循JNI规范的命名格式;名字冗长,容易出错;初次调用会根据函数名去搜索JNI中对应的函数,会影响执行效率;需要编译所有声明了native函数的Java类,每个所生成的class文件都要用javah工具生成一个头文件;
例子
包名 com.example.ndktest
类名NdkManager
java类
package com.example.ndktest;import java.util.ArrayList;import java.util.List;public class NdkManager { static { // native实现在ndktest 库内所以需加载该库 后面cmake中会说到 System.loadLibrary("ndktest"); } public native String stringFromJNI();}
native实现
#include
通过提供一个函数映射表,注册给JVM虚拟机,这样JVM就可以用函数映射表来调用相应的函数,就不必通过函数名来查找需要调用的函数。
Java与JNI通过JNINativeMethod的结构来建立函数映射表,它在jni.h头文件中定义,其结构内容如下:
typedef struct { const char* name; // 对应java中方法名 const char* signature; // 方法签名 void* fnPtr;// /对应交互cpp中方法的 函数指针 (指向对应函数)} JNINativeMethod;
创建映射表后,调用env->RegisterNatives函数将映射表注册给JVM;
当Java层通过System.loadLibrary加载JNI库时,会在库中查JNI_OnLoad函数。JNI_OnLoad为JNI库的入口函数,需要在这里完成所有函数映射和动态注册工作,及其他一些初始化工作。
实例
Java 类名 NativeManager包名com.example.dynamicndk
public class NativeManager { // Used to load the 'dynamicndk' library on application startup. static { System.loadLibrary("dynamicndk"); } // 动态注册 public native String stringFromJNI(); public native void Test(); // native 线程 调用java public native void threadTest();}
cpp
#include
步骤总结
创建函数映射表获取函数对应java类获取JNIEnv,通过JNIEnv注册 函数映射表
优势
动态注册 命名更灵活 1.6 数据类型转换 1.6.1 基本数据类型 java类型 nativie类型 描述 booleanjbooleanunsigned 8 bits 整型bytejbytesigned 8 bits 整型charjcharunsigned 16 bits 整型shortjshortsigned 16 bits 整型intjintsigned 32 bits 整型longjlongsigned 64 bits 整型floatjfloatsigned 32 bits 浮点型doublejdoublesigned 64 bits 浮点型voidvoid无整形1.6.2 引用数据类型 java native objectjobjectjava.lang.Class instancejclassjava.lang.String instancejstringarrayjarrayObject[]jobjectArrayboolean[]jbooleanArraybyte[]jbyteArraychar[]jcharArrayshort[]jshortArrayint[]jintArraylong[]jlongArrayfloat[]jfloatArraydouble[]jdoubleArrayjava.lang.Throwable objectsjthrowable1.6.3 函数签名
格式
[参数1类型字符参数2类型字符...]返回值类型字符
注意引用类型需以L开头后跟类型全路径并以;结尾
例 Ljava/lang/String;
参数
若为多参数也无需间隔直接后面添加类型字符即可
例
若没参数则括号内不写内容若无返回值则返回值位置用V
例 ()V
注意引用类型需以L开头后跟类型全路径并以;结尾
例 Ljava/lang/String;
对照表
Java类型 对应字符 voidVbooleanZintIlongJdoubleDfloatFbyteBcharCshortSint[][I (数组以[开始后跟对应类型字符)StringLjava/lang/String; (引用类型 以L开头后跟类路径以;结尾)Object[][Ljava/lang/object; 1.7 JNI引用 局部引用(Local Reference)
在函数内部创建,声明的变量和对象都属于局部引用
方法调用结束,局部引用自动释放
当然也可以手动释放
DeleteLocalRef() // 创建的对象用DeleteReleaseXXX
全局引用(Global Reference)
JNI 允许从局部变量创建全局变量
//声明全局引用jclass st1Class;extern "C"JNIEXPORT jobject JNICALLJava_com_example_ndktest_NdkManager_changName(JNIEnv *env, jobject thiz, jobject st, jstring name) { //查找类 if(st1Class==NULL){ jclass cls = env->FindClass("com/example/ndktest/Student"); //声明为全局引用 st1Class= static_cast
可以跨方法跨线程
释放需调用DeleteGlobalRef
弱全局引用(Weak Global Reference)
与全局引用类似,弱引用可以跨方法、线程使用。与全局引用不同的是,弱引用不会阻止GC回收它所指向的VM内部的对象
所以在使用弱引用时,必须先检查缓存过的弱引用是指向活动的对象,还是指向一个已经被GC的对象
创建
//声明全局引用jclass st1Class;extern "C"JNIEXPORT jobject JNICALLJava_com_example_ndktest_NdkManager_changName(JNIEnv *env, jobject thiz, jobject st, jstring name) { //判断是否指向活动的对象 jboolean isEqual =env->IsSameObject(st1Class,NULL); if(st1Class==NULL||isEqual){ jclass cls = env->FindClass("com/example/ndktest/Student"); //声明为全局引用 st1Class= static_cast
释放
调用DeleteWeakGlobalRef来释放 NDK 简介
重要结构目录Android NDK 是一组允许您将 C 或 C++(“原生代码”)嵌入到 Android 应用中的工具,NDK描述的是工具集
是通过jni调用的c/c++的原生代码的
我们可以在 sdk/ndk-bundle 中查看 ndk 的目录结构,下面列举出三个重要的成员:
ndk-build: 该 Shell 脚本是 Android NDK 构建系统的起始点,一般在项目中仅仅执行这一个命令就可以编译出对应的动态链接库了。platforms: 该目录包含支持不同 Android 目标版本的头文件和库文件, NDK 构建系统会根据具体的配置来引用指定平台下的头文件和库文件。toolchains: 该目录包含目前 NDK 所支持的不同平台下的交叉编译器 - ARM 、X86、MIPS ,目前比较常用的是 ARM。 // todo ndk-depends.cmd 交叉编译
在一个平台上编译出另一个平台上可以执行的二级制文件的过程叫做交叉编译
比如在windows上编译出android可用的库文件 库文件格式
静态库 `.a
编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了,linux中后缀名为”.a” 动态库.so
在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库。linux 中后缀名为”.so”,gcc在编译时默认使用动态库。 makefile (.mk) 编译 简介
1.1 Android.mkmakefile 就是“自动化编译”:一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,如何进行链接等等操作。 Android 使用 Android.mk 文件来配置 makefile
# 源文件在的位置。宏函数 my-dir 返回当前目录(包含 Android.mk 文件本身的目录)的路径。LOCAL_PATH := $(call my-dir)# 引入其他makefile文件。CLEAR_VARS 变量指向特殊 GNU Makefile,可为您清除许多 LOCAL_XXX 变量# 不会清理 LOCAL_PATH 变量include $(CLEAR_VARS)# 指定库名称,如果模块名称的开头已是 lib,则构建系统不会附加额外的前缀 lib;而是按原样采用模块名称,并添加 .so 扩展名。LOCAL_MODULE := hello# 包含要构建到模块中的 C 和/或 C++ 源文件列表 以空格分开LOCAL_SRC_FILES := hello.c# 构建动态库include $(BUILD_SHARED_LIBRARY)
1.2 对应Gradle的设置
app/gradle
apply plugin: 'com.android.application'android { compileSdkVersion 29 defaultConfig { ... // 应该将源文件编译成几个 CPU so externalNativeBuild{ ndkBuild{ abiFilters 'x86','armeabi-v7a' } } // 需要打包进 apk 几种 so ndk { abiFilters 'x86','armeabi-v7a' } } // 配置 native 构建脚本位置 externalNativeBuild{ ndkBuild{ path "src/main/jni/Android.mk" } } // 指定 ndk 版本 ndkVersion "20.0.5594570" ...}dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) ...}
Google 推荐开发者使用 cmake 来代替 makefile 进行交叉编译了,makefile 在引入第三方预编译好的 so 的时候会在 android 6.0 版本前后有些差异,比如在 6.0 之前需要手动 System.loadLibrary 第三方 so,在之后则不需要。 关于 makefile 还有很多配置参数,这里不在讲解,更多参考官方文档。
在 6.0 以下,System.loadLibrary 不会自动加载 so 内部依赖的 so 在 6.0 以下,System.loadLibrary 会自动加载 so 内部依赖的 so 所以使用 mk 的话需要做版本兼容 Cmake 编译
是一个构建工具 1.1 CMakeLists.txt
# cmake版本cmake_minimum_required(VERSION 3.10.2)#声明并命名项目project("ndktest")# 声明并命名库# 将其设置 是否共享 此处有三个值 #SHARED: 表示动态库,可以在(Java)代码中使用 System.loadLibrary(name) 动态调用;# STATIC: 表示静态库,集成到代码中会在编译时调用;#MODULE: 只有在使用 dyId 的系统有效,如果不支持 dyId,则被当作 SHARED 对待;#EXCLUDE_FROM_ALL: 表示这个库不被默认构建,除非其他组件依赖或手工构建;# 添加库中源文件的路径add_library( # 设置库的名字 比如现在会生成 ndktest.so ndktest # 设置库为共享库 SHARED # 提供源文件相对路径 native-lib.cpp)# 搜索并指定预构建库并将路径存储为变量(这里是log-lib)。# NDK中已经有一部分预构建库(比如 log),并且ndk库已经是被配置为cmake搜索路径的一部分# 可以不写 直接在 target_link_libraries 写上log# 比如上文中我们引入的 android/log.h 就是从调用的ndk 目录下的 liblog.so 前面的lib可省略直接写log 即可找到find_library( # 设置路径变量名称 log-lib # 从系统路径中查找指定名字的so库路径 赋值给上面的log-lib log)# 指定CMake应链接到目标库的库。可以链接多个库,例如在此生成脚本中定义的库、预构建的第三方库或系统库。target_link_libraries( # 指定目标库 ndktest # 链接目标库的路径 这里是log的路径 即上面 find_library 中 的log-lib # 这种是以变量形式链接目标库路径 当然我们也可以省区find_library这一步直接指定目标库 # 就比如这里的ndktest 直接写log 也可以找到 # 如果需要引入三方so 就得指定目录来查找了 ${log-lib})
1.2 Gradle 中的配置
android { //...... defaultConfig { applicationId "com.example.ndktest" minSdk 21 targetSdk 31 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { cmake { // 设置c++ 标准 cppFlags '-std=c++11' //需要生成几种cpu架构下的so,不写默认都生成 abiFilters "armeabi-v7a","x86" } } // 打包支持几种架构的apk包 比如这里就会生成支持x86 和armeabi-v7a两种的架构的apk ndk { abiFilters 'x86','armeabi-v7a' } } // 分架构打包出不同架构的apk splits { abi { enable true reset() include 'armeabi-v7a', 'x86' universalApk true } } //...... // 配置native构建脚本的文件路径 externalNativeBuild { cmake { path file('src/main/cpp/CMakeLists.txt') version '3.10.2' } } //.....}
externalNativeBuild 在defaultConfig 中为指导源文件编译 在defaultConfig 外为配置native的构建脚本路径
添加多个源文件可以在cmake文件内的addlibrary后继续添加 如下
add_library( # 设置库的名字 比如现在会生成 ndktest.so ndktest # 设置库为共享库 SHARED # 提供源文件相对路径 native-lib.cpp test.cpp)
也可以设置一个变量存储 源文件相对路径 然后添加到add_library中
set(LIBSRC native-lib.cpp test.cpp)add_library( # 设置库的名字 比如现在会生成 ndktest.so ndktest # 设置库为共享库 SHARED # 提供源文件相对路径 ${LIBSRC})
也可以设置路径 模糊匹配
file(GLOB native_srcs "src/main/cpp/*.cpp")add_library( # 设置库的名字 比如现在会生成 ndktest.so ndktest # 设置库为共享库 SHARED # 提供源文件相对路径 ${native_srcs})
引用三方动态库.so
假如引用一个c 编写的so -> libtest.so 内只含有一个hc 文件
这里将CMakeList.txt从cpp目录移动到app目录下 方便后面拼接路径
将对应架构的so 放到 jinLibs对应架构目录下 这里以 armeabi-v7a 为例
方式一增加 CMake 查找路径 直接给 cmake 在添加一个查找路径,在这个路径下可以找到 external
在CMakeList.txt中添加如下代码 代码级别与 target_link_libraries、find_library同级别
#设置C++ 编译 -L 设置库路径 -L设置的路径与下面的link_directories 应该类似set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/${CMAKE_ANDROID_ARCH_ABI}")#指定静态库或动态库的搜索路径 该指令的作用主要是指定要链接的库文件的路径#自己写的动态库文件放在自己新建的目录下时,可以用该指令指定该目录的路径以便工程能够找到。link_directories( ${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI})
再CMakeList.txt中设置一个变量
CMAKE_CXX_FLAGS 使用c++编译为c++环境下的变量名CMAKE_C_FLAGS 使用c编译 为C环境下的变量名
该变量会传递给编译器
重新定义该变量 通过-L 指定 目标路径 可以通过如下变量 动态识别目录 从而查找指定的so
CMAKE_SOURCE_DIR 当前文件路径ANDROID_ABI是abi 目录
link_directories
指定静态库或动态库的搜索路径 该指令的作用主要是指定要链接的库文件的路径自己写的动态库文件放在自己新建的目录下时,可以用该指令指定该目录的路径以便工程能够找到。
将test写到target_link_libraries 表示我们要链接libtest.so
so 以lib开头时 写到target_link_libraries 或find_library时 lib 可以省略只写名字即可
target_link_libraries( # Specifies the target library. ndktesttest # links the target library to the log library # included in the NDK. ${log-lib})
方式二 方式二
在CMakeList.txt中添加如下代码 代码级别与 target_link_libraries、find_library同级别
且需要在systemload中 load 该so
这种方式在android 6.0 以下还能用 6.0 以上就会出现目录不对的问题 所以cmake中 我们一般采用第一种方式
# test 代表第三方 so - libtest.so# SHARED 代表动态库,静态库是 STATIC;# importED: 表示是以导入的形式添加进来(预编译库)add_library(test SHARED importED)#设置 test 的 导入路径(importED_LOCATION) 属性,不可以使用相对路径# CMAKE_SOURCE_DIR: 当前cmakelists.txt的路径 (cmake工具内置的)# android cmake 内置的 ANDROID_ABI : 当前需要编译的cpu架构set_target_properties(external PROPERTIES importED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libtest.so)
将test写到target_link_libraries 表示我们要链接libtest.so
target_link_libraries( # Specifies the target library. ndktesttest # links the target library to the log library # included in the NDK. ${log-lib})
引入so后使用情况一 存在test.h若libtest.so 内存在 test() 函数
直接#include
若test函数是用c 编写的
我们的环境又是c++环境 则需要提前标记extern "C" 才能正常调用
extern "C" { extern void test();}
不是c++ 环境 则直接调用test函数即可
情况二 不存在 test.h
则要将函数声明为 extern 即 外部定义在这里引用
extern void test();
若test函数是用c 编写的
我们的环境又是c++环境 则需要提前标记extern "C" 才能正常调用
extern "C" { extern void test();}
不是c++ 环境 则直接调用test函数即可
引入静态库 .a
不用必须放在jnilibs 目录下 只需要像引用so一样设置好库的查找路径即可
# test1 代表第三方.a文件 全名是libtest1.a# SHARED 代表动态库,静态库是 STATIC;# importED: 表示是以导入的形式添加进来(预编译库)add_library(test1 STATIC importED)#设置 test 的 导入路径(importED_LOCATION) 属性,不可以使用相对路径# CMAKE_SOURCE_DIR: 当前cmakelists.txt的路径 (cmake工具内置的)# android cmake 内置的 ANDROID_ABI : 当前需要编译的cpu架构 AS3.2后, ${ANDROID_ABI} 改成 ${CMAKE_ANDROID_ARCH_ABI}set_target_properties(external PROPERTIES importED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libtest1.a)
使用方式 和上面so 使用方式相同