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智能,可能存在个人理解错误和偏见,请注意甄别。

  • [安卓JNI精细化讲解,让你彻底了解JNI](https://link.juejin.cn?target=https%3A%2F%2Fwww.cnblogs.com%2Fqixingchao%2Fp%2F11911787.html "https://www.cnblogs.com/qixingchao/p/11911787.html")\]([www.cnblogs.com/qixingchao/...](https://link.juejin.cn?target=https%3A%2F%2Fwww.cnblogs.com%2Fqixingchao%2Fp%2F11911787.html "https://www.cnblogs.com/qixingchao/p/11911787.html"))

  • Android JNI开发深度学习

相关推荐
BD_Marathon4 小时前
【MySQL】函数
android·数据库·mysql
西西学代码5 小时前
安卓开发---耳机的按键设置的UI实例
android·ui
maki0779 小时前
虚幻版Pico大空间VR入门教程 05 —— 原点坐标和项目优化技巧整理
android·游戏引擎·vr·虚幻·pico·htc vive·大空间
千里马学框架10 小时前
音频焦点学习之AudioFocusRequest.Builder类剖析
android·面试·智能手机·车载系统·音视频·安卓framework开发·audio
fundroid13 小时前
掌握 Compose 性能优化三步法
android·android jetpack
TeleostNaCl14 小时前
如何在 IDEA 中使用 Proguard 自动混淆 Gradle 编译的Java 项目
android·java·经验分享·kotlin·gradle·intellij-idea
旷野说15 小时前
Android Studio Narwhal 3 特性
android·ide·android studio
maki07721 小时前
VR大空间资料 01 —— 常用VR框架对比
android·ue5·游戏引擎·vr·虚幻·pico
xhBruce1 天前
InputReader与InputDispatcher关系 - android-15.0.0_r23
android·ims
领创工作室1 天前
安卓设备分区作用详解-测试机红米K40
android·java·linux