Android JNI开发(一)

前言

长文预警,本文是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中下载这三个组件。


NDK.png

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项目就创建好了,如图:


jni.png

创建好的项目如上图所示。我们无需再生成头文件以及手动写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基本类型相对应的基本类型。具体如下表:


JNI和Java基本类型对照表.png

4.2 引用类型

JNI包括了许多与Java引用类型相对应的引用类型。具体如下表:


JNI和Java引用类型对照表.png

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的类型签名表示。具体如下表:


JVM类型签名对照表.png

例如,对于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
感谢几位原作者辛勤付出。

欢迎交流、点赞、转载,码字不易,转载请注明出处。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266