Android 使用JNI

在Android Studio 新建一个包含C++的工程,新建完成后,可以在app module目录下看到一个叫做CMakeLists.txt的文件,我们打开该文件,可以看到以下的内容,我对其中的cmake代码做了注释:

# cmake的最低版本
cmake_minimum_required(VERSION 3.4.1)

# 添加库
add_library( #  库名称
             native-lib

             # 库的类型 静态(STATIC)/动态(SHARED)
             SHARED

             # 包含的源文件
             src/main/cpp/native-lib.cpp )

# 查找库
find_library( # 重新命名
              log-lib

              # 查找NDK中的库
              log )

# 链接库
target_link_libraries( # 库名称,跟前面add_library中的名称保持一致
                       native-lib

                       # 需要链接的库名称, 可以链接多个库,用空格或换行符隔开
                       ${log-lib} )

在app/src/main/cpp/目录下看到一个native-lib.cpp的文件。打开文件,我们可以看到诸如以下的代码:

#include <jni.h>
#include <string>

extern "C"
JNIEXPORT jstring

JNICALL
Java_com_cgfay_ndklearningproject_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {

    std::string hello = "Hello from C++";
    
    return env->NewStringUTF(hello.c_str());
}

然后我们回到Java层的MainActivity.java文件,可以看到诸如以下的代码:

public class MainActivity extends AppCompatActivity {

    // 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();
}

那么,根据这个模板,我们来分析一下,Android JNI的使用方法吧。
首先,Java层中使用了下面的代码加载了一个库,库的名称是native-lib:

static {
        System.loadLibrary("native-lib");
    }

这个库就是CMakeLists.txt中的add_library中定义的名称,接着定义了一个native方法:

public native String stringFromJNI();

然后,我们看到native-lib.cpp文件中定义了同样的方法:

JNIEXPORT jstring

JNICALL
Java_com_cgfay_ndklearningproject_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */)

这个方法包含了包名,Java层的包名中的 "." 了 "_"。至此,我们了解到了JNI调用的最基本方法。就是通过定义一串长长的包名以及对应方法,通过这样的方式绑定native层。好了,废话不多说,直接进入正题。首先,为了方便使用log输出,我们定义如下方法帮助我们打印输出,用来替代printf:

#include "android/log.h"
#define JNI_TAG "JNI_LEARN"
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, JNI_TAG, __VA_ARGS__)
#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, JNI_TAG, __VA_ARGS__)
数据类型介绍

JNI的原始数据类型 —— Primitive Data Types

Java Type JNI Type C/C++ Type Size
Boolean Jboolean unsigned char unsigned 8 bits
Byte jbyte char signed 8 bits
Char jchar unsigned short unsgined 16 bits
Short jshort short signed 16 bits
Int jint int signed 32 bits
Long jlong long long signed 64 bits
Float jfloat float 32 bits
Double jdouble double 64 bits

JNI 引用类型:

Java Type Native Type
java.lang.Class jclass
java.lang.Throwable jthrowable
java.lang.String jstring
Other objects jobject
java.lang.Object[] jobjectArray
boolean[] jbooleanArray
byte[] jbyteArray
char[] jcharArray
short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdoubleArray
Other Arrays jArray
String 类型
新建一个Java String
std::string str = "hello world";
jstring javaString = env->NewStringUTF(str.c_str())
将Java String转成 C String
    const char *str;
    jboolean isCopy;
    str = env->GetStringUTFChars(javaString, &isCopy);
    if (!str) {
        printf("java string: %s", str);
        if (isCopy == JNI_TRUE) {
            ALOGI("C string is a copy of the Java string");
        } else {
            ALOGI("C string points to actual string");
        }
    }
    env->ReleaseStringUTFChars(javaString, str);

编译运行我们可以打印一下Log:


打印的log
Array 类型

Array类型操作如下:

extern "C"
JNIEXPORT jintArray JNICALL
Java_com_cgfay_ndklearningproject_JniHelper_arrayFromJNI(JNIEnv *env, jobject instance) {

    // 新建一个 int 数组
    jintArray javaArray = env->NewIntArray(10);
    if (javaArray != 0) {
        // 赋值操作
        jint *h = new jint[10];
        for (int i = 0; i < 10; ++i) {
            h[i] = i;
        }
        env->SetIntArrayRegion(javaArray, 0, 10, h);

        // 从jArray中取得数据
        jint nativeArray[10];
        env->GetIntArrayRegion(javaArray, 0, 10, nativeArray);
        for (int i = 0; i < sizeof(nativeArray) / sizeof(jint); ++i) {
            ALOGI("nativeArray: %d", nativeArray[i]);
        }

        // 使用GetIntArrayElements方法获取的是一个指针,是需要释放的
        jint *nativeDirectArray;
        jboolean isCopy;
        nativeDirectArray = env->GetIntArrayElements(javaArray, &isCopy);
        for (int i = 0; i < 10; ++i) {
            ALOGI("nativeDirectArray: %d", nativeDirectArray[i]);
        }
        if (isCopy == JNI_TRUE) {
            ALOGI("native direct array is a copy of the java array");
        } else {
            ALOGI("native direct array points to actual array");
        }
        // 释放类型
        // 0: 复制内容并释放本地数组
        // JNI_CIMMIT: 复制内容但不释放本地数据,用于定期更新Java数据
        // JNI_ABORT: 释放本地数组,但不复制内容
        env->ReleaseIntArrayElements(javaArray, nativeDirectArray, 0);
    }

    return javaArray;
}
NIO操作

NIO(Native I/O)在缓冲管理区、大规模网络和文件I/O以及字符集支持方面的性能有所改进。NIO缓冲区的数据传送性能较好,更适合在原生代码和Java应用程序之间传送大量数据。

创建字节缓冲区
unsigned char *buffer  = (unsigned char *) malloc(1024);
...
jobject directBuffer = env->NewDirectByteBuffer(buffer, 1024);
备注:

原生方法中的内存分配超出虚拟机管理范围,且不能用虚拟机的垃圾回收器回收原生方法中的内存。原生函数应该手动释放未使用的内存以避免内存泄漏。

直接缓冲区获取

直接获取原生字节数组的内存地址:

unsigned char* buffer = (unsigned char *) env->GetDirectBufferAddress(directBuffer);
访问域

Java有两类域: 实例域 和 静态域。所有实例共享一个静态域,实例有自己的实例域副本。访问域的操作如下:
JniHelper中增加两个变量和一个native方法:

    // 静态域
    private static String staticField = "Static Field";
    // 实例域
    private String instanceField = "Instance Field";
    // 静态域与实例域
    public native void instanceFieldAndStaticField();
extern "C"
JNIEXPORT void JNICALL
Java_com_cgfay_ndklearningproject_JniHelper_instanceFieldAndStaticField(JNIEnv *env,
                                                                        jobject instance) {
    // 获取class对象
    jclass clazz =  env->GetObjectClass(instance);
    // 获取实例域的Id
    jfieldID instanceFieldId = env->GetFieldID(clazz, "instanceField", "Ljava/lang/String;");
    // 根据实例域Id获取实际的java 的字符串,也就是获取实例域
    jstring instanceField = (jstring) env->GetObjectField(instance, instanceFieldId);
    const char *instanceStr;
    jboolean isCopy;
    instanceStr = env->GetStringUTFChars(instanceField, &isCopy);
    ALOGI("instance field: %s", instanceStr);
    env->ReleaseStringUTFChars(instanceField, instanceStr);

    // 获取静态域的Id
    jfieldID staticFieldId = env->GetStaticFieldID(clazz, "staticField", "Ljava/lang/String;");
    // 根据静态域Id获取实际的Java的字符串,也就是获取静态域
    jstring staticField = (jstring) env->GetStaticObjectField(clazz, staticFieldId);
    const char *staticStr;
    staticStr = env->GetStringUTFChars(staticField, 0);
    ALOGI("instance field: %s", staticStr);
    env->ReleaseStringUTFChars(staticField, staticStr);

}

其中,"Ljava/lang/String;" 这个后续会讲到。现在你只要知道这是获取String 类型就行。
打印输出的Log如下:


访问域输出
备注:

获取单个域值需要调用两到三个JNI函数,存在性能问题,一般情况下都是直接从Java层传输native方法调用,而不是让Native方法去访问java中的域值。

调用方法

java有两类方法 —— 实例方法和静态方法。下面介绍如何使用Native方法反过来调用java层的方法。
在JniHelper中添加三个方法:

    /**
     * native调用的静态方法
     * @return
     */
    private static String staticMethod() {
        return "Static Method";
    }

    /**
     * native调用的实例方法
     * @return
     */
    private String instanceMethod() {
        return "Instance Method";
    }

    // 静态方法与实例方法
    public native void instanceMethodAndStaticMethod();

访问方法的方式如下:

extern "C"
JNIEXPORT void JNICALL
Java_com_cgfay_ndklearningproject_JniHelper_instanceMethodAndStaticMethod(JNIEnv *env,
                                                                          jobject instance) {

    // 获取class对象
    jclass clazz = env->GetObjectClass(instance);
    // 获取实例方法Id
    jmethodID instanceMethodId = env->GetMethodID(clazz, "instanceMethod", "()Ljava/lang/String;");
    jstring instanceMethodResult = (jstring) env->CallObjectMethod(instance, instanceMethodId);
    const char *instanceStr;
    instanceStr = env->GetStringUTFChars(instanceMethodResult, 0);
    ALOGI("instance method: %s", instanceStr);
    env->ReleaseStringUTFChars(instanceMethodResult, instanceStr);

    // 获取静态方法Id
    jmethodID staticMethodId = env->GetStaticMethodID(clazz, "staticMethod", "()Ljava/lang/String;");
    jstring staticMethodResult = (jstring) env->CallStaticObjectMethod(clazz, staticMethodId);
    const char *staticStr;
    staticStr = env->GetStringUTFChars(staticMethodResult, 0);
    ALOGI("static method call: %s", staticStr);
    env->ReleaseStringUTFChars(staticMethodResult, staticStr);
}

其中,"()Ljava/lang/String;" 表示的是方法的签名,包括参数和返回值。后续将会介绍到。编译运行,则可以看到打印输出一下Log:


访问方法输出
备注:

为了性能考虑,一般会缓存常用的方法Id。比如native层执行操作完成之后,通过这样的方式回调输出。

域和方法描述符

前面提到的获取于域ID 和 方法Id 均分别需要域描述符和方法描述符。描述符可以通过下面的表格对应的签名映射获得:

Java 类型 签名
Boolean Z
Byte B
Char C
Short S
Int I
Long J
Float F
Double D
类全称 L类全称
type[] [type
void V
方法类型 (参数类型)返回值类型

比如前面的 "()Ljava/lang/String;" 表示的是参数为void,返回值为String的方法。java/lang/String; 表示的是类的全称,也就是java.lang.String。
我们可以通过使用javap 来反汇编程序,得到各种类里面的签名

异常处理

native中的异常行为与Java中的有所不同。我们可以在native层发生异常后回传给java层做处理。

捕获与抛出异常

在JniHelper中定义两个方法:

    /**
     * native层捕获异常与抛出异常
     * @throws NullPointerException
     */
    private void throwingMethod() throws NullPointerException {
        throw new NullPointerException("Null Pointer!");
    }
    // 捕获异常
    public native void acessMethods();

native层的实现如下:

/**
 * 捕获异常与抛出异常
 */
extern "C"
JNIEXPORT void JNICALL
Java_com_cgfay_ndklearningproject_JniHelper_acessMethods(JNIEnv *env, jobject instance) {

    jthrowable ex;
    jclass clazz = env->GetObjectClass(instance);
    // 获取跑出异常的方法
    jmethodID throwingMethodId = env->GetMethodID(clazz, "throwingMethod", "()V");
    // 调用方法
    env->CallVoidMethod(instance, throwingMethodId);
    // 查询虚拟机中是否有挂起的异常(显式做异常处理)
    ex = env->ExceptionOccurred();
    if (ex != 0) {
        // 描述
        env->ExceptionDescribe();
        // 使用完成需要清除异常
        env->ExceptionClear();

        // 抛出异常
        jclass newExcCls = env->FindClass("java/lang/IllegalArgumentException");
        if (newExcCls) {
            env->ThrowNew(clazz, "Exception message");
        }
    }

}

调用acessMethods 方法后,你就会看到如下所示的异常:


异常处理
局部和全局引用
局部引用

大多数JNI函数返回局部引用。可以通过FindClass函数返回一个局部引用。局部引用不能再后续的调用中被缓存以及重用。当一个原生方法返回时,它被自动释放,也可以调用DeleteLocalRef(clazz);方法释放原生代码

全局引用

全局引用在原生方法的后续调用过程中依然有效,除非被显式地释放掉。全局方法的使用如下:
1、创建全局引用

jclass localClazz;
jclass globalClazz;
...
localClazz = env->FindClass("java/lang/String");
globalClazz = env->NewGlobalRef(localClazz);
...
env->DeleteLocalRef(localClazz);

2、删除全局引用

env->DeleteGlobalRef(globalClazz);
弱全局引用

1、创建弱全局引用

jclass weakGlobalClazz = env->NewWeakGlobalRef(localClazz);

2、弱全局引用的有效性检验

if (env->IsSameObject(weakGlobalClazz, NULL) == JNI_FALSE) {
// 对象仍然处于活动状态且可以使用
} else {
// 对象回收,不能使用
}

3、删除弱全局引用

env->DeleteWeakGlobalRef(weakGlobalClazz);
线程

虚拟机支持运行时原生代码,但有约束:
只在原生方法执行期间以及正在执行原生方法的线程环境下局部引用是有效的,局部引用不能在多线程间共享
被传递给每个原生方法的JNIEnv接口指针在于方法调用相关的线程中也是有效的,它不能被其他线程缓存或使用

同步

JNI允许原生代码利用Java对象同步,虚拟机保证存储监视器的线程能够安全执行,而其他线程等待监视器对象变成可用状态。
Java 同步代码块

synchronized(obj) {
// 同步线程安全代码块
}

等价原生代码块:

if (env->MonitorEnter(obj) == JNI_OK) {
// 错误处理
}

// 同步线程安全代码块


if (env->MonitorExit(obj) == JNI_OK) {
// 错误处理
}
原生线程

JNI通过JavaVM接口指针提供了AttachCurrentThread 函数便于让原生代码将原生线程附着到虚拟机上。JavaVM接口指针应该尽早被缓存,否则不能被获取
将当前线程与虚拟机附着和分离:

JavaVM *cachedJvm;

JNIEnv *env;

// 将当前线程附着到虚拟机
cachedJvm->AttachCurrentThread(&env, NULL);

/* 可以用JNIEnv 接口实现线程与Java应用程序的通信 */
...

// 将当前线程与虚拟机分离
cachedJvm->DetachCurrentThread();

线程使用例子如下:
在JniHelper中添加以下方法:

    /**
     * native 线程回调
     */
    private String threadCallBack() {
        return "thread call back";
    }

    // 开始线程
    public native void startThread();

native层实现如下:


/**
 * 线程操作例子
 */

JavaVM* javaVM;
jobject gInstance;

static void *thread_sample(void* argv) {
    JNIEnv *env;
    // 从全局的JavaVM中获取环境变量
    javaVM->AttachCurrentThread(&env, NULL);
    ALOGI("log in another thread!!!!");
    // 获取Java层对应的类
    jclass clazz = (jclass) env->GetObjectClass(gInstance);
    // 获取方法
    jmethodID id = env->GetMethodID(clazz, "threadCallBack", "()Ljava/lang/String;");
    jstring jStr = (jstring) env->CallObjectMethod(gInstance, id);
    const char *instanceStr;
    instanceStr = env->GetStringUTFChars(jStr, 0);
    ALOGI("instance method: %s", instanceStr);
    env->ReleaseStringUTFChars(jStr, instanceStr);
    sleep(1);
    // 删除全局变量
    env->DeleteGlobalRef((jobject)gInstance);
    javaVM->DetachCurrentThread();

    return NULL;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_cgfay_ndklearningproject_JniHelper_startThread(JNIEnv *env, jobject instance) {

    // 获取JavaVM
    env->GetJavaVM(&javaVM);
    // 创建全局对象
    gInstance = env->NewGlobalRef(instance);

    // 创建新线程
    pthread_t thread;

    pthread_create(&thread, NULL, thread_sample, NULL);
}

最后编译运行,可以看到以下的Log:


线程使用log

工程的Github 地址如下:
NDKLearningProject

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

推荐阅读更多精彩内容

  • *** 说明:本文不代表博主观点,均是由以下资料整理的读书笔记。 *** 【参考资料】 1、向您的Android ...
    莫绪旻_向屿阅读 1,097评论 0 5
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 懒 今天是一场什么旅程呢? 给自己打多少分? 在家宅了整整一天。和我的床亲密接触。 学习很仓促,很表面,很假。我想...
    小鱼是不老少女阅读 137评论 0 0
  • 小时候最喜欢玩的小游戏是过家家,结婚这个小项目。那时候的我们还有点明清年代的氛围,几个男的小伙伴一起搭把手做个“井...
    珍珍雅阅读 157评论 2 0