一、概念
- NDK是Android本地开发包,用于快速开发C/C++动态库。也可以通过其他方式开发so库,NDK提供便捷的开发和编译环境。类似JDK用于开发JAVA程序一样。
- JNI为应用层提供了调用底层代码功能的能力。底层一般出于性能考虑,通过C/C++直接编写调用硬件能力;或者为了安全,编译成so文件后,进行反编译要比编译jar困难多。
- 同时也提供给底层调用应用层能力。
二、实践步骤
需要注意的是,随着Android的发展和Android Studio版本的迭代。开发语言有Kotlin和Java,构建语言(gradle)有Kotlin DSL和Groovy DSL。但在使用上大差不差,不影响学习和实践。
- 通过Java或者Kotlin编写native方法。
- 通过工具或者手写对应的C/C++文件代码。
- 通过Android Studio配置gradle直接编译运行;或者命令行生成so库。
- 加载so库,使用so库功能。
三、新Nativie项目实践
通过Android Studio新建一个natvie项目来练习JNI,选择如图右下角的Native C++新建项目,按引导设置后等待Android Studio同步完毕,得到一个用来学习的最简单的JNI工程。
得到如下图的项目结构,我们打开MainActivity类。
1. Java层
打开MainActivity类,代码中定义了一个本地方法,由于开发语言是Kotlin,所以关键字是external
。
kotlin
external fun stringFromJNI(): String
如果是Java,那么关键字是native。
java
public native String stringFromJNI();
同时,我们发现代码中通过伴生对象初始化了下面代码,testjni是我们创建的native project的名称,实际上是加载libtestjni.so库,根据规则,只需要写testjni即可.
可以通过修改CMakeLists文件来修改名称,例如通过修改project("abc")
,改为abc,那么对应生成libadc.so,那么下面代码testjni就需要改为abc。
kotlin
//Kotlin伴生对象初始化
companion object {
init {
System.loadLibrary("testjni")
}
}
java
//Java静态初始化
static {
System.loadLibrary("testjnijava");
}
2. native层
Java层定义的本地方法stringFromJNI,在项目中由native-lib.cpp来实现.
c
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_testjni_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
-
导入jni和string文件
-
extern "C"
使用C的方式来实现native方法,也可以选择
extern "C++"
用C++方式来实现. -
JNIEXPORT
指定该函数是JNI函数,可以被外部调用,属于宏定义.
-
jstring JNICALL
表示函数调用规范,宏定义
-
Java_com_example_testjni_MainActivity_stringFromJNI
在Java定义的方法stringFromJNI在cpp变成了Java_com_example_testjni_MainActivity_stringFromJNI,这和JNI接口规范命名规则有关.
cJava_<PackageName>_<ClassName>_<MethodName>
包名中的逗号.换成下划线_
-
JNIEnv
代表了Java环境,通过JNIEnv* 可以对Java端的代码进行调用。例如调用对象方法,设置对象属性等。
-
jobject
代表了定义native函数的Java类或Java类实例。
3. CMakeLists文件
由于采用CMake管理和构建C/C++代码,所以一些配置需要通过CMakeLists来修改,JNI工程中CMakeLists文件默认内容:
cmake
#cmake最小版本
cmake_minimum_required(VERSION 3.22.1)
#工程名称,会影响生成so文件名称,例如这里最终生成是lib+testjni+.so,即libtestjni.so文件
project("testjni")
#添加到动态库 SHARED表示动态库.os文件,STATIC表示静态库.a文件
add_library(${CMAKE_PROJECT_NAME} SHARED
native-lib.cpp)
#链接目标库
target_link_libraries(${CMAKE_PROJECT_NAME}
android
log)
app的gradle默认配置如下,这样app在编译的时候就会对CMakeLists进行解析编译和链接成对应的动态库。
kotlin
//Kotlin DSL版本
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}
groovy
//Groovy DSL版本
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.22.1'
}
}
-
如果cpp目录下有多个源文件和头文件,不止一个native-lib.cpp,如何将它们编入到so文件中。
-
收集所有头文件到
HEADERS
变量中cmakefile(GLOB HEADERS #路径可能需要根据自己的实际修改 ${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/include/*.h )
-
收集所有源文件到
SOURCES
变量中cmakefile(GLOB SOURCES #路径可能需要根据自己的实际修改 ${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/*.cpp )
-
添加到动态库中
cmakeadd_library(${CMAKE_PROJECT_NAME} SHARED ${SOURCES} ${HEADERS})
-
4. so文件路径
按照上面默认配置下,生成的so文件一般在:
bash
app/build/intermediates/cxx/Debug/2f673b4w/obj/arm64-v8a
根据Android Studio的版本,生成的路径也不一样,但大差不差。例如有的在:
bash
app/build/intermediates/cmake/debug/obj/arm64-v8a
arm64-v8a
是当前设备支持的ABI。可能是armeabi-v7a、armeabi。
我们也可以通过设置CMAKE_LIBRARY_OUTPUT_DIRECTORY
来修改so文件的输出路径:
cmake
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/${ANDROID_ABI})
这个设置要放在add_library
之前。CMAKE_CURRENT_SOURCE_DIR
表示现在的源码路径,ANDROID_ABI
表示设备的支持的CPU架构指令集。例如我的CMAKE_CURRENT_SOURCE_DIR
是app/src/main/cpp
,ANDROID_ABI
是arm64-v8a
,那么就会在app/src/main/cpp/jniLibs/arm64-v8a
生成我们的so文件。
如果不清楚,可以通过message来打印查看具体路径:
cmake
message("${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/${ANDROID_ABI}")
在build中查看输出结果:
此时CMakeLists文件的内容:
cmake
#cmake最小版本
cmake_minimum_required(VERSION 3.22.1)
#工程名称,会影响生成so文件名称,例如这里最终生成是lib+testjni+.so,即libtestjni.so文件
project("testjni")
#设置so文件输出地方
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/${ANDROID_ABI})
#打印so文件路径
message("${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/${ANDROID_ABI}")
#添加到动态库 SHARED表示动态库.os文件,STATIC表示静态库.a文件
add_library(${CMAKE_PROJECT_NAME} SHARED
native-lib.cpp)
#链接目标库
target_link_libraries(${CMAKE_PROJECT_NAME}
android
log)
四、注册
在Java代码定义的stringFromJNI
函数如何cpp中的Java_com_example_testjni_MainActivity_stringFromJNI
方法产生关联,也就是Java代码用什么样的方法和本地方法进行关联呢?JNI采用静态注册和动态注册两种方式。上面的代码示例就是采用静态注册方式。
1. 静态注册
在Java代码定义的函数,本地方法需要按照接口规范书写对应的方法名:
通过JNI接口规范书写函数函数名。本地方法对应Java代码格式:
xml
Java_<PackageName>_<ClassName>_<MethodName>
包名在Java是以逗号进行分割,如:com.sample.jni
。那么在本地方法就要写成:com_sample_jni
。
由于静态注册在编译阶段就确定了Java代码与本地代码的映射关系,不需要在运行时进行注册,所以比较高效。
2. 动态注册
动态注册相对来说比较灵活,在运行时可以根据需要动态添加或删除本地方法。在本地代码,通过在JNI_OnLoad
方法中调用RegisterNatives
方法注册Java与Nativie代码函数的映射关系。
Sample:
此时我们应用的包名是:com.sample.testjnijava
。
在MainActivity
定义本地方法,例如这里定义了dynamicFromJNI
。这里Android Studio会报红,提示我们通过静态注册方式自动生成对应的C/C++函数,这里不用管它,晾它一会就好了。
java
public native String dynamicFromJNI();
接着我们在native-lib.cpp
实现dynamicFromJNI
的功能。这里我们只是返回了一个字符串而已。
C++
jstring dynamicFromJNI(JNIEnv *env, jobject thiz) {
std::string hello="dynamic from jni,this is a test";
return env->NewStringUTF(hello.c_str());
}
接着dynamicFromJNI
后面写JNI_OnLoad
函数动态注册的内容。
c
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv *env;
//获取版本是否正常
if((vm)->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK){
return JNI_ERR;
}
//查找我们Java类,对应的包名逗号改成/
jclass cls=env->FindClass("com/sample/testjnijava/MainActivity");
if(cls==NULL){
return JNI_ERR;
}
//Java层函数与本地函数对应关系
JNINativeMethod methods[]={
{"dynamicFromJNI", "()Ljava/lang/String;", (void *)dynamicFromJNI}
};
//动态注册本地函数
if(env->RegisterNatives(cls,methods,sizeof(methods)/sizeof(methods[0]))<0){
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
代码跑起来,一切正常~从这里可以看出,动态注册不用像静态注册那样写本地方法的格式,但Android Studio自动生成也很省事。另外动态注册还要知道方法签名。
几点注意事项:
JNI_OnLoad
函数代码可能会有细小差异,但逻辑是不变的。- 类查找过程,注意包名的变化。
- 本地方法实现需要写在
JNI_OnLoad
函数前。 - JNI类型描述符。见下个小结。
五、类型描述符
JNI类型描述符是用于在JNI中描述Java类型的字符串。用于指示JVM如何将Java类型映射到本地代码C/C++类型。
1. 映射关系
JNI中定义的别名 | Java类型 | C/C++类型 |
---|---|---|
jint / jsize | int | int |
jshort | short | short |
jlong | long | long / long long (__int64) |
jbyte | byte | signed char |
jboolean | boolean | unsigned char |
jchar | char | unsigned short |
jfloat | float | float |
jdouble | double | double |
jobject | Object | _jobject* |
2. 描述符
Java类型 | 字段描述符(签名) | 备注 |
---|---|---|
int | I | int的首字母、大写 |
float | F | float的首字母、大写 |
double | D | double的首字母、大写 |
short | S | short的首字母、大写 |
long | L | long的首字母、大写 |
char | C | char的首字母、大写 |
byte | B | byte的首字母、大写 |
boolean | Z | 因B已被byte使用,所以JNI规定使用Z |
object | L + /分隔完整类名; | String 如: Ljava/lang/String; |
array | [ + 类型描述符 | int[] 如:[I |
Java函数 | 函数描述符(签名) | 备注 |
---|---|---|
void | V | 无返回值类型 |
Method | (参数字段描述符...)返回值字段描述符 | int add(int a,int b) 如:(II)I |
Smaple:
Java本地方法:
java
public native void dynamicFromJNI();
对应JNI描述符:
scss
()V
Java本地方法:
java
public native Student getStudent(String class,int studentId);
对应JNI描述符:
bash
(Ljava/lang/String;I)Lcom/sample/testjnijava/Student;
六、资源
本文大多数内容参考以下内容和AI智能,可能存在个人理解错误和偏见,请注意甄别。