声明
本文主要整理记录Android开发调试过程中的JNI知识,内容来源于网络与自己的整理,如有侵权,请与我联系,谢谢。
文章大多参考了它:写的非常棒,看它:https://blog.youkuaiyun.com/tkwxty/article/details/103454842
了解JNI/NDK基本概念
- JNI(JAVA Native Interface),JAVA本地接口,主要用于JVM内部运行的JAVA与C、C++、汇编语言进行相互调用。
- NDK是Google提供的Native Development Kit,一套开发工具,用于快速开发C、C++库,通俗的理解就是,使用NDK将C、C++源码编译成库文件。
开发流程
- 搭建NDK的环境:新版本的Android Studio(2.2版本以上)已经集成了NDK,只需要在创建项目的时候,创建C++项目就可以,会自动配置NDK的环境,默认是使用CMake的方式,也可以选用Android.mk & Application.mk编译方式(需要手动更改配置)。
- 创建本地代码文件(C、C++)并随之创建更改CMake文件(或者Android.mk文件)。
- 编译C、C++源文件,生成.so,添加到工程目录(在/src/main下创建jniLibs文件夹,将.so所在目录(如x86_64)直接放入该目录);新版本的Android Studio,编译整个apk会编译so,并打包进apk,不需要额外的创建目录了。
- 在Android Java项目中使用JNI的功能。
搭建ndk编译环境遇到的问题
- 修改使用Android.mk编译,需要修改为build.gradle.kts,当然也可以添加更多的参数,比如cFlags,abiFilters,argument等
externalNativeBuild {
ndkBuild {
path("src/main/cpp/jni/Android.mk")
}
}
- 编译时的头文件包含和链接库问题。
LOCAL_C_INCLUDES:添加编译的头文件路径,添加该参数时候,NDK编译时会去该路径下搜索头文件。
LOCAL_EXPORT_C_INCLUDES:当其他模块使用该模块时,会自动包含该参数指定的头文件路径。
PREBUILT_SHARED_LIBRARY:用来指定一个预先编译好多动态库, 与BUILD_SHARED_LIBRARY and BUILD_STATIC_LIBRARY不同,此时模块的LOCAL_SRC_FILES应该被指定为一个预先编译好的动态库,而非source file,需要注意的是若该库没有被其他库链接使用,则打包的apk,不会包含该包。
include ($call all-subdir-makefiles):用于编译该目录的所有子目录下的Android.mk文件
$(call import-module,<name>):允许寻找并inport其它modules到本Android.mk中来。 它会从NDK_MODULE_PATH寻找指定的模块名。
LOCAL_LDLIBS:用于添加LOCAL_LDLIBS 主要用于系统库或静态库
- 开发调试问题:
1)使用include ($call all-subdir-makefiles)编译多个子目录中的Android.mk时,一个Android.mk所在的子目录始终编译不到,显示当前的文件没有加入工程,sync也没有什么用。主要需要排查两个方向,一个就是NDK的版本与gradle版本的对应关系,而就是需要把生成的build目录文件全删除,天杀的Android studio,排查了好长时间。
2)PREBUILT_SHARED_LIBRARY预先编译好的动态库,若该库没有被其他库链接使用,则打包的apk,不会包含该包,若确定不会被其他库引用使用,可直接放在jinLibs上。
JNI数据类型和描述符
- JNI数据类型就两种:
- 基本数据类型:可以直接在JNI中使用,不用进行转换。
- 引用数据类型:引用类型JNI不能直接使用,需要转换才可以,且jstring,数组也是引用数据类型。
- 描述符:
- 类描述符:在JNI的Native方法中,要使用Java中的对象(包括自定义的或者Android源码中已有的对象),即在C/C++中怎么找到Java中的类,这就要使用到JNI开发中的类描述符了。JNI提供的函数中有个FindClass()就是用来查找Java类的,其参数必须放入一个类描述符字符串,类描述符一般是类的完整名称(包名+类名)。
Java中String类 完整类名:java.lang.String 对应的类描述符:java/lang/String jclass intArrCls = env->FindClass(“java/lang/String”) jclass intArrCls = env->FindClass(“Ljava/lang/String;”)
- 域描述符:域描述符是JNI中对Java数据类型的一种表示方法(就是对Java类中的变量,在JNI世界的定义),即在JVM虚拟机中,存储数据类型的名称时,是使用指定的描述符来存储,而不是我们习惯的 int,float 等。基本数据类型域描述符、引用数据类型域描述符。
- 方法描述符:定义了方法的返回值和参数的表示形式,将参数类型的域描述符按声明顺序放入一对括号中(如果没有参数则不需要括号),括号后跟返回值类型的域描述符即形成方法描述符。
- 类描述符:在JNI的Native方法中,要使用Java中的对象(包括自定义的或者Android源码中已有的对象),即在C/C++中怎么找到Java中的类,这就要使用到JNI开发中的类描述符了。JNI提供的函数中有个FindClass()就是用来查找Java类的,其参数必须放入一个类描述符字符串,类描述符一般是类的完整名称(包名+类名)。
- 调试记录:
1)主要是静态注册时,函数方法描述符,特别是引用数据类型的时候,注意加“ ;”
JNIEnv与JavaVM
JavaVM
JavaVM就是Java虚拟机,一个Java虚拟机只有个JavaVM对象,即是一个进程仅有一个javaVM对象,这个JavaVM则可以在进程中的各个线程共享,这个特性非常的重要。
1、获取JavaVM虚拟机接口
在JNI的开发中,有三种获取JavaVM的方式,如下。
方式一
在加载动态链接库的时候,JVM调用JNI_OnLoad(JavaVM jvm,void reserved)。第一个参数会传入JavaVM指针,可以用JNI全局的静态变量保存下来,以便全局使用。
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv* env = NULL;
mJavaVM = vm;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
LOGE("Failed to get JNI ENV");
return JNI_ERR;
}
if (!registerNativeMethods(env, ClassPathName, QemuMethod, sizeof (QemuMethod) / sizeof(QemuMethod[0]))) {
LOGE("Failed to register native methods for %s", ClassPathName);
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
方式二
通过JNIEnv获取JavaVM,具体参考如下:
JNIEXPORT void JNICALL Java_com_xxx_android2native_JniManager_openJni
(JNIEnv * env, jobject object)
{
//线程不允许共用env环境变量,但是JavaVM指针是整个jvm共用的,所以可以通过下面的方法保存JavaVM指针,在线程中使用
env->GetJavaVM(&gJavaVM);
...
}
方式三
调用JNI_CreateJavaVM(JavaVM *p_vm,void ** p_env,void vm_args)可以获取到JavaVM指针。Android系统是利用第二种方式来创建art虚拟机的,一般来讲,这种方式不允许用户调用。
JNI_CreateJavaVM(JavaVM **p_vm,void ** p_env,void* vm_args)
p_vm: 保存创建的虚拟机指针
p_env: 保存获取的JNIEnv对象的指针
vm_args: 一个JavaVMInitArgs类型的指针,用来设置初始化参数
return:创建成功返回JNI_OK,失败返回其他
其中JavaVMInitArgs是存放虚拟机参数的结构体,定义如下
typedef struct JavaVMOption {
const char* optionString;
void* extraInfo;
} JavaVMOption;
typedef struct JavaVMInitArgs {
jint version;
jint nOption;
JavaVMOption* options;
jboolean ignoreUnrecognized;
}
2、JavaVM的定义
JavaVM声明在jni.h文件里面,我们在JNI开发中,必定要引入#include <jni.h>头文件。
C语言中JavaVM声明如下:
struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;//C语言定义
#endif
/*
* JNI invocation interface.
*/
struct JNIInvokeInterface {
void* reserved0;
void* reserved1;
void* reserved2;
jint (*DestroyJavaVM)(JavaVM*);
jint (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
jint (*DetachCurrentThread)(JavaVM*);
jint (*GetEnv)(JavaVM*, void**, jint);
jint (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};
C++中JavaVM声明如下:
struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
/*
* C++ version.
*/
struct _JavaVM {
const struct JNIInvokeInterface* functions;
#if defined(__cplusplus)
jint DestroyJavaVM()
{ return functions->DestroyJavaVM(this); }
jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThread(this, p_env, thr_args); }
jint DetachCurrentThread()
{ return functions->DetachCurrentThread(this); }
jint GetEnv(void** env, jint version)
{ return functions->GetEnv(this, env, version); }
jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};
JavaEnv
JNIEnv全称为Java Native Interface Environment,就是Java本地接口环境。JNIEnv是提供JNI Native函数的基础环境,线程相关,不同线程的JNIEnv相互独立。(JNIEnv只在当前线程有效,不能将JNIEnv从一个线程传递到另一个线程中,相同的Java线程中对本地方法多次调用,传递的JNIEnv是相同的;一个方法可以被不同的Java线程所调用,可以接收不同的JNIEnv)JNIEnv是一个接口指针,指向了本地方法的一个函数表,该函数表中的每一个成员指向了一个JNI函数,本地方法通过JNI函数来访问JVM中的数据结构,详情如下图:
1、JNIEnv的定义
C语言中JNIEnv声明如下:
struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;//C的定义
typedef const struct JNIInvokeInterface* JavaVM;
#endif
/*
* Table of interface function pointers.
*/
struct JNINativeInterface {
void* reserved0;
void* reserved1;
void* reserved2;
void* reserved3;
jint (*GetVersion)(JNIEnv *);
...
}
C++中定义如下:
struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;//C的定义
typedef const struct JNIInvokeInterface* JavaVM;
#endif
/*
* C++ object wrapper.
*
* This is usually overlaid on a C struct whose first element is a
* JNINativeInterface*. We rely somewhat on compiler behavior.
*/
struct _JNIEnv {
/* do not rename this; it does not seem to be entirely opaque */
const struct JNINativeInterface* functions;
#if defined(__cplusplus)
jint GetVersion()
{ return functions->GetVersion(this); }
...
}
调试问题
1)C、C++编译so的时候,调用的JNIEnv相关函数的方式不同
2)若使用C++编译so时,需要加上以下条件,否则使用dlopen打开连接库后,dlsym获取函数指针的时候,会找不到reference。
#ifdef __cplusplus
extern "C" {
#endif
...
...
#ifdef __plusplus
}
#endif
jobject与jclass
- jobject是实例引用,与java.lang.Object类或它的子类的实例对应,jclass是类引用,与java.lang.Class实例对应,它代表着类的类型。在所有的JNI方法中jobject和实例操作的结合,jclass和类操作的结合保持一致。
//下述函数都是类引用有关系,所以参数都是jclass
jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
jobject (*CallStaticObjectMethod)(JNIEnv*, jclass, jmethodID, ...);
jobject (*GetStaticObjectField)(JNIEnv*, jclass, jfieldID);
//下述函数都是和实例引用有关系,所以参数都是jobject
jobject (*GetObjectField)(JNIEnv*, jobject, jfieldID);
jobject (*CallObjectMethod)(JNIEnv*, jobject, jmethodID, ...);
object (*GetObjectField)(JNIEnv*, jobject, jfieldID);
当Java中定义的native方法为静态方法时,则第二个参数为jclass,jclass代表native方法所属类的class本身。
当Java中定义的native方法为非静态方法时,则第二个参数为jobject,jobject代表native方法所属类的实例对象。
JNI动态与静态注册
注册就是JNI中通过一定的机制建立Java中Native方法和本地代码中函数的一一对应关系,这种机制就是JNI的注册机制,通过这种机制就能找到Java中Native方法对应的JNI函数了。
-
静态注册
(1)对应的规则如下:Java + 包名 + 类名 + 方法名
(2)实现步骤:编写java代码和对应的Native方法;编译java代码,生成.class文件;用过javah指令,利用生成的.class文件生成JNI的.h文件;生成后的JNI头文件中包含了Java函数在JNI层的声明。 -
动态注册
(1)实现原理:直接通过 JNIEnv中提供的函数RegisterNatives方法手动完成 Java中Native 方法和JNI中相关函数的的绑定,这样虚拟机就可以通过这个函数映射表直接找到相应的方法了。
(2)实现步骤:利用结构体JNINativeMethod保存Java Native函数和JNI函数的对应关系;在一个JNINativeMethod数组中保存所有native函数和JNI函数的对应关系;在Java中通过System.loadLibrary加载完JNI动态库之后,调用JNI_OnLoad函数,开始动态注册;JNI_OnLoad中调用通过调用JNIEnv中的函数RegisterNatives函数进行函数注册;
JNI的多线程编程
- JNI的多线程问题大多是JNIEnv的局部性和jobject的局部性引起的
- 线程之间不能直接传递JNIEnv和jobject这类通过JNI函数传递下来的属性值,因为他们和线程有关系,且属于局部引用在函数调用结束后会被GC回收并且销毁。
- JavaVM是可以进行传递的,因为它属于JNI进程的,每个进程有且只有一个JavaVM所以可以被多线程共享,但是JNIEnv和jobject是属于线程私有的,不能共享。
- 所以在多线程中需要使用jobject和JNIEnv的解决办法就是保存JavaVM,并且创建全局jobject引用,然后使用AttachCurrentThread从JavaVM中获取JNIEnv。
- 使用线程持有数据:pthread_key_create、pthread_setspecific、pthread_getspecific,来保证线程数据安全。