一、Android-JNI开发

882 阅读26分钟

通过这个章节我们将掌握JNI的基本使用/开发

JNI与NDK介绍

JNI(Java Native Interface)

JNI描述的是一种技术:提供一种供Java字节码调用C/C++的解决方案

  • JNI是C语法实现的。
  • JNI是用于连接Java与C/C++的通讯桥梁,JNI是个转换器。
  • JNI调用Java通过反射,Java调用C/C++通过JNI,

NDK(Native Development Kit)

Android NDK 是一组允许您将 C 或 C++(“原生代码”)嵌入到 Android 应用中的工具,NDK描述的是工具集。

能够在 Android 应用中使用原生代码对于想执行以下一项或多项操作的开发者特别有用:

  • 在平台之间移植其应用。
  • 重复使用现有库,或者提供其自己的库供重复使用。
  • 在某些情况下提高性能,特别是像游戏这种计算密集型应用。

总结:

JNI: JNI 是 Java Native Interface 的缩写,即 Java 的本地接口。目的是使得 Java 与本地其他语言(如 C/C++)进行交互。JNI 是属于 Java 的,与 Android 无直接关系。

NDK: NDK 是 Native Development Kit 的缩写,是 Android 的工具开发包。作用是更方便和快速开发 C/C++ 的动态库,并自动将动态库与应用一起打包到 apk。NDK 是属于 Android 的,与 Java 无直接关系。

JNI 是实现的目的,NDK 是 Android 中实现 JNI 的手段。

数据类型转换

在安卓APP中:Java代码与C/C++代码他们属于同一个进程,他们在APP中分为两个分区:纯Java、C/C++。

在Java与C/C++沟通,如传递数据时,那么C/C++与我们的Java是有差异的,双方的数据不一定能相互读取。因此JNI提供了对应的数据类型(基础数据类型与引用数据类型)来匹配Java中的数据类型。所以在JNI中要接受Java传递的数据时,JNI要使用与Java相匹配的JNI数据类型。

由于Java支持函数重载,在Java与C/C++的函数匹配时,就不仅仅需要匹配函数名,还需要匹配函数的参数信息、返回值信息。因此JNI中提供了 函数签名信息字符

基础数据类型转换

Java类型JNI类型C/C++类型描述
boolean(布尔类型)jbooleanunsigned short无符号8位
byte(字节类型)jbytesigned char有符号8位
char(字符型)jcharunsigned short无符号16位
shor(短整型)jshortshort有符号16位
int(整型)jint/jsizeint有符号32位
long(长整型)jlonglong/long long(_int64)有符号64位
float(浮点型)jfloatfloat32位
double(双精度浮点型)jdoubledouble64位

引用数据类型转换

除了 Class String Throwable 基本数据 类型的数组外,其余所有Java对象的数据类型在JNI中都用 jobject 表示。

Java中的String也是引用类型,但是由于使用频率较高,所以在JNI中单独创建了一个jstring类型。

Java引用类型JNI类型Java引用类型JNI类型
All objectsjobectchar[]jcharArray
java.lang.Classjclassshort[]jshortArray
java.lang.Stringjstringint[]jintArray
java.lang.Throwablejthrowablelong[]jlongArray
Object[]jobjectArrayfloat[]jfloatArray
boolean[]jbooleanArraydouble[]jdoubleArray
byte[]jbyteArray
  • 引用类型不能直接在Native层使用,需要根据JNI函数进行类型的转化后,才能使用。
  • **多维数组(含二维数组)都是引用类型,需要使用jobjectArray类型来存取。
    **列如,二维整型数组就是指向一维数组的数组,其声明使用方式如下:
//获得一维数组的类引用,即jintArray类型。
jclass intArrayClass = env->FindClass("[I");//在 JNI 中,"[I" 表示一维 int 数组的签名。
//构造一个指向jintArray类一维数组的对象数组,该对象数组初始大小为lenght,数组中的元素类型是intArrayClass。
jobjectArray objectIntArray = env->NewObjectArray(length,intArrayClass,NULL);

JNI函数签名sig信息

签名信息的字符主要是用于JNI函数注册使用的,他们不属于JNI的数据类型,而是文本字符串,用来指代数据类型信息的。

JNI函数注册时需要用到 JNI函数签名信息字符 来进行参数类型、返回值类型的匹配。

**由于Java支持函数重载,因此仅仅根据函数名是没法找到对应的JNI函数的。
**为了解决这个问题,JNI将参数类型和返回值类型作为了函数的签名信息。

签名信息 可用来描述 对象信息, 以及描述 函数信息。

描述对象类型的叫做对象签名信息。

描述函数的叫做函数签名信息。

  1. JNI规范定义的 函数签名信息 **格式:
    **(参数1类型字符...)返回值类型字符
  2. JNI常用的数据类型及对应字符:
Java类型字符Java类型字符
voidVbyteB
booleanZ (容易误写成B)charC
intIshortS
longJ(容易误写成L)int[][I(数组以“I”开始)
doubleDStringLjava/lang/String;
floatFobject[]L+/分割完整类型** **如 String:[Ljava/lang/String;
array**[+类型描述符 ** int[] 类型的签名字符为: [I

注:JNI反射Java对象时,会用到 签名信息 来描述对象类型。

  1. 函数签名信息例子:
Java函数函数签名签名含义
String fun()"()Ljava/lang/String;"函数没有入参,函数返回值类型为String。
long fun(int i,Class c)"(ILjava/lang/Class;)J"I表示int,Ljava/lang/Class;表示Class类型。
void fun(byte[] bytes)"([B)V"函数有一个入参,入参类型时字节数组;函数返回值类型是Void(无返回值)。

JNI方法注册

Linxy/JNIDemo

静态注册

当Java层调用navtie函数时,会在JNI库中根据函数名查找对应的JNI函数。如果没找到,会报错。如果找到了,则会在native函数与JNI函数之间建立关联关系,其实就是保存JNI函数的函数指针。

下次再调用native函数,就可以直接使用这个函数指针。

JNI的函数名命名格式

Java_ + 包名(com.example.auto.jnitest)+ 类名(MainActivity) + 函数名(stringFromJNI) ****(需要将包名的 ****. ****改为 ****_ ****)

得到JNI函数名:Java_com_example_auto_jnitest_MainActivity_stringFromJNI

extern "C" JNIEXPORT jstring JNICALL
Java_com_xunua_MyFecDemo_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

静态注册的缺点

  • 要求JNI函数的名字必须遵循JNI规范的命名格式。
  • 名字冗长,容易出错。
  • 初次调用会根据函数名取搜索JNI中对应的函数,会影响执行效率。
  • 需要编译所有声明了native函数的Java类,每个类所生成的class文件都需要用javah工具生成一个头文件。

“静态注册Native函数”的调用流程

Java层中执行static{System.loadlibrary("")}加载so库时,会在虚拟机中生成一个函数映射表:

一般情况下,JNI(Java Native Interface)库中的函数是通过JNI函数表进行映射的。这个JNI函数表存储了Java中的方法和本地方法之间的映射关系。在Java本地库加载后,JNI函数表会被填充,以便Java虚拟机可以通过JNI调用本地方法。

虚拟机本身是一个C++的可执行文件,叫做libart.so(所有的安卓系统源码会被编译成libart.so);每次程序调用Java函数时都会走到libart.so。

在调用函数时,会通过dlopen("libart.so") 去hook虚拟机的.so,接着查找对应的函数,如果是普通函数,则从Java中找;如果为native函数,则通过映射表找。

接着虚拟机从映射表中根据函数名去找到对应函数地址并执行。

因为Java声明的native函数名是很简短的,但是在JNI中定义该函数时,函数名是有很多前缀的。那么Java层声明的Native函数名 是如何 与JNI实现的函数名去对应:

  1. 首先必须在Java代码中实现库的加载loadlibrary,用于生成本地函数的映射表。
  2. Java层调用Native函数时,Java层声明的函数名会主动的拼接 包名+当前类型+函数名。然后通过拼接后的字符串去映射表中找到对应的JNI函数地址。

动态注册

动态注册是通过提供一个函数映射表,注册给JVM虚拟机,这样JVM就可以用函数映射表来调用相应的函数,就不必通过函数名来查找需要调用的函数。

动态注册相比静态注册,静态注册是通过函数名等限定符并借助JNI来实现函数注册的。而动态注册则可以抛弃Native函数名规则,直接在JNI_OnLoad中进行函数方法的注册。

FFmpeg和Android系统的JNI函数,都是使用的动态注册。

动态注册的特点

  • 调用更快,动态注册函数的执行效率更高。
    因为虚拟机中会缓存动态注册的函数名所以效率更高;而静态注册函数每次在被调用时都要重新去表中查找(难度更大)。
  • 安全性高,防反编译的能力强。

动态注册的实现

原理:

利用 RegisterNatives 方法来注册 java 方法与 JNI 函数的一一对应关系;

实现流程:

  1. 利用结构体 JNINativeMethod 数组记录 java 方法与 JNI 函数的对应关系;
  2. 实现 JNI_OnLoad 方法,在加载动态库后,执行动态注册;
  3. 调用 FindClass 方法,获取 java 对象;
  4. 调用 RegisterNatives 方法,传入 java 对象,以及 JNINativeMethod 数组,以及注册数目完成注册;

优点:

  1. 流程更加清晰可控;
  2. 效率更高;

示例:

//实现的native函数
jstring stringFromJNI(JNIEnv *env, jobject thiz){
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

//创建JNINativeMethod数组来映射函数stringFromJNI。
//其中stringFromJNI是Java层的函数名,"()Ljava/lang/String;"是入参/返回值类型的字符,(jstring*)stringFromJNI 是当前native函数的指针
static const JNINativeMethod gMethods[] = {
	{"stringFromJNI", "()Ljava/lang/String;", (jstring*)stringFromJNI}
};

//动态库进行加载的入口。
//Java层调用System.loadLibrary时此回调会被执行。
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){
    __android_log_print(ANDROID_LOG_INFO, "native", "Jni_OnLoad");
    JNIEnv* env = NULL;
    if(vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) //从JavaVM获取JNIEnv,现在通常使用1.6的版本
        return -1;//如果获取JNIEnv失败,那么就return -1。
    
    jclass clazz = env->FindClass("com/xunua/MyFecDemo/MainActivity");
    if (!clazz){
        __android_log_print(ANDROID_LOG_INFO, "native", "cannot get class: com/xunua/MyFecDemo/MainActivity");
        return -1;
    }
    //sizeof(gMethods)/sizeof(gMethods[0]):在C中,测量数组的长度,就得使用sizeof测量结构体总字节大小再除以其中一个元素的字节大小来得到总数量。
    if(env->RegisterNatives(clazz, gMethods, sizeof(gMethods)/sizeof(gMethods[0]))!=JNI_OK)
    {
        __android_log_print(ANDROID_LOG_INFO, "native", "register native method failed!\n");
        return -1;
    }
    //注册成功,返回JNI版本号。
    return JNI_VERSION_1_6;
}
#include <android/log.h>

#define LOG_TAG "MyJNI"

#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

JNIEXPORT jint JNI_OnLoad(JavaVM* jvm,void* reserved){
    __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,"执行了JNI_OnLoad");
    JNIEnv* env = NULL;
    if(jvm->GetEnv((void **)(&env),JNI_VERSION_1_6)!=JNI_OK)
        return -1;
    //此时env得到了JNIEnv
    //借助env去获取class类
    jclass clazz=env->FindClass("com/xunua/MyFecDemo/MainActivity");
    if (!clazz){
        __android_log_print(ANDROID_LOG_INFO, "native", "cannot get class: com/xunua/MyFecDemo/MainActivity");
        return -1;
    }
    //开始注册native函数
    env->RegisterNatives(clazz,gMethods,sizeof(gMethods)/sizeof(gMethods[0]));
    return JNI_VERSION_1_6;
}

JNINativeMethod介绍

Java与JNI通过JNINativeMethod的结构体对象来建立函数映射表,它在jni.h头文件中定义,其结构内容如下:

typedef struct{
	const char* name;
	const char* signature;
	void*	fnPtr;
}JNINativeMethod;

结构体的第一个参数 name 是java 方法名;

第二个参数 signature 是函数签名信息,用于描述方法的参数与返回值;

第三个参数 fnPtr 是函数指针,指向 jni 函数;

其中,第二个参数 signature 使用字符串记录方法的参数与返回值,具体格式形如“()V”、“(II)V”,其中分为两部分,括号内表示的是参数,括号右侧表示的是返回值。

  1. 创建映射表后,调用RegisterNatives函数将映射表注册给JVM。
  2. 当Java层通过System.loadLibrary加载JNI库时,会在库中查找JNI_OnLoad函数。
    我们可以将JNI_OnLoad函数视为JNI库的入口函数,需要在这里完成所有函数映射和动态注册工作,及其他的一些初始化工作。

JavaVM*

在JNI_OnLoad函数中,我们能看到第一个参数类型是‘JavaVM*’

在JVM虚拟机中,我们的APK可以看做是一个线程,APK的代码都是运行在主线程。如果我们在native开辟了一个线程,那么native层开辟的这个线程是无法访问到APK主线程的代码的。因为线程与线程之间是完全隔离的。
如果我们在native开辟的子线程中,想要和Java层的主线程进行通讯,那么就一定要使用到JavaVM;以及我们动态注册也需要使用到JavaVM。(在native中是没有handle的。)
如果native子线程想要回调Java层,则可以通过JavaVM的AttachCurrentThread()函数来与主线程绑定,绑定之后我们的native子线程也可以反射回调Java层对象的函数。

native子线程回调Java层函数后,此时Java层的被回调函数会被执行在子线程中,所以如果此时想切换到主线程,那么就需要在Java层主动的进行一次线程切换,切换到主线程中去。

JavaVM->GetEnv

JavaVM->GetEnv 是 JNI(Java Native Interface)中的一个函数调用,用于获取与当前线程关联的 JNIEnv(Java环境)指针。这个函数通常在 JNI 的本地方法中使用,以便在本地代码中与 Java 代码进行交互。

jint (*jvm)->GetEnv(JavaVM *jvm, void **penv, jint version);

参数说明:

  • jvm:Java 虚拟机的指针,可以通过 JavaVM 结构体的二级指针进行访问。
  • penv:用于接收 JNIEnv 指针 的指针。在调用完成后,penv 将指向当前线程关联的 JNIEnv。
  • version:指定的JNI 版本号,通常使用 JNI_VERSION_1_4、JNI_VERSION_1_6

返回值:

返回值如下,通常用宏JNI_OK去判断本次获取JNIEnv指针是否成功。

  • JNI_OK (0): 表示成功获取与当前线程关联的 JNIEnv 指针。
  • JNI_EDETACHED (-2): 表示当前线程未附加到 Java 虚拟机。这种情况下,可以选择调用 *(jvm)->AttachCurrentThread 来附加线程。
  • JNI_EVERSION (-3): 表示请求的 JNI 版本不受支持。

env->FindClass("com/xunua/MyFecDemo/MainActivity");

FindClass 函数是 JNI 中的一个函数,用于在 Java 虚拟机中查找一个类。

函数原型:

jclass FindClass(JNIEnv *env, const char *name);
  • env:JNIEnv 指针,用于与 Java 虚拟机进行交互。
  • name:要查找的类的全限定名(包括包名和类名),使用斜杠( / )分隔包名和类名。如:"com/xunua/MyFecDemo$MainActivity"

返回值:

  • 该函数返回一个 jclass 对象,代表找到的 Java 类。如果找不到对应的类,返回 NULL

env->RegisterNatives

RegisterNatives是用于动态注册本地方法的函数。

RegisterNatives 是 JNI(Java Native Interface)中的一个函数,用于在运行时将本地方法注册到 Java 类中。通过注册本地方法,你可以在 Java 代码中调用这些本地方法,实现 Java 与本地代码的交互。

函数原型:

jint (*RegisterNatives)(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);

参数说明:

  • env:JNIEnv 指针,用于与 Java 虚拟机进行交互。
  • clazz:要注册本地方法的 Java 类的 jclass 对象。
  • methods:一个指向 JNINativeMethod 结构体数组的指针,每个结构体表示一个本地方法的信息,包括方法名、方法签名和本地实现的函数指针。即我们定义的本地方法数组。
  • nMethods:要注册的本地方法数量。即methods数组的数量。

函数返回值类型:

RegisterNatives 函数的返回值类型是 jint,它表示函数调用的结果或状态。

  • 如果注册本地方法成功,RegisterNatives 函数将返回 JNI_OK(等于 0)。
  • 如果注册失败,它可能返回 JNI_EEXIST 表示已经存在相同签名的本地方法,或者其他错误代码。

JNI函数语法

上面讲完了静态注册与动态注册,接下来讲JNI函数的简单语法。
静态注册与动态注册在函数语法的区别就是:

    1. 静态注册函数必须有 JNIEXPORT jstring JNICALL,而动态注册无。
    2. 静态注册函数的方法名需要按照JNI的命名规则去命名,而动态注册函数不用这么做。
    3. 静态注册是JNI去注册,而动态注册需要自己实现JNI_onLoad()函数去绑定Java与native函数。

下面以静态注册为例来讲解。

JNI函数的声明

以下列函数为例:

extern "C" //表示以C的语法来编译
JNIEXPORT jstring JNICALL   //jstring 表示返回值为jstring->string类型。
Java_com_xunua_MyFecDemo_NativeLibManager_getString(JNIEnv *env, jobject thiz) {//参数的顺序不可以随意修改。 JNIEnv *env 一定永远是每个jni函数的第一个参数。
    string hello = "Hello from C++";
	//使用 c_str() 获取 C 风格字符串的指针。C 风格字符串是以 null 字符 '\0' 结尾的字符数组。
    return env->NewStringUTF(hello.c_str());
}

C/C++与java的值传递:

  • C++字符串是C++的对象。JAVA字符串是JAVA的对象。两者是在不同区域的。JAVA的对象在方法区。C++的对象在栈区。
  • 所以如果要将C++栈区的String字符串传递给Java,那么就需要一个转换器;首先将C++的字符串转成C的字符数组,之后再通过转换器将C的字符数组存储到Java的方法区,再将这个java方法区的字符串引用返回给到java代码。
  • 代码中的hello.c_str()是将C++的字符串转成C的字符数组(因为C++的String结构比较复杂,所以转成C的字符数组结构更简单,在内存中读取/转换更简单);这里的转换器就是JNI代码中的env。
    "env->NewStringUTF"它将C的字符数组转为Java字符串后存储到Java的方法区,然后返回Java方法区中该字符串的引用。接着通过jni函数将JAVA字符串的引用返回给到java代码。(env->NewStringUTF不需要在native中释放;因为通过env去创建的对象,会由GC来管理它的内存与释放)

注: Java调用Native是通过JNI来实现的,但是Native层调用Java则是通过反射来调用的。

extern“C”

JNI是C语法实现的。所以在.cpp文件中,我们要添加 extern "C" 来声明当前代码是C的语法。

注:extern "C" 可以修饰 #include ,声明导入的头文件是C语法。

C++是高级语言,它使用gcc编译器来编译,与c使用的编译器是不一样的。所以添加extern“C”的目的是为了告知编译时使用C的编译器来编译。

如果不给jni函数声明extern“C”,那么程序在运行该函数时,会崩溃,报错信息为:No implementation found for java.lang.String.xxx.xxxx.func2() 在C++中没有发现该方法的实现。
上述错误的几种出现原因:①对应的jni函数上没有添加extern "C"。 ②在java代码中没有添加static{ System.loadLibrary("libname")} 加载库。 ③没有在build文件中声明externalNativeBuild属性。 ④apk内的lib文件夹下没有对应库(没有被编译进去apk中)

不添加 extern“C” **导致 No implementation found for java.lang.String.xxx.xxxx.func2() 错误的原因:
**因为C不支持重载,所以Java调用JNI函数时,编译器是根据JNI的函数名去匹配的。但是C++支持重载,所以Java调用JNI函数时,编译器会根据JNI的函数名+参数一起去匹配,所以出现找不到对应JNI函数的异常。

JNIEXPORT jstring JNICALL

静态注册函数中,会声明这个,用于定义函数可见性、函数返回值类型

JNIEXPORT jstring JNICALL

JNIEXPORT是一个宏,它其实是__attribute__ ((visibility ("default")))。

#define JNIEXPORT  __attribute__ ((visibility ("default")))
  • attribute ((visibility ("default")))
attribute方法属性
visibility可见性
“default” 取值:“hidden”则表示外部不可见,等价于private。表示默认值(等价于为public,允许我们的java层去访问)。
  • jstring:表示当前jni函数的返回值类型为string。
  • JNICALL:在源码中是jni的空实现宏,可以没有。

JNI函数的参数类型

//Java声明的非静态本地函数(native void)在JNI函数中的参数列表
(JNIEnv *env, jobject thiz) 

其中 JNIEnv 指代的是当前 java 环境,利用 JNIEnv 可以操作 java 层代码;jobject 指代的是 jni 函数对应的 java native 方法的类实例,如果 java 方法是 static,则代表的是 class 对象;

  • 参数的顺序不可以随意修改。
  • *JNIEnv env 一定永远是每个jni函数的第一个参数。JNIEnv 其实是 _JNIEnv 结构体的别名。
    这个 _JNIEnv 结构体内部定义了许多的函数,这些函数的作用基本上都是为了用于获取Java那边的内容。
    Java主线程中调用jni函数是,env都是同一个。
    如果在子线程或者其他不同的线程调用,那么jni函数收到的env不是同一个,属于不同线程。
    所以在不同现场执行jni函数时,需要在jni函数中切换线程去处理(后面补充)。
  • thiz 表示Java中定义该函数的实例对象

静态本地函数(static native void) 的jni函数中参数列表有些变化,会变为:

(JNIEnv *env, jclass clazz) 

即:第二个参数由jobject变成了jclass。

  • cLazz ****指的不再是Java中定义该函数的实例对象,而是指的定义该函数的Java类(非对象)。

Sig签名信息字符

上述的调用GetMethodID获取函数ID、GetFieldID获取成员变量ID时,最末尾的实参传入了sig字符串,那个就是对象的签名信息,jni就是通过签名信息来判断成员的数据类型、函数的入参与返回值;用以准确的查找/匹配正确的目标成员。签名信息在前面 JNI函数签名sig信息 中有详细讲解,此处略。

C++主动访问JAVA对象(C++反射)

C++中访问java对象其实是通过反射去实现访问的。 Java层的反射API其实本质上是调用C++的native函数去实现的,所以C++中的反射可以直接调用native函数去实现,会比Java的反射要更加直接,减少了一层调用链。

比如在Java层中定义了两个成员,并声明一个native函数:

public String mName="Test";
public int num=0;

public native void func3();

JNI层实现func3(),并在函数中做反射去修改mName、num。

extern "C"
JNIEXPORT void JNICALL
Java_com_xunua_MyFecDemo_MainActivity_func3(JNIEnv *env, jobject thiz) {
    jclass clazz=env->GetObjectClass(thiz);
    //拿到java中两个成员的id
    jfieldID mName = env->GetFieldID(clazz,"mName","Ljava/lang/String;");
    jfieldID num = env->GetFieldID(clazz,"num","I");
    //修改他们的值
    env->SetObjectField(thiz,mName,env->NewStringUTF("哈哈哈,被JNI改了"));
    env->SetIntField(thiz,num,50);
}

JNI实现回调Java方法

在java层创建普通函数,JNI通过反射来调用该Java函数,从而实现回调:

/**
 * 创建普通函数,用于native来调用当前函数。实现回调。
 * @param code返回的状态码
 */
public void onCallback(int code){
    Toast.makeText(this, "被native回调了,code:"+code, Toast.LENGTH_SHORT).show();
}

因为要模拟JNI去回调Java层的回调函数,所以Java层还需要再写一个native函数来模拟触发JNI函数来回调:

public native void func4();

JNI函数实现func4:

extern "C"
JNIEXPORT void JNICALL
Java_com_xunua_MyFecDemo_MainActivity_func4(JNIEnv *env, jobject thiz) {
    //通过对象thiz,来获取该类
    jclass clazz=env->GetObjectClass(thiz);
    //借助jclass,获取该类下对应的函数ID
    jmethodID onCallbackId = env->GetMethodID(clazz,"onCallback","(I)V");//通过函数签名信息 "(I)V" 来确保调用的函数准确性。
    //通过对象thiz 和对应函数id来调用函数。
	env->CallVoidMethod(thiz,onCallbackId,123);//callVoidMethod的Void表示目标函数返回值类型是void
}

当java层调用native函数func4时,JNI层会主动去调起Java层的onCallback回调函数,并返回状态码。

这样就完成了一次模拟JNI回调Java函数的过程。
调用流程:Java.func4() -> JNI.func4() -> java.onCallBack()

JNI中创建Java的Bean对象

C++访问Java对象中的方法我们已经在上个步骤实现了,那么如果在回调Java函数中,他的参数类型是Java的数据类呢,我们该如何返回Java自定义类型数据Bean对象?

实例:

package com.example;

public class MyJavaClass {
    private byte[] byteArray;
    private String stringValue;
    private byte byteValue;
    private long longValue;
    private int intValue;

    public MyJavaClass(byte[] byteArray, String stringValue, byte byteValue, long longValue, int intValue) {
        this.byteArray = byteArray;
        this.stringValue = stringValue;
        this.byteValue = byteValue;
        this.longValue = longValue;
        this.intValue = intValue;
    }

    // Getters and other methods...
}
#include <jni.h>
#include <stdint.h>

JNIEXPORT jobject JNICALL Java_com_example_MyClass_createJavaObject(JNIEnv *env, jobject thisObj) {
    // 获取类引用
    jclass myJavaClass = (*env)->FindClass(env, "com/example/MyJavaClass");

    // 获取构造函数的方法ID
    jmethodID constructor = (*env)->GetMethodID(env, myJavaClass, "<init>", "([BLjava/lang/String;BJI)V");

    // 创建一个byte数组
    int lenght=3;
    jbyteArray byteArray = (*env)->NewByteArray(env, (jint)lenght);
    uint8_t data[] = {1, 2, 3};
    // 将 uint8_t 数组内容拷贝到 jbyteArray
    (*env)->SetByteArrayRegion(env, byteArray, 0, 3, (jbyte*)data);

    // 创建一个字符串
    const char* stringValue = "Hello JNI";
    jstring jstringValue = (*env)->NewStringUTF(env, stringValue);

    // 使用 uint8_t 转换为 jbyte
    uint8_t byteValue = 42;
    jbyte jbyteValue = (jbyte)byteValue;

    // 使用 uint64_t 转换为 jlong
    uint64_t longValue = 1234567890;
    jlong jlongValue = (jlong)longValue;

    // 使用 uint32_t 转换为 jint
    uint32_t intValue = 42;
    jint jintValue = (jint)intValue;

    // 调用构造函数创建对象
    jobject myJavaObject = (*env)->NewObject(env, myJavaClass, constructor, byteArray, jstringValue, jbyteValue, jlongValue, jintValue);

    return myJavaObject;
}

实践案例:

typedef struct tagMsgUdpPacketHeader
{
    //一共35个字节(不包含内存对齐)
    uint32_t    		ulHtag;					// protocol header "ITCU"
    uint8_t     		ucVer;					// protocol version: 3
    uint32_t    		ulMsgLen;				// message body length = packet header len + data  len
    uint16_t    		usUdpSeqNo;				// Udp seqno
    uint32_t    		ulTcpMsgLen;			// tcp body length = tcp header len + message len
    uint16_t    		usTcpSeqNo;				// Tcp seqno
    uint64_t    		uxSessionId;			// session id: maybe meetingroom id
    uint32_t    		ulSenderId;				// sender id
    uint16_t    		uFecDataPktsNum;		// fec data packets number;
    uint16_t    		uFecRepairPktsNum;		// fec repair packets number;
    uint8_t             uFecPkgSizeUnits;       // fec packet buffer len;
} MsgUdpPacketHeader;

typedef struct tagMsgUdpPacketData
{
    MsgUdpPacketHeader* udpPacketHeader;//包头
    char* contentBytes;//视频流,要经过fec的部分。
}MsgUdpPacketData;
package com.itc.suppaperless.channels.udp;

import com.itc.suppaperless.channels.common.CommandData;

public class UDPMediaData extends CommandData {

    public UDPMediaData(String HEAD_TAG, byte strVer,long iSessionId,int iSenderID,int contentLength, byte[] content) {
        //protocol header "ITCU"
        this.HEAD_TAG=HEAD_TAG;
        //ucVer:protocol version: 3
        this.strVer=strVer;
        // session id: maybe meetingroom id
        this.iSessionId=iSessionId;
        // sender id
        this.iSenderID=iSenderID;
        //contentLength
        this.iMsgLength=contentLength;
        //contentData:h264
        this.content=content;
    }
}
package com.itc.suppaperless.channels.udp;

import android.util.Log;
import com.itc.suppaperless.utils.GsonUtil;

public class MediaUDPClient {
    public void onDecodeScreenDataCallback(UDPMediaData udpMediaData){
        Log.e(TAG, "onDecodeScreenDataCallback:   udpMediaData:"+ GsonUtil.objectToJson(udpMediaData));
    }
}
// 获取类引用
        jclass udpMediaDataClass = env->FindClass("com/itc/suppaperless/channels/udp/UDPMediaData");
        if(!udpMediaDataClass){
            LOGE("cannot get class: com/itc/suppaperless/channels/udp/UDPMediaData");
        }else{
            //创建对象,返回包头和数据
//            String HEAD_TAG, byte strVer,long iSessionId,int iSenderID,int contentLength, byte[] content
            // 获取构造函数的方法ID
            jmethodID constructorMethodId = env->GetMethodID(udpMediaDataClass, "<init>","(Ljava/lang/String;BJII[B)V");
            // 调用构造函数创建对象
            jstring head_tag_str = env->NewStringUTF(ulHtagStr);
//            jstring head_tag_str = env->NewString(msgUdpPacketHeaderBean->ulHtag,4);
            LOGE("创建对象   head_tag_str  test01");
            const char *utfChars = env->GetStringUTFChars(head_tag_str, NULL);
            LOGE("创建对象   head_tag_str  test02  %p",utfChars);
            LOGE("创建对象   head_tag_str:%s",utfChars);
            env->ReleaseStringUTFChars(head_tag_str, utfChars); // 释放资源

            // 创建一个byte数组
            jbyteArray contentArrayForJni = env->NewByteArray((jint)result.second);
            // 将 uint8_t 数组内容拷贝到 jbyteArray
            env->SetByteArrayRegion(contentArrayForJni, 0,(jint)result.second, (jbyte*)result.first);


            jobject udpMediaDataForJavaObject = env->NewObject(udpMediaDataClass, constructorMethodId,
                                                               head_tag_str,
                                                               (jbyte)msgUdpPacketHeaderBean->ucVer,
                                                               (jlong)msgUdpPacketHeaderBean->uxSessionId,
                                                               (jint)msgUdpPacketHeaderBean->ulSenderId,
                                                               (jint)result.second,
                                                               contentArrayForJni
                                                  );
            jclass clazz=env->GetObjectClass(thiz);
            jmethodID onDecodeScreenDataCallback = env->GetMethodID(clazz,"onDecodeScreenDataCallback","(Lcom/itc/suppaperless/channels/udp/UDPMediaData)V");
            env->CallVoidMethod(thiz,onDecodeScreenDataC

JNI编译

Android Studio现有打包so库的方式有两种:

  1. ndk-build编译项目
  2. CMake脚本构建项目(主流)

在最新版的Android Studio,是使用CMake来进行项目构建。

ndk-Build编译

android studio ndk-build 编译C生成.so文件(ndk基础篇),看完你就懂了

1、下载NDK及构建工具

为您的应用编译和调试原生代码,您需要以下组件:

  • Android 原生开发工具包 (NDK) :这套工具集允许您为 Android 使用 C 和 C++ 代码,并提供众多平台库,让您可以管理原生 Activity 和访问物理设备组件,例如传感器和触摸输入。
  • CMake:一款外部构建工具,可与 Gradle 搭配使用来构建原生库。如果您只计划使用 ndk-build,则不需要此组件。
  • LLDB:一种调试程序,Android Studio 使用它来调试原生代码。(目前已知 Android4.0.4 版本及之后的版本都已将LLDB调试工具内置到了NDK中)

上述的工具都可以在Android Studio的setting->Android SDK中下载。

Android Studio下载NDK后已经内置了LLDB,无需单独下载, 安装 Cmake+NDK 即可直接调试JNI程序。

2、配置项目NDK版本

  1. 新建一个Android项目,如:NdkDemo,包名:com.xunua.pdfdemo。
  2. File->Project Structyu->Android NDK location中进行NDK版本/路径的选择即可。
    一般使用NDK21版本就可。
  3. 在gradle.properties中新增代码以启用NDK。
android.useDeprecatedNdk=true

之后项目环境就配置好了,可以开始编写java和c的代码了。

3、Java代码与C代码的编写过程

  1. 首先新建一个java类JNIUtils.java(用于so库的初始化加载及native函数的声明),代码如下:
public class JNIUtils {
    // 加载native-jni
    static {
        System.loadLibrary("native-jni");
    }
    //java调C中的方法都需要用native声明且方法名必须和c的方法名一样
    public native String stringFromJNI();
}
  1. 重新Make Project一下工程,完成后会在工程目录 ... /NdkDemo/app/build/intermediates/classes/debug/com/niwoxuexi/ndkdemo 看到自己编译后的classes文件JNIUtils.class
  2. 用javah工具生成头文件
    1. 首先新建一个java类JNIUtils.java。
    2. 打开Terminal命令行工具(Alt+F12)。
    3. 在命令行中先进入到工程的main目录下。
    4. 输入命令:javah -d jni -classpath 自己编译后的class文件的绝对路径
      例如:
      javah -d jni -classpath /Users/zhuxiaocheng/android/workspace/NdkDemo/app/build/intermediates/classes/debug com.niwoxuexi.ndkkemo.JNIUtils
      需要注意的点: debug后的空格 windows 系统路径中的文件的分割线是 '' 而不是mac系统的 '/'
    5. 执行javah命令之后就会在main目录下生成jni文件夹,同时生成.h文件 如下图所示。
  1. 接下来,我们在jni目录下新建一个 native-lib.c 的c文件,内容如下:
#include "com_niwoxuexi_ndkdemo_JNIUtils.h"
/**
 * 上边的引用标签一定是.h的文件名家后缀,方法名一定要和.h文件中的方法名称一样
 */
JNIEXPORT jstring JNICALL Java_com_xunua_pdfdemo_JNIUtils_stringFromJNI
        (JNIEnv *env, jobject ojb){
    return (*env) -> NewStringUTF(env,"Hello, I'm from jni");
}
  1. 之后在app的build.gradle配置文件中添加如下代码:
//ndk编译生成.so文件
ndk {
    moduleName "native-lib"         //生成的so名字
    abiFilters "armeabi", "armeabi-v7a", "x86"  //输出指定三种abi体系结构下的so库。
}

  1. 最后进行我们JNI函数的验证,只需要在MainActivity中调用一下C的代码就可以了
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView textView = (TextView) findViewById(R.id.text);
        textView.setText(new JNIUtils().stringFromJNI());
    }
}
  1. 直接运行项目,结果如下所示:

4、将.so库的引入其他项目并使用

前面的几个步骤已经帮助我们完成了NDK代码的编写,及使用ndk-build对ndk代码编译后;编写Java类、JNI函数声明、NDK的加载,实现了对NDK代码的调用。那么如果想要将我们项目中生成的.so库提供给其他项目使用的话,该怎么做呢?

  1. 首先从项目中找到.so文件,位置如下:
  2. 将生成的.so库放到新项目的libs目录下。
  3. 在app module下的build.gradle中添加下面代码:
//放在libs目录中
sourceSets {
    main {
        jniLibs.srcDirs = ['libs']
   }
}

如图所示:

  1. so库的初始化及native函数的声明。
    参考步骤3-1新建一个java类JNIUtils.java 来实现对NDK的初始化及Native函数的声明和调用。

这样便完成了对.so库的调用。

Cmake编译

CMake 则是一个跨平台的编译工具,它并不会直接编译出对象,而是根据自定义的语言规则(CMakeLists.txt)生成对应 makefile 或 project 文件,然后再调用底层的编译, 在Android Studio 2.2 之后支持Cmake编译。

  • add_library指令
    语法:add_library(libname [SHARED | STATIC | MODULE] [EXCLUDE_FROM_ALL] [source])
    将一组源文件source编译出一个库文件,并保存为libname.solib前缀是生成文件时CMake自动添加上去的)。
    其中有三种库文件类型,不写的话默认为STATIC:
    • SHARED:表示动态库,可以在(java)代码中使用System.loadLibrary(name);动态调用。
    • STATIC:表示静态库,集成到代码中会在编译时调用。
    • MODULE:只有在使用 dyId 的系统有效,如果不支持dyid,则被当做SHARED对待。
    • EXCLUDE_FROM_ALL:表示这个库不被默认构建,除非其他组件依赖或手工构建。
#将compress.c 编译成 libcompress.so 的共享库
add_library(compress SHARED compress.c)
  • find_library指令**
    ****语法:find_library(name1 path1 path2 ...)
    name1表示将find到的库取的别名。
    **path1 path2 ...变量表示找到的库全路径,包含库文件名。如:
find_library(libx x11 /usr/lib)
find_library(log-lib log)#路径为空时,应该是查找系统环境变量路径。
  • target_link_libraries指令
    语法:target_link_libraries(target library <debug | optimized> library2...)
    这个指令可以用来为target
    添加需要链接的共享库library,同样也可以用于为自己编写的共享库添加共享库链接。如:
#指定 compress 工程需要用到 libjpeg 库和 log 库
target_link_libraries(compress libjpeg ${log-lib})

CMake的简易使用

  1. 首先下载NDk,这个步骤与NDK-build编译一致。
  2. 在module的build.gradle中添加CMake清单声明:
android {
    //......
	externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }
}
  1. 在main目录下创建cpp文件夹
  2. 在cpp文件夹下创建CMakeLists.txt文件、自己编写c代码的.cpp文件。
  3. CMakeLists.txt中配置生成的so库的名称、要引入的三方库、及需要编译入so库的自己编写的.cpp代码的文件名。
  4. 编写JAVA类,定义需要JNI去实现的本地函数getString()
public class NativeLibManager {
    static {
        System.loadLibrary("native-lib");
    }
    
    public native String getString();
}
  1. 去CPP文件夹下的native-lib.cpp中实现getString()函数:
extern "C" //表示以C的语法来编译
JNIEXPORT jstring JNICALL   //函数签名信息,表示返回值为jstring->string类型。
Java_com_xunua_MyFecDemo_NativeLibManager_getString(JNIEnv *env, jobject thiz) {
    string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

AS4.0之后的版本都是支持自动创建对应的JNI函数的:

  1. 在安卓项目中,调用NativeLibManager的getString(),能拿到对应的内容就表示执行成功。

Android Studio中CMake的编译过程简述

后面的章节会更加详细讲这部分

  1. 程序开始编译时,AS会从build.gradle找externalNativeBuild的cmake配置信息。从配置信息中找到CMakeLists.txt文件的位置、版本号。
    as4.0版本之前的CMakeLists.txt文件位置是在app-module下;4.0及之后的版本则是在 /main/cpp文件夹下存放的。
  2. 将第一步骤得到的信息交给Android SDK目录里的CMake.exe可执行文件。
  3. CMakeLists.txt文件中的cmake_minimum_required(VERSION 3.10.2) 定义了编译所需的CMake最低版本号,必须高于这个最低版本号。
  4. CMake会按照CMakeLists.txt文件中的配置信息来进行库的编译。

CMakeLists文件基本配置介绍

//要求CMake的最小版本
cmake_minimum_required(VERSION 3.10.2)

# Declares and names the project.
# 当前项目的名称
project("MyFecDemo")

#配置要被编译入库中的代码文件
add_library( # Sets the name of the library.
             # 本地lib库的名称
             native-lib

             # Sets the library as a shared library.
        	 # 设置本地库的权限 SHARED属于动态库,STATIC属于静态库
             SHARED

             # Provides a relative path to your source file(s).
             native-lib.cpp 
             # 如果有新增的文件,需要使用到,那么必须在此继续注册,如新增native-lib02.cpp
             native-lib02.cpp
             # 支持添加多个.cpp、.a、.so文件。
           )

#查找三方库
find_library( # Sets the name of the path variable.
              # 给找到的库取的别名为 log-lib ,log-lib是一个变量名。
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              # 从系统环境变量配置的系统库路径中查找liblog.so库
              # 库名叫做log,但是我们编译出来的so库文件会被默认在库名前加上'lib'前缀。
              # 所以编译器去查找log库时,实际找的是liblog.so文件。
              log )

#将查找到的三方库链接到当前库
target_link_libraries( # Specifies the target library.
                      //被链接到 native-lib库中去。
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                      #将find_library得到的库的别名通过 ${} 取出来,然后将这个库链接到native-lib中。
                       ${log-lib} )

纯Java项目中实现JNI

参考链接: Java JNI实现原理初探 JAVA JNI简单实现 JAVA基础之理解JNI原理

介绍

在Android的Native项目中,我们在Java层定义JNI函数时,Android Studio会自动帮我们编译并创建JNI函数 及 自动生成SO库。本篇文章则是在Java项目中,手动使用javah生成头文件,手动调用gcc编译so库;进行了一个完整的JNI实现流程。

JNI的简单实现(静态注册+生成动态库)

假设当前的目录结构如下:

-
| - xunua
						| Test.java

1、首先编写java文件:

package xunua;
public class Test{
    static{
        System.loadLibrary("bridge");
    }
    
    public native int nativeAdd(int x,int y);
    
    public static void main(String[] args){
        Test obj = new Test();
        System.out.printf("%d\n",obj.nativeAdd(2012,3));
    }
}

代码很简单,这里声明了 nativeAdd(int x,inty) 的方法,执行的时候简单的打出执行的结果。另外这里调用API去加载 名为 bridge 的库,接下来就来实现这个库。

2、生成JNI调用需要的头文件

生成头文件需要使用到javah.exe工具,这个工具在Java SDK的bin目录中。

javac xunua/Test.java	
javah -jni xunua.Test

第一行代码的作用是:编译xunua包下的Test.java文件,生成Test.class可执行文件文件。

第二行代码的作用是:将xunua包下的Test类,通过javah生成JNI调用所需要的.h头文件:xunua.h

执行完javah命令后的目录结构是这样的:

-
| - xunua
						| Test.java
						| Test.class
| - xunua_Test.h

xunua_Text.h头文件内容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class xunua_Test */
//对_Included_xunua_Test的判断是为了避免头文件的重复引入。
#ifndef _Included_xunua_Test	//如果宏_Included_xunua_Test没有被定义,那么执行下面的代码。
#define _Included_xunua_Test	//定义宏_Included_xunua_Test。

#ifdef __cplusplus	//判断当前编译器是否 是支持 cplusplus,如果是C++的编译器,那么就添加extern“C”让编译器以C的方式编译。   如果不是C++的环境,说明是C的环境,那么就不需要添加extern"C"了。
extern "C" {
#endif

JNIEXPORT jint JNICALL Java_xunua_Test_nativeAdd
    (JNIEnv *, jobject, jint, jint);
    
#ifdef __cplusplus
}
#endif

对javah工具生成.h头文件的代码阅读

经常会见到__cplusplus关键字,比如下面的代码:

#ifdef __cplusplus  //当前是否是CPP文件中
extern "C" {
#endif
JNIEXPORT jint JNICALL Java_xunua_Test_nativeAdd
	(JNIEnv *, jobject, jint, jint);
#ifdef __cplusplus
}
#endif

这里面,两种关键字,都是为了实现C++与C兼容的;extern “C”是用来在C++程序中声明或定义一个C语言代码块的符号,比如:

extern “C” {
    int func(int);
    int var;
}

上面的代码中,C++编译器会将 在extern “C”的大括号内部的代码当做C语言来处理。

由于C和C++毕竟是不同的,为了实现某个程序在C和C++中都是兼容的,如果定义两套头文件,未免太过麻烦,所以就有了cplusplus的出现,这个是在C++中特有的, cplusplus就是C++,也就有了上面第一段代码的使用。

  • 如果这段代码是在C++文件中出现,那么经过编译后,该段代码就变成了:
/**********C++文件中条件编译后的结果***************/
extern "C" {
JNIEXPORT jint JNICALL Java_xunua_Test_nativeAdd
	(JNIEnv *, jobject, jint, jint);
}
  • 而在C文件中,经过条件编译,该段代码变成了:
JNIEXPORT jint JNICALL Java_xunua_Test_nativeAdd
	(JNIEnv *, jobject, jint, jint);

3、native方法的实现

接下来新增bridge.c文件来实现之前声明的native方法,目录结构如下:

-
| - xunua
			| Test.java
			| Test.class
| - xunua_Test.h
| - bridge.c

bridge.c的内容如下:

#include "xunua_Test.h"//将javah生成的头文件引入

//对头文件声明的函数进行定义
JNIEXPORT jint JNICALL Java_xunua_Test_nativeAdd
	(JNIEnv * env, jobject obj, jint x, jint y){
	return x+y;
}

这里的实现只是简单的把两个参数相加,然后返回结果。

4、生成动态链接库

配置好mingw64工具后,在bin目录下有gcc.exe工具。

linux指令:gcc -shared -I /usr/lib/jdk1.6.0_45/include-I /usr/lib/jdk1.6.0_45/include/linux bridge.c -o libbridge.so

windows指令:gcc -shared -o dll_demo.dll bridge.c //将bridge.c文件 编译成windows可执行文件dll_demo.dll。

注意:gcc编译时如果出现找不到jni.h库,那么就去sdk中将jni.h和jni_md.h导入过来当前目录作为本地文件。检查bridge.c的引入方式,include<>是引入系统库中文件,include" "是引入本地的文件;避免引入方式不正确而无法编译。

我们在java代码中调用的是**System.loadLibrary**("xxx")加载so库,那么生成的动态链接库的名称就必须是libxxx.so的形式(这里指Linux环境;windows中生成是.dll后缀(即:dll_xxx.dll形式),使用'System.load("xxx.dll")'进行加载,它们在使用上没有区别),否则在执行java代码的时候,就会报 java.lang.UnsatisfiedLinkError: no XXX in java.library.path 的错误!也就是说找不到这个库。

生成动态链接库之后的目录结构如下:

-
| - xunua
            | Test.java
            | Test.class
| - xunua_Test.h
| - bridge.c
| - libbridge.so

5、执行代码验证结果

java -Djava.library.path=. xunua.Test

//输出2015

Java调用JNI的最简单例子完成。

JNI技术实现原理

我们知道cpu只认得 “0101101” 类似这种二进制符号, C、C++ 这些代码最终都得通过编译、汇编成二进制代码,cpu才能识别并执行。。而Java比C、C++又多了一层虚拟机,过程也复杂许多。Java代码经过编译成class文件、虚拟机装载等步骤最终在虚拟机中执行。class文件里面就是一个结构复杂的表,而最终告诉虚拟机怎么执行的就是靠里面的字节码说明。

Java虚拟机在执行的时候,可以采用解释执行和编译执行的方式执行,但最终都是转化为机器码执行。

Java虚拟机运行时的数据区,包括:方法区、虚拟机栈、堆、程序计数器、本地方法栈。

问题来了,按目前的理解,如果是解释执行,那么方法区中应该存的是字节码,那执行的时候,通过JNI 动态装载的c、c++库,放哪去?怎么执行?

那么,刚刚生成的动态链接库”libbridge.so”是如何装进内存的?native方法怎么调用?跟普通的方法调用有什么区别吗?

我们把Test.java改改,增加普通的方法 int add(int x,int y) :

public class Test{
    static{
        System.loadLibrary("bridge");
    }
    public native int nativeAdd(int x,int y);
    public int add(int x,int y){
        return x+y;
    }
    public static void main(String[] args){
        Test obj = new Test();
        System.out.printf("%d\n",obj.nativeAdd(2012,3));
        System.out.printf("%d\n",obj.add(2012,3));
    }
}

接下来将它编译成class文件,看看class文件中,native方法和普通方法有何区别:

javac hackooo/Test.java
javap -verbose hackooo.Test

解析后,”nativeAdd”和”add”两个方法的结果如下:

  public native int nativeAdd(int, int);
    flags: ACC_PUBLIC, ACC_NATIVE

  public int add(int, int);
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1       
         1: iload_2       
         2: iadd          
         3: ireturn       
      LineNumberTable:
        line 8: 0

可见,普通的“add”方法是直接把字节码放到code属性表中,而native方法,与普通的方法通过一个标志“ACC_NATIVE”区分开来。java在执行普通的方法调用的时候,可以通过找方法表,再找到相应的code属性表,最终解释执行代码,那么,对于native方法,在class文件中,并没有体现native代码在哪里,只有一个“ACC_NATIVE”的标识,那么在执行的时候改怎么找到动态链接库的代码呢?

深入原理参考此文章:Java JNI实现原理初探_jnibridge-CSDN博客

Question

FAQ

  1. java字符串为什么在方法区:JVM方法区主要存储:class的字节码、字符串、常量池。
  2. java调用的JNI方法都需要加 extern “C”,后面如果使用动态注册的话就可以不加extern “C”。
  3. 在C语言的JNI函数中,有定义返回值类型的JNI函数,如果不return返回值,那么也可以编译通过,但是运行执行该函数时会报错崩溃。
  4. jni方法中参数无法传接口。因为java和jni是不同的东西,jni没有对java接口的支持。但是JNI可以通过反射去实现回调Java函数。

如何区分安卓项目是java工程还是native工程?

  1. build文件中是否有externalNativeBuild声明,有则表示属于native工程:
//native工程的入口
externalNativeBuild{
	cmake{
        path"src/main/cpp/CMakeLists.txt"
        version "3.10.2"
    }
}
  1. 检查代码中是否有 native修饰的本地函数声明。

如何区分生成的lib库是静态库还是动态库

在as中打开output中编译的apk,看lib目录下生成的native库是以何种后缀结尾:

  1. .so结尾是动态库。(.so理解为插件化)
  2. .a结尾则是静态库。(.a理解为组件化)

JNI的静态注册/动态注册、静态库/动态库、ndk-build编译/cmake编译的关系?

在Android开发中,特别是使用NDK(Native Development Kit)进行本地代码(C/C++)的开发,涉及到JNI的静态注册、动态注册、静态库、动态库、ndk-build编译以及cmake编译。让我们逐步解释它们之间的关系:

  1. JNI(Java Native Interface): ****是Java提供的一种机制,允许Java代码调用本地(C/C++)代码。JNI提供了Java和本地代码之间的桥梁
  2. 静态注册和动态注册:
    • 静态注册: 在Java源代码中使用native关键字声明本地方法,并通过javah工具生成包含函数签名的头文件,然后在C/C++代码中实现这些本地方法。这些方法在Java类加载时被静态注册到JNI中。
    • 动态注册: 在C/C++代码中使用JNI提供的函数动态注册本地方法,而不是在Java代码中静态声明。这样,你可以在运行时选择注册哪些本地方法。
  1. 静态库和动态库:
    • 静态库: 编译时链接到目标程序,形成一个可执行文件。 .a(Unix/Linux)或 .lib(Windows)是静态库的常见扩展名。
    • 动态库: 在运行时动态链接到目标程序。 .so(Unix/Linux)或 .dll(Windows)是动态库的常见扩展名。
  1. ndk-build编译:
    • ndk-build 是Android NDK提供的一个构建工具,用于编译、链接和构建本地代码。通过编写Android.mk文件,你可以配置项目的构建过程,包括静态/动态库的编译和链接,以及JNI本地方法的静态注册。
  1. cmake编译:
    • cmake 是一种跨平台的构建工具,可以用于配置、编译和构建项目。在Android开发中,可以使用CMake来代替ndk-build,并且它提供更灵活和现代的构建配置。你可以编写CMakeLists.txt文件来描述项目的结构和依赖关系。

关系总结:

  • 无论是使用ndk-build还是CMake,它们都是用于构建NDK项目的工具,可以配置本地代码的编译、链接和构建。
  • JNI的静态注册和动态注册是关于本地方法在JNI中如何注册的两种不同方式。
  • 静态库和动态库是两种不同的库的形式,它们在编译和链接时的行为不同。在Android NDK中,通常使用动态库( .so文件)。
  • 在JNI中,静态注册和静态库之间没有直接的关系。无论是静态注册还是动态注册,都可以生成动态库( .so文件)或者静态库( .a.lib文件)。

在实际开发中,你可以选择使用ndk-build或者CMake来进行项目构建,同时选择静态注册或动态注册方式,具体取决于你的项目需求和个人偏好。

JNI中的异常如何捕获

JNI中C代码的异常,是无法在Java代码中去捕获的;需要在JNI的代码中,使用C/C++的try catch来进行捕获。

Native的反射使用方式非常接近于直接调用函数

Native的反射与Java的反射是不一样的,这归功于 JNIEnv env* ,我们通过env反射拿到的jmethodID是一个指针,这个指针指向了方法的字节码指令,所以Native层通过env实现的反射要快于Java的普通反射。

Mediacodec的硬编解码 与 FFmpeg的硬编解码有何区别?项目中如何选择?

FFmpeg的硬编解码是通过C++反射直接调用Mediacodec。所以两者的硬编解码没有什么区别。

音频的软解码faac库与FFmpeg的软解码哪个好?

  • 如果项目在音频方面是简单应用,那么就使用faac库。
  • 如果项目是专业的音频应用,那么就使用FFmpeg。

FFmpeg的播放器实现,视频渲染使用C++来写是不是高效点?

Native层实现视频渲染确实会更高效。

在CPP文件中引入C语言库后,出现大量报错(且代码实际是无错误的),该怎么处理?

这就要活用#ifdef 与 extern"C"了,在CPP文件中,我们的代码会由编译器来决定是使用C的编译方式来编译,还是使用C++的编译方式来编译。出现报错大概率是因为C的代码被当做C++代码后,使用C++的编译方式去进行编译了。那么此时我们就需要通过指令来声明当前引入的库文件是C语言库,需要使用C语言的编译方式去编译。
比如我们引入一个so库,并且它提供的头文件名为:of_openfec_api.h。

那么在我们写的JNI文件中,需要做如下声明:

//代码解读: 如果当前是C++的编译环境,那么则声明当前引入的代码是C代码,要使用C的方式去编译。
#ifdef __cplusplus
extern "C" {
#endif
	#include <of_openfec_api.h>
#ifdef __cplusplus
};
#endif

注:

  • 最新的CMake版本,好像是支持自动识别C与C++代码的。