【Android】【JNI用法与调试记录】

声明

本文主要整理记录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 等。基本数据类型域描述符、引用数据类型域描述符。
    • 方法描述符:定义了方法的返回值和参数的表示形式,将参数类型的域描述符按声明顺序放入一对括号中(如果没有参数则不需要括号),括号后跟返回值类型的域描述符即形成方法描述符。
  • 调试记录:
    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,来保证线程数据安全。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值