Android JNI入门

一、概念

  • NDK是Android本地开发包,用于快速开发C/C++动态库。也可以通过其他方式开发so库,NDK提供便捷的开发和编译环境。类似JDK用于开发JAVA程序一样。
  • JNI为应用层提供了调用底层代码功能的能力。底层一般出于性能考虑,通过C/C++直接编写调用硬件能力;或者为了安全,编译成so文件后,进行反编译要比编译jar困难多。
  • 同时也提供给底层调用应用层能力。

二、实践步骤

需要注意的是,随着Android的发展和Android Studio版本的迭代。开发语言有Kotlin和Java,构建语言(gradle)有Kotlin DSL和Groovy DSL。但在使用上大差不差,不影响学习和实践。

  1. 通过Java或者Kotlin编写native方法。
  2. 通过工具或者手写对应的C/C++文件代码。
  3. 通过Android Studio配置gradle直接编译运行;或者命令行生成so库。
  4. 加载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接口规范命名规则有关.

    c 复制代码
    Java_<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变量中

      cmake 复制代码
      file(GLOB HEADERS
      		#路径可能需要根据自己的实际修改
          ${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/include/*.h
      )
    • 收集所有源文件到 SOURCES变量中

      cmake 复制代码
      file(GLOB SOURCES
      		#路径可能需要根据自己的实际修改
          ${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/*.cpp
      )
    • 添加到动态库中

      cmake 复制代码
      add_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_DIRapp/src/main/cpp,ANDROID_ABIarm64-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智能,可能存在个人理解错误和偏见,请注意甄别。

相关推荐
Eastsea.Chen2 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年9 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿11 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神13 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛13 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法13 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter14 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快16 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android
暮志未晚Webgl16 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5
麦田里的守望者江16 小时前
KMP 中的 expect 和 actual 声明
android·ios·kotlin