前言
长文预警,本文是JNI开发的基础知识介绍和使用经验总结,基本上涵盖了Android JNI开发的大多数知识点,因此文章较长。
1. NDK介绍
1.1 NDK简介
NDK,即原生开发套件 (Native Development Kit),它是一套工具集,让我们能够在 Android 应用中使用 C/C++代码。Android Studio首先使用NDK 将 C/C++ 代码编译成原生库,然后使用集成构建系统 Gradle 将原生库打包到 APK 中。Java 代码随后可以通过JNI(Java原生接口)框架调用原生库中的C/C++函数。
NDK主要包括了:
- 从C / C++生成原生代码库所需要的工具和构建文件。
- 支持Android平台的一系列原生系统(C/C++、JNI)的头文件和库。
1.2 NDK的使用场景
我们知道,C/C++是比java更底层的语言,C/C++的执行效率更高、延迟更小,因此在某些情况下我们可能会使用NDK,通过Java代码来调用C/C++代码,完成一些工作。例如:
- 进一步提升设备性能,以降低延迟,或运行计算密集型应用,如游戏或物理模拟。
- 重复使用已有的 C 或 C++ 库。
- 图像、音频、视频处理。
1.3 下载 NDK 和工具
要进行NDK开发,我们需要下载以下组件:
- Android 原生开发套件 (NDK):编译C/C++
- CMake:C/C++构建工具,类似Makefile,比Makefile使用更加方便。可与 Gradle 搭配使用来构建原生库。
- LLDB:Android Studio 用于调试原生代码的调试程序。
我们可以直接在SDKManager中下载这三个组件。
2. JNI简介
JNI (Java Native Interface),即java本地接口,它是为java语言和其它语言交互调用而设计的标准应用程序接口。JNI允许JVM中运行的Java代码与用其他编程语言(例如C,C ++和汇编)编写的应用程序和库进行交互操作。
实际使用中,JNI的一个重要的用途是让java通过JNI调用C/C++函数。C/C++函数编译后就存放在库文件中,如大家所知,库文件在Windows平台是DLL文件,在UNIX/Linux平台上是SO文件。而库文件和本地环境是强依赖的,是不具备跨平台特性的。因此,想要实现跨平台,就必须提供在所有的目标平台能运行的库文件。例如实现常见的android平台,一般需要提供这些版本的库文件:arm64-v8a, armeabi, armeabi-v7a, mips, mips64, x86, x86_64。
3. JNI版Hello world
和学习所有编程语言一样,我们首先来写一个JNI版的Hello world。
3.1 手动编译和使用JNI
虽然很繁琐,但是为了完整的了解JNI的开发流程,我们还是要先来说说手动编译和使用JNI的一般流程:
- a. 编写Test.java,添加native方法
package com.qxt;
public class Test {
public static native String sayHello();
}
- b. 使用javac命令将Test.java编译生成Test.class:
javac Test.java
- c. 使用javah命令将Test.class编译生成com_qxt_Test.h头文件。注意要指定正确的classpath。
javah com.qxt.Test
生成的com_qxt_Test.h头文件:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_qxt_Test */
#ifndef _Included_com_qxt_Test
#define _Included_com_qxt_Test
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_qxt_Test
* Method: sayHello
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_qxt_Test_sayHello
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
-
d. 编写com_qxt_Test.cpp实现头文件,并实现具体的函数。
com_qxt_Test.cpp文件:
#include <jni.h>
extern "C" JNIEXPORT jstring JNICALL
Java_com_qxt_Test_sayHello(
JNIEnv* env,
jclass clazz) {
return env->NewStringUTF("Hello world from JNI");
}
- e. 使用Makefile或者Cmake构建和使用NDK编译com_qxt_Test.cpp或者com_qxt_Test.c源文件,生成libtest.so库文件
Application.mk:
APP_ABI := armeabi-v7a arm64-v8a
Android.mk:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := test
LOCAL_C_INCLUDES := ./
LOCAL_SRC_FILES := ./com_qxt_Test.cpp
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
NDK编译命令:
ndk-build NDK_PROJECT_PATH=. NDK_APPLICATION_MK=Application.mk APP_BUILD_SCRIPT=Android.mk
- f. 在Test.java中使用静态代码块加载库文件:
static {
System.loadLibrary("test");
}
可以看到手动编译和使用JNI的流程是比较麻烦的,不禁让人想起大学时代初学java,老师让大家用记事本手撕java项目的恐惧。不过幸运的是google的Android Studio已经可以帮我们完成这些工作。接下来将介绍使用Android Studio开发JNI。
3.2 使用Android Studio开发JNI
我们只要在Android Studio中选择 File > New Project,在Select a Project Template界面选择Native C++,一路Next往下,一个JNI项目就创建好了,如图:
创建好的项目如上图所示。我们无需再生成头文件以及手动写Makefile了,Android Studio已经帮我们完成了这一切,并且新版的Android Studio已经用使用上更加方便的cmake替换了Makefile了。开发时我们只要编辑java文件和cpp文件、配置好CMakeLists.txt、在build.gradle中指定CMakeLists.txt就可以了。在项目编译时,Android Studio会自动编译cpp文件,并将生成的库文件打包到apk或者aab文件当中。我们来看看主要文件的代码:
MainActivity.java:
package com.qxt.test;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends Activity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = (TextView) findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}
native-lib.cpp
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_qxt_myapp_MainActivity_stringFromJNI(
JNIEnv* env,
jobject ) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
CMakeLists.txt:(为了看起来比较清晰,已删除了无效的注释)
cmake_minimum_required(VERSION 3.4.1)
add_library( native-lib
SHARED
native-lib.cpp )
find_library( log-lib
log )
target_link_libraries( native-lib
${log-lib} )
刚刚我们已经创建了一个简单的JNI项目。
我们注意一下,在native-lib.cpp中本地函数stringFromJNI的参数中有个JNIEnv* env。JNIEnv,顾名思义就是JNI环境变量或者叫JNI接口指针。JNIEnv非常重要,JNI中所有的接口函数都是通过JNIEnv来调用的。
在native-lib.cpp中我们还导入了一个叫jni.h头文件,这个头文件也非常重要。jni.h中定义了JNI的数据类型、数据结构、接口函数、回调函数以及常量等等。在接下来的章节中会逐一介绍相关的重要内容,并举例说明。在最后还会简单介绍cmake的使用。
另外,由于native、method、function等单词翻译的问题,JNI和Java的概念经常容易搞混,因此我们在接下来的章节中,有如下命名约定:
- native方法指代Java层的native方法。
- 本地函数指代JNI层的C/C++函数。
4. JNI的数据类型和数据结构
4.1 基本类型
JNI包括了许多与Java基本类型相对应的基本类型。具体如下表:
4.2 引用类型
JNI包括了许多与Java引用类型相对应的引用类型。具体如下表:
4.3 字段和方法ID
除了Java中常用的基本类型和引用类型,JNI还定义了jfieldID和jmethodID。
jfieldID和jmethodID是常规的C指针类型,它们的声明如下:
struct _jfieldID; /* opaque structure */
typedef struct _jfieldID *jfieldID; /* field IDs */
struct _jmethodID; /* opaque structure */
typedef struct _jmethodID *jmethodID; /* method IDs */
在JNI中调用java对象的变量或者方法时常常会用到jfieldID和jmethodID。
4.4 jvalue类型
除了以上介绍的数据类型,JNI还定义了jvalue联合类型,jvalue被用作自变量数组的元素类型。jvalue的声明如下:
typedef union jvalue {
jboolean z;
jbyte b;
jchar c;
jshort s;
jint i;
jlong j;
jfloat f;
jdouble d;
jobject l;
} jvalue;
jvalue在本地函数调用java方法时,经常会被用做参数类型。
4.5 类型签名
JNI同样使用了JVM的类型签名表示。具体如下表:
例如,对于Java方法:
long f (int n, String s, int[] arr);
它的类型签名为:
(ILjava/lang/String;[I)J
使用javap命令查看类型签名
在接下来的章节中会介绍JNI函数的动态注册,在动态注册时,就需要用到类型签名,而类型签名可以通过javap命令来查看,例如,查看Test.java的类型签名,先将Test.java编译成Test.class。然后:
javap -s Test.class
5. JNI的引用
在了解完JNI的数据类型之后,我们接下来继续说说JNI的引用。JNI的引用分为四种:
- 全局引用(GlobalReferences),全局引用全局有效,JVM无法释放和回收全局引用,全局引用必须通过调用DeleteGlobalRef()显式释放。
分配全局引用:
jobject NewGlobalRef(JNIEnv *env, jobject obj);
释放全局引用:
void DeleteGlobalRef(JNIEnv *env, jobject globalRef);
- 弱全局引用(WeakGlobalReferences),弱全局引用是一种特殊的全局引用,与全局引用不同,JVM可以对它进行垃圾回收。弱全局引用可以在使用全局或局部引用的任何情况下使用。当进行垃圾回收时,如果一个对象仅被弱全局引用所引用,则它将被释放。指向该对象的弱全局引用将指向NULL。因此,使用弱全局引用前需要进行非空判断。我们还可以通过IsSameObject将弱引用与NULL进行比较,来检测弱全局引用是否指向NULL。
分配弱全局引用:
jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);
释放弱全局引用:
void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);
- 局部引用(LocalReferences),局部引用在本地方法调用期间有效,局部引用在本地方法返回后自动释放。每个局部引用都要消耗一定数量的JVM资源。因此,使用时需要确保本地方法不会分配过多的局部引用。尽管在本地方法返回Java之后会自动释放局部引用,但是分配过多的局部引用可能会导致JVM在执行本机方法期间耗尽内存(OOM)。
分配局部引用:
jobject NewLocalRef(JNIEnv *env, jobject ref);
释放局部引用:
void DeleteLocalRef(JNIEnv *env, jobject localRef);
查询局部引用容量:
jint EnsureLocalCapacity(JNIEnv *env, jint capacity);
- 无效引用(InvalidReferences),无效引用一般情况下没有什么用,不展开介绍。
总的来说,JNI的引用并不复杂,但使用时我们仍需要保持良好的编程习惯:
a. 一个引用不管能不能被JVM释放和回收,不再使用后立即显示释放。
b. 在不需要额外引用的情况下,绝不分配新的引用。
6. JNI使用类和对象
6.1 类
DefineClass
/*
* @param env: JNI接口指针.
* @param name: 要定义的类或接口的名称。该字符串以修改后的UTF-8编码。
* @param loader: 分配给已定义类的类加载器。
* @param buf: 包含.class文件数据的缓冲区。
* @param bufLen: 缓冲区长度。
* @return 返回Java类对象,如果发生错误,则返回NULL。
*/
jclass DefineClass(JNIEnv *env, const char *name, jobject loader, const jbyte *buf, jsize bufLen);
FindClass
/*
* @param env: JNI接口指针。
* @param name: 完全限定的类名(即,包名,以“ /” 分隔,后跟类名,例如java.lang.String:“java/lang/String”)。
* 如果名称以“ [”(数组签名字符)开头,则返回数组类。该字符串以修改后的UTF-8编码。
* @return 返回完全限定的名称的类的对象,如果找不到该类,则返回NULL。
*/
jclass FindClass(JNIEnv *env, const char *name);
GetSuperclass
/*
* @param env: JNI接口指针。
* @param clazz: 一个Java类对象。
* @return 返回以clazz所属类的超类或者NULL。
*/
jclass GetSuperclass(JNIEnv *env, jclass clazz);
IsAssignableFrom
/*
* @param env: JNI接口指针。
* @param clazz1: 第一个类参数。
* @param clazz2: 第二个类参数。
* @return 如果以下任一条件为真,则返回JNI_TRUE:
* 第一个类参数和第二个类参数引用相同的Java类。
* 第一个类参数是第二个类参数的子类。
* 第二个类参数为接口,第一个类参数实现了该接口。
*/
jboolean IsAssignableFrom(JNIEnv *env, jclass clazz1, jclass clazz2);
类的使用实例将在后面的小节中和对象一起介绍。
6.2 对象
AllocObject
/*
* 分配新的Java对象,而无需调用该对象的任何构造函数。返回对该对象的引用。clazz参数不能为任何数组类。
* @param env: JNI接口指针。
* @param clazz: 一个Java类对象。
* @return 返回Java对象,无法构造该对象则返回NULL。
*/
jobject AllocObject(JNIEnv *env, jclass clazz);
NewObject
/*
* 构造一个新的Java对象。methodID指定要调用的构造方法。必须通过GetMethodID()获取构造方法的methodID。
* GetMethodID获取构造方法的methodID时方法名为<init>,返回类型为void(V)。clazz参数不能引用数组类。
* @param env: JNI接口指针。
* @param clazz: 一个Java类对象。
* @param methodID:构造函数的methodID。
* @param ...:构造函数的参数。
* @param args:构造函数的参数数组。
* @param args:构造函数参数的va_list。
* @return 返回Java对象,无法构造该对象则返回NULL。
*/
jobject NewObject(JNIEnv *env, jclass clazz, jmethodID methodID, ...);
jobject NewObjectA(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args);
jobject NewObjectV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);
GetObjectClass
/*
* 获取对象所属的类
* @param env: JNI接口指针。
* @param obj:一个Java对象(必须不是NULL)。
* @return 返回一个Java类对象。
*/
jclass GetObjectClass(JNIEnv *env, jobject obj);
GetObjectRefType
/*
* 获取obj的引用类型 。该参数obj可以是局部引用,全局引用或弱全局引用。
* @param env: JNI接口指针。
* @param obj: 局部引用、全局引用或者弱全局引用。
* @return 返回以下枚举值之一:
* 如果obj不是有效的引用,则返回JNIInvalidRefType = 0。
* 如果obj是局部引用类型,则返回JNILocalRefType = 1。
* 如果obj是全局引用类型,则返回JNIGlobalRefType = 2。
* 如果obj是弱全局引用类型,则返回JNIWeakGlobalRefType = 3。
*/
jobjectRefType GetObjectRefType(JNIEnv* env, jobject obj);
IsInstanceOf
/*
* 判断对象是否是类的实例。
* @param env: JNI接口指针。
* @param obj:一个Java对象
* @return obj可以强制转换为clazz返回JNI_TRUE; 否则返回JNI_FALSE。一个NULL对象可以强制转换为任何类。
*/
jboolean IsInstanceOf(JNIEnv *env, jobject obj, jclass clazz);
IsSameObject
/*
* 测试两个引用是否引用相同的Java对象。
* @param env: JNI接口指针。
* @param ref1:一个Java对象。
* @param ref2:一个Java对象。
* @return 如果ref1和ref2引用相同的Java对象,或者两者均为NULL,返回JNI_TRUE; 否则返回JNI_FALSE。
*/
jboolean IsSameObject(JNIEnv *env, jobject ref1, jobject ref2);
6.3 类和对象的应用实例
调用类的构造方法创建一个对象。
MainActivity.java:
public native Object testClass(int value);
native-lib.cpp
extern "C" JNIEXPORT jobject JNICALL Java_com_qxt_myapp_MainActivity_testClass(JNIEnv *env, jobject thiz, jint value) {
jclass clazz = env->FindClass("java/lang/Integer");
if (clazz != nullptr) {
jmethodID integerConstructID = env->GetMethodID(clazz, "<init>", "(I)V");
return env->NewObject(clazz, integerConstructID, value);
}
return NULL;
}
6.4 调用实例字段和方法
6.4.1 调用实例字段
GetFieldID
/*
* 返回类的实例(非静态)字段的字段ID。该字段由其名称和签名指定。
* Get<type>Field和Set<type>Field系列函数使用字段ID检索对象字段。
* GetFieldID() 将使未初始化的类被初始化。GetFieldID()无法用于获取数组的长度,获取数组长度请使用GetArrayLength()代替。
*
* @param env: JNI接口指针。
* @param clazz:一个Java类对象。
* @param name:字段名称,以\0结尾的UTF-8字符串。
* @param sig:字段签名,以\0结尾的UTF-8字符串。
* @return 返回字段ID,如果操作失败返回NULL。
*/
jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
Get<type>Field
NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID);
jobject GetObjectField(JNIEnv*, jobject, jfieldID);
jboolean GetBooleanField(JNIEnv*, jobject, jfieldID);
jbyte GetByteField(JNIEnv*, jobject, jfieldID);
jchar GetCharField(JNIEnv*, jobject, jfieldID);
jshort GetShortField(JNIEnv*, jobject, jfieldID);
jint GetIntField(JNIEnv*, jobject, jfieldID);
jlong GetLongField(JNIEnv*, jobject, jfieldID);
jfloat GetFloatField(JNIEnv*, jobject, jfieldID);
jdouble GetDoubleField(JNIEnv*, jobject, jfieldID);
Set<type>Field
void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value);
void SetObjectField(JNIEnv*, jobject, jfieldID, jobject);
void SetBooleanField(JNIEnv*, jobject, jfieldID, jboolean);
void SetByteField(JNIEnv*, jobject, jfieldID, jbyte);
void SetCharField(JNIEnv*, jobject, jfieldID, jchar);
void SetShortField(JNIEnv*, jobject, jfieldID, jshort);
void SetIntField(JNIEnv*, jobject, jfieldID, jint);
void SetLongField(JNIEnv*, jobject, jfieldID, jlong);
void SetFloatField(JNIEnv*, jobject, jfieldID, jfloat);
void SetDoubleField(JNIEnv*, jobject, jfieldID, jdouble);
6.4.2 调用实例方法
GetMethodID
/*
* 返回类或接口的实例(非静态)方法的方法ID。该方法可以在clazz的超类之一中定义,并由继承clazz。该方法由其名称和签名确定。
* 调用 GetMethodID() 将使未初始化的类被初始化。
* 要获取构造函数的方法ID,请提供 <init>作为方法名称,并提供 void(V)作为返回类型。
*
* @param env: JNI接口指针。
* @param clazz:一个Java类对象。
* @param name:方法名称,以\0结尾的UTF-8字符串。
* @param sig:方法签名,以\0结尾的UTF-8字符串。
* @return 返回方法ID,如果操作失败返回NULL。
*/
jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
Call<type>Method
NativeType Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...);
NativeType Call<type>MethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
NativeType Call<type>MethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);
这三个操作族中的方法用于从本地函数中调用Java实例方法,它们的区别仅仅是传参机制不同。
/*
* 调用实例方法
* GetMethodID获取构造方法的methodID时方法名为<init>,返回类型为void(V)。clazz参数不能引用数组类。
* @param env: JNI接口指针。
* @param jobject: 一个Java对象。
* @param methodID:java函数的methodID, 必须通过调用GetMethodID()来获得。
* @param ...:java函数的参数。
* @param args:java函数的参数数组。
* @param args:java函数参数的va_list。
* @return 返回Java对象,无法构造该对象则返回NULL。
*/
jobject CallObjectMethod(JNIEnv*, jobject, jmethodID, ...);
jobject CallObjectMethodV(JNIEnv*, jobject, jmethodID, va_list);
jobject CallObjectMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
jboolean CallBooleanMethod(JNIEnv*, jobject, jmethodID, ...);
jboolean CallBooleanMethodV(JNIEnv*, jobject, jmethodID, va_list);
jboolean CallBooleanMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
jbyte CallByteMethod(JNIEnv*, jobject, jmethodID, ...);
jbyte CallByteMethodV(JNIEnv*, jobject, jmethodID, va_list);
jbyte CallByteMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
jchar CallCharMethod(JNIEnv*, jobject, jmethodID, ...);
jchar CallCharMethodV(JNIEnv*, jobject, jmethodID, va_list);
jchar CallCharMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
jshort CallShortMethod(JNIEnv*, jobject, jmethodID, ...);
jshort CallShortMethodV(JNIEnv*, jobject, jmethodID, va_list);
jshort CallShortMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
jint CallIntMethod(JNIEnv*, jobject, jmethodID, ...);
jint CallIntMethodV(JNIEnv*, jobject, jmethodID, va_list);
jint CallIntMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
jlong CallLongMethod(JNIEnv*, jobject, jmethodID, ...);
jlong CallLongMethodV(JNIEnv*, jobject, jmethodID, va_list);
jlong CallLongMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
jfloat CallFloatMethod(JNIEnv*, jobject, jmethodID, ...);
jfloat CallFloatMethodV(JNIEnv*, jobject, jmethodID, va_list);
jfloat CallFloatMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
jdouble CallDoubleMethod(JNIEnv*, jobject, jmethodID, ...);
jdouble CallDoubleMethodV(JNIEnv*, jobject, jmethodID, va_list);
jdouble CallDoubleMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
void CallVoidMethod(JNIEnv*, jobject, jmethodID, ...);
void CallVoidMethodV(JNIEnv*, jobject, jmethodID, va_list);
void CallVoidMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
CallNonvirtual<type>Method
NativeType CallNonvirtual<type>Method(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, ...);
NativeType CallNonvirtual<type>MethodA(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue *args);
NativeType CallNonvirtual<type>MethodV(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args);
这三个操作族中的方法用于从本地函数中调用Java实例方法,它们的区别仅仅是传参机制不同。
CallNonvirtual<type>Method族方法和Call<type Method族方法的区别在于:
Call<type>Method基于对象的类来调用方法,而CallNonvirtual<type>Method基于由clazz参数指定的类来调用方法,并从中获取方法ID。方法ID必须从对象的真实类或其超类之一获得。
jobject CallNonvirtualObjectMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
jobject CallNonvirtualObjectMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
jobject CallNonvirtualObjectMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
jboolean CallNonvirtualBooleanMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
jboolean CallNonvirtualBooleanMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
jboolean CallNonvirtualBooleanMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
jbyte CallNonvirtualByteMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
jbyte CallNonvirtualByteMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
jbyte CallNonvirtualByteMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
jchar CallNonvirtualCharMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
jchar CallNonvirtualCharMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
jchar CallNonvirtualCharMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
jshort CallNonvirtualShortMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
jshort CallNonvirtualShortMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
jshort CallNonvirtualShortMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
jint CallNonvirtualIntMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
jint CallNonvirtualIntMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
jint CallNonvirtualIntMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
jlong CallNonvirtualLongMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
jlong CallNonvirtualLongMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
jlong CallNonvirtualLongMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
jfloat CallNonvirtualFloatMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
jfloat CallNonvirtualFloatMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
jfloat CallNonvirtualFloatMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
jdouble CallNonvirtualDoubleMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
jdouble CallNonvirtualDoubleMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
jdouble CallNonvirtualDoubleMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
void CallNonvirtualVoidMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
void CallNonvirtualVoidMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
void CallNonvirtualVoidMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
6.4.3 对象的字段和方法使用实例
MainActivity.java:
public int age = 20;
public String getAge(String name) {
return "Hello " + name + ", I'm java method getAge";
}
public native void testObject();
native-lib.cpp:
extern "C" JNIEXPORT void JNICALL Java_com_qxt_myapp_MainActivity_testObject(JNIEnv *env, jobject thiz) {
jclass clazz = env->FindClass("com/qxt/myapp/MainActivity");
if (clazz != nullptr) {
//Access object field
jfieldID ageID = env->GetFieldID(clazz, "age", "I");
jint ageInt = (jint) env->GetIntField(thiz, ageID);
//Access object method
jmethodID getAgeID = env->GetMethodID(clazz, "getAge", "(Ljava/lang/String;)Ljava/lang/String;");
jstring nameStr = env->NewStringUTF("JNI");
jstring msgStr = (jstring) env->CallObjectMethod(thiz, getAgeID, nameStr);
//Use string, convert jstring to char sequence
char *name = (char *) env->GetStringUTFChars(nameStr, NULL);
char *msg = (char *) env->GetStringUTFChars(msgStr, NULL);
LOGD("[testObject] message:%s; age:%d", msg, ageInt);
env->ReleaseStringUTFChars(nameStr, name);
env->ReleaseStringUTFChars(msgStr, msg);
}
env->DeleteLocalRef(clazz);
}
6.5 调用静态的字段和方法
6.5.1 调用静态字段
GetStaticFieldID
/*
* 返回类的静态字段的字段ID。该字段由其名称和签名指定。
* GetStatic<type>Field和SetStatic<type>Field系列函数使用字段ID检索静态字段。
* GetFieldID() 将使未初始化的类被初始化。
*
* @param env: JNI接口指针。
* @param clazz:一个Java类对象。
* @param name:字段名称,以\0结尾的UTF-8字符串。
* @param sig:字段签名,以\0结尾的UTF-8字符串。
* @return 返回字段ID,如果操作失败返回NULL。
*/
jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
GetStatic<type>Field
NativeType GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID);
jobject GetStaticObjectField(JNIEnv*, jclass, jfieldID);
jboolean GetStaticBooleanField(JNIEnv*, jclass, jfieldID);
jbyte GetStaticByteField(JNIEnv*, jclass, jfieldID);
jchar GetStaticCharField(JNIEnv*, jclass, jfieldID);
jshort GetStaticShortField(JNIEnv*, jclass, jfieldID);
jint GetStaticIntField(JNIEnv*, jclass, jfieldID);
jlong GetStaticLongField(JNIEnv*, jclass, jfieldID);
jfloat GetStaticFloatField(JNIEnv*, jclass, jfieldID);
jdouble GetStaticDoubleField(JNIEnv*, jclass, jfieldID);
SetStatic<type>Field
void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, NativeType value);
void SetStaticObjectField(JNIEnv*, jclass, jfieldID, jobject);
void SetStaticBooleanField(JNIEnv*, jclass, jfieldID, jboolean);
void SetStaticByteField(JNIEnv*, jclass, jfieldID, jbyte);
void SetStaticCharField(JNIEnv*, jclass, jfieldID, jchar);
void SetStaticShortField(JNIEnv*, jclass, jfieldID, jshort);
void SetStaticIntField(JNIEnv*, jclass, jfieldID, jint);
void SetStaticLongField(JNIEnv*, jclass, jfieldID, jlong);
void SetStaticFloatField(JNIEnv*, jclass, jfieldID, jfloat);
void SetStaticDoubleField(JNIEnv*, jclass, jfieldID, jdouble);
6.5.2 调用静态方法
GetStaticMethodID
/*
* 返回类或接口的静态方法的方法ID。该方法由其名称和签名确定。
* 调用 GetStaticMethodID() 将使未初始化的类被初始化。
* 要获取构造函数的方法ID,请提供 <init>作为方法名称,并提供 void(V)作为返回类型。
*
* @param env: JNI接口指针。
* @param clazz:一个Java类对象。
* @param name:方法名称,以\0结尾的UTF-8字符串。
* @param sig:方法签名,以\0结尾的UTF-8字符串。
* @return 返回方法ID,如果操作失败返回NULL。
*/
jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
CallStatic<type>Method
NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID, ...);
NativeType CallStatic<type>MethodA(JNIEnv *env, jclass clazz, jmethodID methodID, jvalue *args);
NativeType CallStatic<type>MethodV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);
jobject CallStaticObjectMethod(JNIEnv*, jclass, jmethodID, ...);
jobject CallStaticObjectMethodV(JNIEnv*, jclass, jmethodID, va_list);
jobject CallStaticObjectMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
jboolean CallStaticBooleanMethod(JNIEnv*, jclass, jmethodID, ...);
jboolean CallStaticBooleanMethodV(JNIEnv*, jclass, jmethodID, va_list);
jboolean CallStaticBooleanMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
jbyte CallStaticByteMethod(JNIEnv*, jclass, jmethodID, ...);
jbyte CallStaticByteMethodV(JNIEnv*, jclass, jmethodID, va_list);
jbyte CallStaticByteMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
jchar CallStaticCharMethod(JNIEnv*, jclass, jmethodID, ...);
jchar CallStaticCharMethodV(JNIEnv*, jclass, jmethodID, va_list);
jchar CallStaticCharMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
jshort CallStaticShortMethod(JNIEnv*, jclass, jmethodID, ...);
jshort CallStaticShortMethodV(JNIEnv*, jclass, jmethodID, va_list);
jshort CallStaticShortMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
jint CallStaticIntMethod(JNIEnv*, jclass, jmethodID, ...);
jint CallStaticIntMethodV(JNIEnv*, jclass, jmethodID, va_list);
jint CallStaticIntMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
jlong CallStaticLongMethod(JNIEnv*, jclass, jmethodID, ...);
jlong CallStaticLongMethodV(JNIEnv*, jclass, jmethodID, va_list);
jlong CallStaticLongMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
jfloat CallStaticFloatMethod(JNIEnv*, jclass, jmethodID, ...);
jfloat CallStaticFloatMethodV(JNIEnv*, jclass, jmethodID, va_list);
jfloat CallStaticFloatMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
jdouble CallStaticDoubleMethod(JNIEnv*, jclass, jmethodID, ...);
jdouble CallStaticDoubleMethodV(JNIEnv*, jclass, jmethodID, va_list);
jdouble CallStaticDoubleMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
void CallStaticVoidMethod(JNIEnv*, jclass, jmethodID, ...);
void CallStaticVoidMethodV(JNIEnv*, jclass, jmethodID, va_list);
void CallStaticVoidMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
6.5.3 静态的字段和方法使用实例
MainActivity.java:
public static String LOG_TAG = "MainActivity";
public static String getLogTag(String name) {
return "Hello " + name + ", I'm java static method getLogTag";
}
public native void testStatic();
native-lib.cpp:
extern "C" JNIEXPORT void JNICALL Java_com_qxt_myapp_MainActivity_testStatic(JNIEnv *env, jobject thiz) {
jclass clazz = env->FindClass("com/qxt/myapp/MainActivity");
if (clazz != nullptr) {
//Access static field
jfieldID logTagID = env->GetStaticFieldID(clazz, "LOG_TAG", "Ljava/lang/String;");
jstring logTagStr = (jstring) env->GetStaticObjectField(clazz, logTagID);
//Access static method
jmethodID getLogTagID = env->GetStaticMethodID(clazz, "getLogTag", "(Ljava/lang/String;)Ljava/lang/String;");
jstring nameStr = env->NewStringUTF("JNI");
jstring msgStr = (jstring) env->CallStaticObjectMethod(clazz, getLogTagID, nameStr);
//Use string, convert jstring to char sequence
char *logTag = (char *) env->GetStringUTFChars(logTagStr, NULL);
char *name = (char *) env->GetStringUTFChars(nameStr, NULL);
char *msg = (char *) env->GetStringUTFChars(msgStr, NULL);
LOGD("[testStatic] message:%s; logTag:%s", msg, logTag);
env->ReleaseStringUTFChars(logTagStr, logTag);
env->ReleaseStringUTFChars(nameStr, name);
env->ReleaseStringUTFChars(msgStr, msg);
}
env->DeleteLocalRef(clazz);
}
7. JNI的字符串
7.1 API介绍
NewString
/*
* 创建unicode字符串
*/
jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize len);
GetStringLength
/*
* 获取字符串长度
*/
jsize GetStringLength(JNIEnv *env, jstring string);
GetStringChars
/*
* 将字符串转换成字符数组
*/
const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy);
ReleaseStringChars
/*
* 释放字符串
*/
void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars);
GetStringChars和ReleaseStringChars通常成对使用。
GetStringRegion
/*
* 从字符串中的指定位置复制指定长度的字符到字符数组中
*/
void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize len, jchar *buf);
NewStringUTF
/*
* 创建UTF-8字符串
*/
jstring NewStringUTF(JNIEnv *env, const char *bytes);
GetStringUTFLength
/*
* 获取UTF-8字符串长度
*/
jsize GetStringUTFLength(JNIEnv *env, jstring string);
GetStringUTFChars
/*
* 将UTF-8字符串转换成字符数组
*/
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
ReleaseStringUTFChars
/*
* 释放UTF-8字符串
*/
void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);
GetStringUTFChars和ReleaseStringUTFChars通常成对使用。
GetStringUTFRegion
/*
* 从UTF-8字符串中的指定位置复制指定长度的字符到字符数组中
*/
void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize len, char *buf);
7.2 字符串的使用实例
MainActivity.java:
public native String testString(String s);
native-lib.cpp:
extern "C" JNIEXPORT jstring JNICALL Java_com_qxt_myapp_MainActivity_testString(JNIEnv *env, jobject thiz, jstring s) {
//Get java string
char *msg = (char *) env->GetStringUTFChars(s, NULL);
std::string hello = msg;
hello.append("\n");
hello.append("Hello java");
env->ReleaseStringUTFChars(s, msg);
//New java string
return env->NewStringUTF(hello.c_str());
}
8. JNI的数组
8.1 基本类型数组
New<PrimitiveType>Array
ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);
jbooleanArray NewBooleanArray(JNIEnv*, jsize);
jbyteArray NewByteArray(JNIEnv*, jsize);
jcharArray NewCharArray(JNIEnv*, jsize);
jshortArray NewShortArray(JNIEnv*, jsize);
jintArray NewIntArray(JNIEnv*, jsize);
jlongArray NewLongArray(JNIEnv*, jsize);
jfloatArray NewFloatArray(JNIEnv*, jsize);
jdoubleArray NewDoubleArray(JNIEnv*, jsize);
Get<PrimitiveType>ArrayElements
NativeType *Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, jboolean *isCopy);
jboolean* GetBooleanArrayElements(JNIEnv*, jbooleanArray, jboolean*);
jbyte* GetByteArrayElements(JNIEnv*, jbyteArray, jboolean*);
jchar* GetCharArrayElements(JNIEnv*, jcharArray, jboolean*);
jshort* GetShortArrayElements(JNIEnv*, jshortArray, jboolean*);
jint* GetIntArrayElements(JNIEnv*, jintArray, jboolean*);
jlong* GetLongArrayElements(JNIEnv*, jlongArray, jboolean*);
jfloat* GetFloatArrayElements(JNIEnv*, jfloatArray, jboolean*);
jdouble* GetDoubleArrayElements(JNIEnv*, jdoubleArray, jboolean*);
Release<PrimitiveType>ArrayElements
void Release<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, NativeType *elems, jint mode);
void ReleaseBooleanArrayElements(JNIEnv*, jbooleanArray, jboolean*, jint);
void ReleaseByteArrayElements(JNIEnv*, jbyteArray, jbyte*, jint);
void ReleaseCharArrayElements(JNIEnv*, jcharArray, jchar*, jint);
void ReleaseShortArrayElements(JNIEnv*, jshortArray, jshort*, jint);
void ReleaseIntArrayElements(JNIEnv*, jintArray, jint*, jint);
void ReleaseLongArrayElements(JNIEnv*, jlongArray, jlong*, jint);
void ReleaseFloatArrayElements(JNIEnv*, jfloatArray, jfloat*, jint);
void ReleaseDoubleArrayElements(JNIEnv*, jdoubleArray, jdouble*, jint);
Get<PrimitiveType>ArrayRegion
void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, NativeType *buf);
void GetBooleanArrayRegion(JNIEnv*, jbooleanArray, jsize, jsize, jboolean*);
void GetByteArrayRegion(JNIEnv*, jbyteArray, jsize, jsize, jbyte*);
void GetCharArrayRegion(JNIEnv*, jcharArray, jsize, jsize, jchar*);
void GetShortArrayRegion(JNIEnv*, jshortArray, jsize, jsize, jshort*);
void GetIntArrayRegion(JNIEnv*, jintArray, jsize, jsize, jint*);
void GetLongArrayRegion(JNIEnv*, jlongArray, jsize, jsize, jlong*);
void GetFloatArrayRegion(JNIEnv*, jfloatArray, jsize, jsize, jfloat*);
void GetDoubleArrayRegion(JNIEnv*, jdoubleArray, jsize, jsize, jdouble*);
Set<PrimitiveType>ArrayRegion
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, const NativeType *buf);
void SetBooleanArrayRegion(JNIEnv*, jbooleanArray, jsize, jsize, const jboolean*);
void SetByteArrayRegion(JNIEnv*, jbyteArray, jsize, jsize, const jbyte*);
void SetCharArrayRegion(JNIEnv*, jcharArray, jsize, jsize, const jchar*);
void SetShortArrayRegion(JNIEnv*, jshortArray, jsize, jsize, const jshort*);
void SetIntArrayRegion(JNIEnv*, jintArray, jsize, jsize, const jint*);
void SetLongArrayRegion(JNIEnv*, jlongArray, jsize, jsize, const jlong*);
void SetFloatArrayRegion(JNIEnv*, jfloatArray, jsize, jsize, const jfloat*);
void SetDoubleArrayRegion(JNIEnv*, jdoubleArray, jsize, jsize, const jdouble*);
8.2 引用类型数组
NewObjectArray
/*
* 构造一个新的数组,它包含 elementClass 类的对象。数组中所有元素的初值都设置为initialElement。
*
* @param env: JNI接口指针。
* @param length:数组长度。
* @param elementClass:数组元素类。
* @param initialElement:数组元素初值。
* @return 返回Java数组对象,无法构造数组则返回NULL。
*/
jobjectArray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement);
GetObjectArrayElement
/*
* 返回Object数组的元素。
*
* @param env: JNI接口指针。
* @param array:一个Java数组对象。
* @param index:数组索引。
* @return 返回一个Java对象。
*/
jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);
SetObjectArrayElement
/*
* 设置Object数组的元素。
*
* @param env: JNI接口指针。
* @param array:一个Java数组对象。
* @param index:数组索引。
* @param value:新值。
*/
void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);
8.3 获取数组长度
GetArrayLength
/*
* 返回数组中元素的数量。
*
* @param env: JNI接口指针。
* @param array:一个Java数组对象。
* @return 返回数组的长度。
*/
jsize GetArrayLength(JNIEnv *env, jarray array);
8.4 数组的使用实例
MainActivity.java:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = findViewById(R.id.sample_text);
int[] arr1 = {1, 1};
String[] arr2 = {"java value"};
String s = "result:" + Arrays.toString(testArray(arr1, arr2))
+ "\n" + "arr1:" + Arrays.toString(arr1)
+ "\n" + "arr2:" + Arrays.toString(arr2);
tv.setText(s);
}
public native int[] testArray(int[] arr1, String[] arr2);
native-lib.cpp:
extern "C" JNIEXPORT jintArray JNICALL Java_com_qxt_myapp_MainActivity_testArray(JNIEnv *env, jobject thiz, jintArray arr1, jobjectArray arr2) {
//Update primitive type array item
jint* _arr1 = env->GetIntArrayElements(arr1, NULL);
int length1 = env->GetArrayLength(arr1);
for (int i = 0; i < length1; i++) {
_arr1[i] = 2;
}
env->ReleaseIntArrayElements(arr1, _arr1, 0);
//Update object array
jstring _arr2 = (jstring) env->GetObjectArrayElement(arr2, 0);
const char* s = env->GetStringUTFChars(_arr2, NULL);
LOGD("[testArray] old arr2[0]:%s", s);
jstring newArr2 = env->NewStringUTF("JNI value");
env->SetObjectArrayElement(arr2, 0, newArr2);
//create new array
int array[2] = {3, 3};
jintArray dst = env->NewIntArray(2);
env->SetIntArrayRegion(dst, 0, 2, array);
return dst;
}
9. JNI的异常
9.1 API介绍
Throw
/*
* 抛出一个 java.lang.Throwable对象。
*
* @param env: JNI接口指针。
* @param obj:java.lang.Throwable对象。
* @return 成功返回0,否则返回负数。
*/
jint Throw(JNIEnv *env, jthrowable obj);
ThrowNew
/*
* 使用指定的消息从指定的类构造一个异常对象,并抛出该异常。
*
* @param env: JNI接口指针。
* @param clazz:java.lang.Throwable的子类
* @param message:用于构造java.lang.Throwable对象的消息。该字符串以修改后的UTF-8编码。
* @return 成功返回0,否则返回负数。
*/
jint ThrowNew(JNIEnv *env, jclass clazz, const char *message);
ExceptionOccurred
/*
* 确定是否引发异常。在本地代码调用ExceptionClear()或Java代码处理该异常之前,该异常将一直被抛出 。
*
* @param env: JNI接口指针。
* @return 返回当前正在抛出的异常对象,如果当前没有抛出异常则返回NULL。
*/
jthrowable ExceptionOccurred(JNIEnv *env);
ExceptionDescribe
/*
* 将异常和堆栈的回溯打印到系统错误报告通道,例如stderr。这是为调试提供的便利例程。
*
* @param env: JNI接口指针。
*/
void ExceptionDescribe(JNIEnv *env);
ExceptionClear
/*
* 清除当前引发的任何异常。如果当前未引发任何异常,则该工作不生效。
*
* @param env: JNI接口指针。
*/
void ExceptionClear(JNIEnv *env);
FatalError
/*
* 引发致命错误,并且不希望VM恢复。此函数不返回。
*
* @param env: JNI接口指针。
* @param msg:错误消息。该字符串以修改后的UTF-8编码。
*/
void FatalError(JNIEnv *env, const char *msg);
ExceptionCheck
/*
* JNI提供的一种便利功能,可以检查正在抛出的异常,而无需创建对异常对象的本地引用。
*
* @param env: JNI接口指针。
* @return 有正在抛出的异常时返回JNI_TRUE;否则返回JNI_FALSE。
*/
jboolean ExceptionCheck(JNIEnv *env);
9.2 异常使用实例
MainActivity.java:
public native void throwException();
native-lib.cpp:
extern "C" JNIEXPORT void JNICALL Java_com_qxt_myapp_MainActivity_throwException(JNIEnv *env, jobject thiz) {
jclass clazz = env->FindClass("java/lang/UnsupportedOperationException");
if (clazz != nullptr) {
env->ThrowNew(clazz, "Sorry, device is unsupported.");
}
env->DeleteLocalRef(clazz);
}
10. 注册native方法
java 中的native方法和JNI中的本地函数,需要建立起对应关系才能正常调用。建立对应关系的方式有两种:
- 静态注册
- 动态注册
10.1 静态注册
本地函数静态注册的格式为:
extern "C" JNIEXPORT [JNI参数类型] JNICALL Java_[包名][类名][方法名](JNIEnv* env, jobject, [JNI参数类型,参数名])
其中,包名、类名、方法名之间的点用下划线代替,包名、类名、方法名都必须与java文件中声明的native方法完全一致,返回类型和参数类型,根据第三节的Java和JNI的参数对照表,一一对应。从第4节中的代码截图可以看到,用Android Studio刚刚创建的这个项目,Android Studio已经自动为我们选择了自动静态注册的方式。
extern "C" JNIEXPORT jstring JNICALL Java_com_qxt_myapp_MainActivity_stringFromJNI(JNIEnv* env, jobject ) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
静态注册的缺点:
编写不方便,JNI 方法名字必须遵循固定的规则且名字很长,在包名和类名较长的情况下,会显得非常恶心,对于有代码规范强迫症的人,这一点很难忍受。
程序运行效率不高,首次调用native方法时需要根据方法名在JNI中查找对应的本地函数并建立对应关系,这个过程是比较耗时的。
静态注册的优点:
使用方便,本来按手动流程,静态注册使用起来是很麻烦的,但是现在Android Studio已经完美的帮我们解决了这个问题。现在静态注册使用起来非常方便,静态注册的native方法和本地函数之间可以像普通java方法一样进行跳转查看。
10.2 动态注册
在调用 System.loadLibrary的加载库文件时,JNI会回调一个叫 JNI_OnLoad()的函数,在JNI_OnLoad函数中做一些初始化相关的工作。JNI还提供一个叫RegisterNatives的函数,用于注册native方法。它的定义为:
jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);
因此,我们可以在JNI_OnLoad()的函数中调用RegisterNatives函数注册native方法。 这样提前建立native方法和本地函数的对应关系,优化掉静态注册首次调用需要查找的耗时。动态注册整体流程如下:
- a. 编写cpp实现JNI_Onload()方法。
- b. 将Java 方法和 C/C++方法通过签名信息一一对应起来,可以使用javap -s xx.class查看签名信息。
- c. 使用类名和对应起来的方法作为参数,调用RegisterNatives函数注册native方法。
这里已经写了一个比较标准的模板,可以直接拷贝使用,需要注册新函数时只需要在nativeMethods数组中填写相应的native方法名、签名信息、本地函数名即可。
jstring stringJNI(JNIEnv *env, jobject) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
static int registerNatives(JNIEnv *env) {
//要注册的java类的路径(完整的包名和类名)
const char *className = "com/qxt/myapp/MainActivity";
/*
* 要注册的函数列表
* 参数:
* 1.java中用native关键字声明的函数名
* 2.函数签名,格式:(参数类型)返回类型, 可以使用javap -s xx.class查看
* 3.C/C++中对应函数的函数名(地址)
* */
const JNINativeMethod nativeMethods[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void *) stringJNI},
};
jclass clazz = nullptr;
clazz = env->FindClass(className);
if (clazz == nullptr) {
return JNI_FALSE;
}
int methodsCount = sizeof(nativeMethods) / sizeof(nativeMethods[0]);
//注册函数 参数:java类名, 要注册的函数数组 ,要注册函数的数量
if (env->RegisterNatives(clazz, nativeMethods, methodsCount) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
assert(env != nullptr);
//registerNatives -> env->RegisterNatives
if (!registerNatives(env)) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
动态注册的缺点:
- 使用不方便,与静态注册相对的,动态注册的本地函数目前在Android Studio中是无法进行跳转查看的,使用起来相对不方便。
动态注册的优点:
- 流程更加清晰可控。
- 效率更高,提前建立了对应关系,首次调用无需查找。
11. cmake的简单使用介绍
在介绍cmake的使用之前,我们先来回顾一下使用Makefile构建和编译C/C++源代码,一般情况下我们需要在Android.mk文件中:
- 定义头文件路径。
- 定义依赖的库文件路径。
- 定义源代码路径。
- 定义相关的FLAG,ABI等等。(这里只简单介绍cmake的使用,这项不展开讲,有需要的可以去cmake官网看一下。)
- 定义需要链接的库。
对应到CMakeLists.txt也是一样的套路,以下是一个比较常见的cmake使用的例子,包含导入头文件、库文件、链接库文件等等:
#定义支持的cmake的最小版本
cmake_minimum_required(VERSION 3.4.1)
#导入的库对应的头文件的路径
include_directories(${CMAKE_SOURCE_DIR}/src/main/cpp/include)
#定义导入的库
add_library(#库名称
opencv_java3
#库的类型,可以为SHARED或者STATIC,根据导入的库填写,.so为SHARED, .a为STATIC
SHARED
#声明是导入的
IMPORTED)
#定义导入的库文件的路径
set_target_properties(#库名称
opencv_java3
PROPERTIES
#库路径
IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libopencv_java3.so)
#定义要编译的库
add_library( #库名称
native-lib
#库的类型,可以为SHARED或者STATIC,一般为SHARED
SHARED
#C/C++源文件,需要完整的路径和名称,源文件可以有多个,每个以空格隔开
native-lib.cpp )
find_library( log-lib
log )
#链接
target_link_libraries( #编译的库
native-lib
#需要依赖的库,可以有多个,每个以空格隔开
opencv_java3
${log-lib} )
cmake就简单介绍一下,我们只需要知道它是用来替代Makefile用来构建和编译C/C++源文件的,并且了解CMakeLists.txt的一般配置,应付一般的JNI项目开发就完全没有问题了。如果需要了解更多cmake的使用,请看官网教程:https://cmake.org/cmake/help/latest/guide/tutorial/index.html
12. 参考:
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html
https://blog.csdn.net/qq_20404903/article/details/80662316
https://www.oschina.net/p/android+ndk?hmsr=aladdin1e1
感谢几位原作者辛勤付出。
欢迎交流、点赞、转载,码字不易,转载请注明出处。