概念
JNI:Java本地调用 ,是Java Native Interface的缩写。JNI是一种技术,可以做到以下两点:
- Java程序中的函数可以调用Native语言写的函数,Native一般指的是C/C++编写的函数。
- ·Native程序中的函数可以调用Java层的函数,也就是在C/C++程序中可以调用Java的函数。
为什么需要jni
- C/C++语言已经有了很多成熟的模块,Java只需要直接调用即可。还有一些追求效率和速度的场合,需要Native参与。
-
Java语言是平台无关,但是承载Java世界的虚拟机是用Native语言写的,而虚拟机又运行在具体平台上,所以虚拟机本身无法做到平台无关,JNI技术可以针对Java层屏蔽不同操作系统之间的差异,这样就能够实现平台无关特性。
基本使用
- 在Java代码里面声明Native方法原型,比如
public native String stringFromJNI();
- java静态代码加载so库( JNI层必须实现为动态库的形式,这样Java虚拟机才能加载它并调用它的函数)
static {
System.loadLibrary("JniDemo");
}
JniDemo是JNI库的名字。实际加载动态库的时候会拓展成libJniDemo.so,在Windows平台上将拓展为JniDemo.dll。
- java代码调用native函数
public class MainActivity extends AppCompatActivity {
// Used to load the 'JniDemo' library on application startup.
static {
System.loadLibrary("JniDemo");
}
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(stringFromJNI()); // java代码调用native函数
}
/**
* A native method that is implemented by the 'JniDemo' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}
- 在C/C++代码里面声明JNI方法原型并实现
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_JniDemo_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
- extern "C"根据需要动态添加,如果是C++代码,则必须要添加extern “C”声明,如果是C代码,则不用添加。(extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。)
- JNIEXPORT 这个关键字说明这个函数是一个可导出函数,C/C++ 库里面的函数有些可以直接被外部调用,有些不可以,原因就是每一个C/C++库都有一个导出函数列表,只有在这个列表里面的函数才可以被外部直接调用,类似Java的public函数和private函数的区别。 JNI层必须实现为动态库的形式,这样Java虚拟机才能加载它并调用它的函数。
- 说明这个函数是一个JNI函数,用来和普通的C/C++函数进行区别,实际发现不加这个关键字,Java也是可以调用这个JNI函数的。
- jstring 这个函数的返回值是jstring
- Java_com_example_JniDemo_MainActivity_stringFromJNI(JNIEnvenv, jobject thiz)这是完整的JNI函数声明,JNI函数名的原型如下:
Java_ + JNI方法所在的完整的类名,把类名里面的”.”替换成”_” + 真实的JNI方法名,这个方法名要和Java代码里面声明的JNI方法名一样+ JNI函数必须的默认参数(JNIEnv env, jobject thiz), env参数是一个指向JNIEnv函数表的指针,thiz参数代表的就是声明这个JNI方法的Java类的引用
JNI调用流程
通过原生Android代码分析流程
java: MediaCrypto.java
jni:android_media_MediaCrypto.cpp
MediaCrypto.java 部分代码
99 private static native final void native_init(); //声明一个native函数。native为Java的关键字,表示它将由JNI层完成。
100
101
102
104 static {
105 System.loadLibrary("media_jni"); //加载对应的JNI库,media_jni是JNI库的名字
106 native_init(); //调用 jni native_init函数
107 }
108
加载JNI库
Java要调用Native函数,就必须通过一个位于JNI层的动态库才能做到。加载动态库的时机原则上是在调用native函数前,任何时候、任何地方加载都可以。通行的做法是,在类的static语句中加载,通过调用System.loadLibrary方法就可以了。Java的native函数
从上面代码中可以发现,native_init函数前有Java的关键字native,它表示这两个函数将由JNI层来实现。、只要完成下面两项工作就可以使用JNI了:
· 1. 加载对应的JNI库。
· 2. 声明由关键字native修饰的函数。
android_media_MediaCrypto.cpp 部分代码
static void android_media_MediaCrypto_native_init(JNIEnv *env) { //这个函数是native_init的JNI层实现。
160 jclass clazz = env->FindClass("android/media/MediaCrypto");
161 CHECK(clazz != NULL);
162
163 gFields.context = env->GetFieldID(clazz, "mNativeContext", "J");
164 CHECK(gFields.context != NULL);
165 }
android_media_MediaCrypto_native_init是native_init的jni 层实现, java层和native层需要将这2个函数绑定形成关联关系,系统才能找到它们,也就是接下来要说的注册.
注册
动态注册
在JNI技术中,用JNINativeMethod的结构来记录对应关系,其定义如下
typedef struct {
const char* name; //Java中native函数的名字,不用携带包的路径。例如“native_init“。
const char* signature;//Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合。
void* fnPtr; //JNI层对应函数的函数指针,注意它是void*类型。
} JNINativeMethod;
如何使用这个数据结构呢,看下对应 android_media_MediaCrypto.cpp 文件代码
6 static const JNINativeMethod gMethods[] = {
307 { "release", "()V", (void *)android_media_MediaCrypto_release },
308 { "native_init", Java中native函数的函数名。
"()V", native_init签名信息,后面再做介绍
(void *)android_media_MediaCrypto_native_init //JNI层对应函数指针。
},
310
312 ...
324 };
325
326 int register_android_media_Crypto(JNIEnv *env) { //注册JNINativeMethod数组
327 return AndroidRuntime::registerNativeMethods(env,
328 "android/media/MediaCrypto", gMethods, NELEM(gMethods));
329 }
- 定义一个JNINativeMethod数组,其成员就是所有native函数的一一对应关系。
- 注册JNINativeMethod数组 registerNativeMethods .第二个参数表明是Java中的哪个类
AndroidRunTime类提供了一个registerNativeMethods函数来完成注册工作,实际上是调用JNIEnv的RegisterNatives函数完成注册的
int jniRegisterNativeMethods(JNIEnv* env, const char* className,
320 const JNINativeMethod* methods, int numMethods)
321 {
322 ALOGV("Registering %s's %d native methods...", className, numMethods);
323 jclass clazz = (*env)->FindClass(env, className);
324 ALOG_ALWAYS_FATAL_IF(clazz == NULL,
325 "Native registration unable to find class '%s'; aborting...",
326 className);
327 int result = (*env)->RegisterNatives(env, clazz, methods, numMethods);
328 (*env)->DeleteLocalRef(env, clazz);
329 ...
347 }
注册函数register_android_media_Crypto调用时机
1463 jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
1464 {
1465 JNIEnv* env = NULL;
1466 jint result = -1;
1467
1468 if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
1469 ALOGE("ERROR: GetEnv failed\n");
1470 goto bail;
1471 }
1472 assert(env != NULL);
1473
1474 if (register_android_media_ImageWriter(env) != JNI_OK) {
1475 ALOGE("ERROR: ImageWriter native registration failed");
1476 goto bail;
1477 }
...
1554 if (register_android_media_Crypto(env) < 0) {
1555 ALOGE("ERROR: MediaCodec native registration failed");
1556 goto bail;
1557 }
}
当Java层通过System.loadLibrary加载完JNI动态库后,紧接着会查找该库中一个叫JNI_OnLoad的函数,如果有,就调用它,而动态注册的工作就是在这里完成的。所以,如果想使用动态注册方法,就必须要实现JNI_OnLoad函数,只有在这个函数中,才有机会完成动态注册的工作
静态注册
静态注册是根据函数名来找对应的JNI函数,它要求JNI层函数的名字必须遵循特定的格式, 函数名规则如下
Java_ + JNI方法所在的完整的类名,把类名里面的”.”替换成”_” + 真实的JNI方法名,这个方法名要和Java代码里面声明的JNI方法名一样+ JNI函数必须的默认参数(JNIEnv* env, jobjectthiz)
//native_init对应的JNI函数
//Java层函数名中如果有一个”_”的话,转换成JNI后就变成了”_l”。
JNIEXPORT void JNICALL Java_android_media_MediaCrypto_native_1init(JNIEnv* env, jclass thiz);
当Java层调用native_init函数时,它会从对应的JNI库查找Java_android_media_MediaCrypto_native_linit,如果没有,就会报错。如果找到,则会为这个native_init和Java_android_media_MediaCrypto_native_linit建立一个关联关系,其实就是保存JNI层函数的函数指针。以后再调用native_init函数时,直接使用这个函数指针就可以了,当然这项工作是由虚拟机完成的。
对应的jni头文件可以手写,不过比较麻烦, 可以使用 javah工具自动生成
- 先编写Java代码,然后编译生成.class文件。
- 使用Java的工具程序javah,如javah–o output packagename.classname ,这样它会生成一个叫output.h的JNI层头文件。其中packagename.classname是Java代码编译后的class文件,而在生成的output.h文件里,声明了对应的JNI层函数,只要实现里面的函数即可。头文件的名字一般都会使用packagename_class.h的样式,例如MediaCrypto对应的JNI层头文件就是android_media_MediaCrypto.h
弊端:
- 编译所有声明了native函数的Java类,每个生成的class文件都得用javah生成一个头文件。
- javah生成的JNI层函数名特别长,书写起来很不方便。
- 初次调用native函数时要根据函数名字搜索对应的JNI层函数来建立关联关系,这样会影响运行效率。
jdk10已经移除javah工具,相应的功能已经集成到javac中,你可以试试javac -h替代javah。
数据类型
在Java中调用native函数传递的参数是Java数据类型,这些参数类型到了JNI层会变成JNI对应的数据类型
Java数据类型分为基本数据类型和引用数据类型两种,JNI层也是区别对待这二者的。
基本数据类型转换关系表
引用类型对照表
除了Java中基本数据类型的数组、Class、String和Throwable外,其余所有Java对象的数据类型在JNI中都用jobject表示。
静态JNI方法和实例JNI方法参数区别
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_JniDemo_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_JniDemo_MainActivity_stringFromJNI2(JNIEnv *env, jclass clazz) {
// TODO: implement stringFromJNI2()
}
普通的JNI方法对应的JNI函数的第二个参数是jobject类型,而静态的JNI方法对应的JNI函数的第二个参数是jclass类型
JNIEnv的认识
概念
在JNI世界里离不开JNIEnv,JNIEnv是一个和线程相关的,代表JNI环境的结构体
JNIEnv提供了一些JNI系统函数,通过这些函数可以
- 调用Java函数【jni层调用java层】
- 操作jobject对象等很多事情【jni层调用native】
JNIEnv,是一个和线程有关的变量。线程A有一个JNIEnv,线程B有一个JNIEnv。由于线程相关,所以不能在线程B中使用线程A的JNIEnv结构体。当后台线程收到一个网络消息,需要由Native层函数主动回调Java层函数时,JNIEnv是从何而来呢?根据前面的介绍可知,我们不能保存另外一个线程的JNIEnv结构体,然后把它放到后台线程中来用。
前面提到过JNI_OnLoad函数,第一个参数是JavaVM,它是虚拟机在JNI层的代表。不论检查中多少个线程,JavaVM独此一份,在任意地方都可以使用它。
如果我们需要在其他线程访问JVM,那么必须先调用AttachCurrentThread将当前线程与JVM进行关联,然后才能获得JNIEnv对象。
JavaVMAttachArgs args = {JNI_VERSION_1_4, NULL, NULL};
JavaVM* vm = AndroidRuntime::getJavaVM();
int result = vm->AttachCurrentThread(&env, (void*) &args);
当然,我们在必要时需要调用DetachCurrentThread来解除链接。
JavaVM* vm = AndroidRuntime::getJavaVM();
int result = vm->DetachCurrentThread();
使用
通过JNIEnv操作jobject
Java的引用类型除了少数几个外,最终在JNI层都用jobject来表示对象的数据类型,操作jobject的本质是操作这些对象的成员变量和成员函数
JNI规则中,用jfieldID 和jmethodID 来表示Java类的成员变量和成员函数
通过JNIEnv的下面两个函数可以得到:
jfieldID GetFieldID(jclass clazz,const char*name, const char *sig);
jmethodID GetMethodID(jclass clazz, const char*name,const char *sig);
- jclass代表Java类(成员函数和成员变量都是类的信息)
- name表示成员函数或成员变量的名字
- sig为这个函数和变量的签名信息
static void android_media_MediaCrypto_native_init(JNIEnv *env) {
160 jclass clazz = env->FindClass("android/media/MediaCrypto");
161 CHECK(clazz != NULL);
162
163 gFields.context = env->GetFieldID(clazz, "mNativeContext", "J");
164 CHECK(gFields.context != NULL);
165 }
- 先找到android/media/MediaCrypto类在JNI层中对应的jclass实例
- 取出MediaCrypto类中变量mNativeContext的jfieldID
- 将变量mNativeContext的jfieldID 保存起来,因为每次操作jobject前都去查询jmethoID或jfieldID的话将会影响程序运行的效率
接下来就是使用
static sp<JCrypto> setCrypto(
142 JNIEnv *env, jobject thiz, const sp<JCrypto> &crypto) {
143 sp<JCrypto> old = (JCrypto *)env->GetLongField(thiz, gFields.context);
144 if (crypto != NULL) {
145 crypto->incStrong(thiz);
146 }
147 if (old != NULL) {
148 old->decStrong(thiz);
149 }
150 env->SetLongField(thiz, gFields.context, (jlong)crypto.get());
151
152 return old;
153 }
- 调用JNIEnv的GetLongField函数,第一个是代表MediaCrypto的jobject对象,第二个参数是mNativeContext的jfieldID
- 通过JNIEnv输出的GetLongField,再把jobject和jMethodID(如果对应参数)传进去,JNI层就能够调用Java对象的字段了!
实际上JNIEnv输出了一系列类似GetLongField的函数,形式如下:
//获得fieldID后,可调用Get<type>Field系列函数获取jobject对应成员变量的值。
NativeType Get<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID)
//或者调用Set<type>Field系列函数来设置jobject对应成员变量的值。
void Set<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)
//下面我们列出一些参加的Get/Set函数。
GetObjectField() SetObjectField()
GetBooleanField() SetBooleanField()
如果想调用Java中的static 字段,则用JNIEnv输出的GetStatic<Type>Field系列函数。
同理 通过JNIEnv操作jobject的成员函数和字段类似
形式如下:
NativeType Call<type>Method(JNIEnv *env,jobject obj,jmethodID methodID, ...)
调用Java中的static函数
JNIEnv输出的CallStatic<Type>Method系列函数
jstring
Java中的String也是引用类型,不过由于它的使用非常频繁,所以在JNI规范中单独创建了一个jstring类型来表示Java中的String类型。Java中的String包含很多成员函数,但是jstring是一种独立的数据类型,并没有提供成员函数供操作。
操作jstring得依靠JNIEnv提供的帮助。
可以把一个jstring对象看成是Java中String对象在JNI层的代表,也就是说,jstring就是一个Java String
- C/C++字符串转JNI字符串
NewString函数用来生成Unicode JNI字符串
NewStringUTF函数用来生成UTF-8 JNI字符串 - JNI字符串转C/C++字符串
GetStringChars函数用来从jstring获取Unicode C/C++字符串
GetStringUTFChars函数用来从jstring获取UTF-8 C/C++字符串 - 如果在代码中调用了上面几个函数,在做完相关工作后,就都需要调用
ReleaseStringChars或ReleaseStringUTFChars函数对应地释放资源,否则会导致JVM内存泄露。
JNI类型签名
动态注册中的数组信息
static const JNINativeMethod gMethods[] = {
307 { "release", "()V", (void *)android_media_MediaCrypto_release },
308 { "native_init", "()V", (void *)android_media_MediaCrypto_native_init },
309
310 { "native_setup", "([B[B)V",
311 (void *)android_media_MediaCrypto_native_setup },
312
313 { "native_finalize", "()V",
314 (void *)android_media_MediaCrypto_native_finalize },
315
316 { "isCryptoSchemeSupportedNative", "([B)Z",
317 (void *)android_media_MediaCrypto_isCryptoSchemeSupportedNative },
318
319 { "requiresSecureDecoderComponent", "(Ljava/lang/String;)Z",
320 (void *)android_media_MediaCrypto_requiresSecureDecoderComponent },
321
322 { "setMediaDrmSession", "([B)V",
323 (void *)android_media_MediaCrypto_setMediaDrmSession },
324 };
(void *)android_media_MediaCrypto_native_init 为函数native_init的签名信息,由参数类型和返回值类型共同组成
格式为:
(参数1类型标示参数2类型标示...参数n类型标示)返回值类型标示
- 括号内是参数类型的标示,最右边是返回值类型的标示,
- 当参数的类型是引用类型时,其格式是”L包名;”(标示最后有一个“;”),其中包名中的”.”换成”/”
- 如果Java类型是数组,则标示中会有一个“[”
因为Java支持函数重载,也就是说,可以定义同名但不同参数的函数。但仅仅根据函数名,是没法找到具体函数的。为了解决这个问题,JNI技术中就使用了参数类型和返回值类型的组合,作为一个函数的签名信息,有了签名信息和函数名,就能很顺利地找到Java中的函数了。
类型标示示意表
签名信息可以通过以下几种方式获取:
- 对照java函数手写签名,不过容易写错
- 通过AndroidStudio 新建C++工程 自动生成
- 通过javap工具,javap –s -p xxx,其中xxx为编译后的class文件,s表示输出内部数据类型的签名信息,p表示打印所有函数和成员的签名信息,而默认只会打印public成员和函数的签名信息。
垃圾回收
- Java中创建的对象最后是由垃圾回收器来回收和释放内存的(引用计数和可达性)
- JNI层使用save_thiz = thiz 这样的语句,是不会增加引用计数的
从上面2条结论可得知,当jni层通过 赋值 “=” 保存Java层传入的jobject对象,在某个对象调用时,java层可能已经释放对象
但是JNI规范已很好地解决了这一问题,JNI技术一共提供了三种类型的引用,它们分别是:
Local Reference:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference。它包括函数调用时传入的jobject、在JNI层函数中创建的jobject。LocalReference最大的特点就是,一旦JNI层函数返回,这些jobject就可能被垃圾回收。
如果不调用DeleteLocalRef,pathStr将在函数返回后被回收;如果调用DeleteLocalRef的话,pathStr会立即被回收。Global Reference:(env->NewGlobalRef(client)) )全局引用,这种对象如不主动释放,就永远不会被垃圾回收,调用DeleteGlobalRef释放这个全局引用。
Weak Global Reference:弱全局引用,一种特殊的GlobalReference,在运行过程中可能会被垃圾回收。所以在程序中使用它之前,需要调用JNIEnv的IsSameObject判断它是不是被回收了。
JNI常用函数
参考 https://blog.csdn.net/qinjuning/article/details/7595104
异常处理
当JNI函数调用的Java方法出现异常的时候,并不会影响JNI方法的执行,但是我们并不推荐JNI函数忽略Java方法出现的异常继续执行,这样可能会带来更多的问题。我们推荐的方法是,当JNI函数调用的Java方法出现异常的时候,JNI函数应该合理的停止执行代码。
- ExceptionOccurred函数用来判断JNI函数调用的Java方法是否出现异常
-
ExceptionClear函数用来清除JNI函数调用的Java方法出现的异常
Java代码:
JNI代码:
JNI通过ThrowNew函数抛出Java类型的异常