美文网首页
Android JNI学习笔记

Android JNI学习笔记

作者: 生活简单些 | 来源:发表于2021-05-19 13:42 被阅读0次

1. build.gradle里的基本配置

android {
  compileSdkVersion 30
  ndkVersion '22.1.7171670'  // 最新的版本如果不指定ndk的版本,编译会卡死并报错

  defaultConfig {
      externalNativeBuild {
          cmake {
              cppFlags '-std=c++17'
          }
      }
      ndk {
          // 发布版本可以适当保留部分abi so,比如只保留"arm64-v8a"
          abiFilters "x86", "x86_64", "armeabi-v7a", "arm64-v8a"
      }
  }

  externalNativeBuild {
      cmake {
          path file('src/main/cpp/CMakeLists.txt')
          version "3.6.0" // 更高的版本会导致cmake里的message()不能打印log
      }
  }
}

2. 为什么JNI 函数为什么要申明extern "C"

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

答案: 函数申明加上 extern "C", 是因为如果写C++函数时候,C++支持函数重载,在编译时候会把函数名和参数一同编译,编译出来的目标文件(即:.a文件)无法和C编译器编出来的目标文件连接的,加了extern "C"后编译出来的目标文件不包含参数类型定义, 如果不加会导致Java native方法找不到C++函数。

3. 基础类型转换

Java类型 别名 C++本地类型 字节(bit)
boolean jboolean unsigned char 8
byte jbyte signed char 8
char jchar unsigned short 16
short jshort short 16
int jint long 32
long jlong long long 64
float jfloat float 32
double jdouble double 64

基本数据类型JNI的实现已经非常好,无需做太多的事情,案例如下:

public native int callNativeInt();
extern "C"
JNIEXPORT jint JNICALL
Java_com_learn_jni_MainActivity_callNativeInt(JNIEnv *env, jobject thiz) {
   return 123;
}

如上: 在c++中的属性类型自动会通过jint转为java的int,其它基础类型转换也类似。

4. 将Java string转为C/C++ string

extern "C"
JNIEXPORT jstring JNICALL
Java_com_learn_jni_MainActivity_stringToNative(JNIEnv *env, jobject thiz, jstring name) {
    // 因为Java编码基本都是utf-8格式,因此推荐此API
    const char* utfStr = env->GetStringUTFChars(name, JNI_FALSE);
    env->ReleaseStringUTFChars(name, utfStr);

    // 如果Java编码未采用utf-8,则采取此API
    const jchar* str = env->GetStringChars(name, JNI_FALSE);
    env->ReleaseStringChars(name, str);

    return env->NewStringUTF("this is from C/C++");
}

字符串不属于基本数据类型,需要额外手动通过JNI的函数转换, 而且需要手动释放内存。

5. 引用类型转换

Java类型 别名 C++本地类型
Object jobject 任何Java对象或没有对应的Java类型对象
Class jclass Class类对象
String jstring 字符串对象
Object[] jobjectArray 任何对象数组
boolean[] jbooleanArray 布尔型数组
byte[] jbyteArray 比特型数组
char[] jcharArray 字符型数组
short[] jshortArray 短整型数组
int[] jintArray 整型数组
long[] jlongArray 长整型数组
float[] jfloatArray 浮点型数组
double[] jdoubleArray 双浮点型数组
java.lang.Throwable jthrowable 异常

引用类型有很多种,包括对象和数组,其中JNI提供了很多基础数据类型的数组的各种API, Java字符串数组也属于对象数组范畴,下面是Java字符串数组传入C/C++的案例,也是相对最麻烦的一种情况:

public native String callNativeStringArray(String[] strArray);
extern "C"
JNIEXPORT jstring JNICALL
Java_com_learn_jni_MainActivity_callNativeStringArray(JNIEnv *env, jobject thiz,
                                                      jobjectArray str_array) {
    int len = env->GetArrayLength(str_array);
    LOGD("array length: %d", len);

    auto first = static_cast<jstring>(env->GetObjectArrayElement(str_array, 0));
    const char* str = env->GetStringUTFChars(first, JNI_FALSE);
    LOGD("converted C/C++ string: %s", str);

    env->ReleaseStringUTFChars(first, str);
    return env->NewStringUTF(str);
}

6. C/C++访问Java静态变量

基本数据类型的Sign定义:

Java Sign
boolean Z
byte B
char C
short S
int I
long J
float F
double D

引用类型的Sign定义:

Java Sign
String Ljava/lang/String;
Class Ljava/lang/Class;
Throwable Ljava/lang/Throwable;
int[] [I
Object[] [java/lang/Object;

下面实例如何让C/C++篡改Java对象里的static变量和成员变量的值:

public class World {
    public static String name;
    public int height;
}

// C/C++篡改Java变量
World world = new World();
accessStaticField(world);
System.out.println(world.name); // 值被改变成了"this is new name"

 // C/C++实现在下面
public native void accessStaticField(World world);
extern "C"
JNIEXPORT void JNICALL
Java_com_learn_jni_MainActivity_accessStaticField(JNIEnv *env, jobject thiz, jobject world) {
    jclass clazz = env->GetObjectClass(world);
    jfieldID nameField = env->GetStaticFieldID(clazz, "name", "Ljava/lang/String;");
    jstring newStr = env->NewStringUTF("this is new name");
    env->SetStaticObjectField(clazz, nameField, newStr);
}

7. C/C++访问Java对象字段

extern "C"
JNIEXPORT void JNICALL
Java_com_learn_jni_MainActivity_callJavaField(JNIEnv *env, jobject thiz, jobject world) {
    jclass clazz = env->GetObjectClass(world);
    jfieldID heightFieldID = env->GetFieldID(clazz, "height", "I");
    if (heightFieldID == nullptr) {
        return;
    }

    jint height = env->GetIntField(world, heightFieldID);
    LOGD("height: %d", height);
}

8. C/C++访问Java对象方法(C/C++内部构造一个Java对象)

首先,定义定义一个World class,并在其中定义一个打印log的方法,此方法随后会被C/C++传入参数并调用:

package com.learn.jni;
import android.util.Log;

public class World {
    public void hello(String message){
        Log.d("world", message);
    }
}

然后,定义一个native方法,用于从Java端唤起C/C++调用上面的hello()方法:

public native void callJavaMethod();

对应的C/C++实现如下:

extern "C"
JNIEXPORT void JNICALL
Java_com_learn_jni_MainActivity_callJavaMethod(JNIEnv *env, jobject thiz) {
    // Find Java class
    jclass worldClass = env->FindClass("com/learn/jni/World");
    if (worldClass == nullptr) {
        return;
    }

    // Find Java method to be called via C/C++
    jmethodID helloMethod = env->GetMethodID(worldClass, "hello", "(Ljava/lang/String;)V");
    if (helloMethod == nullptr) {
        return;
    }

    // Find construct method
    jmethodID constructMethod = env->GetMethodID(worldClass, "<init>", "()V");
    if (constructMethod == nullptr){
        return;
    }

    // Call construct method to create new java object
    jobject worldObj = env->NewObject(worldClass, constructMethod);
    if (worldObj == nullptr) {
        return;
    }

    // Call Java method with parameter
    jstring message = env->NewStringUTF("hello, I'm C/C++ message");
    env->CallVoidMethod(worldObj, helloMethod, message);
    
    // Release memory
    env->DeleteLocalRef(message);
    env->DeleteLocalRef(worldObj);
    env->DeleteLocalRef(worldClass);
}

7. C/C++非子线程访问Java对象方法

先定义C/C++会触发调用的Callback以及JNI函数

public interface JNICallback {
    void callback();
}

public native void nativeCallback(JNICallback callback);
extern "C"
JNIEXPORT void JNICALL
Java_com_learn_jni_MainActivity_nativeCallback(JNIEnv *env, jobject thiz, jobject callback) {
    jclass callbackClazz = env->GetObjectClass(callback);
    if (callbackClazz == nullptr) {
        return;
    }

    jmethodID callbackMethod = env->GetMethodID(callbackClazz, "callback", "()V");
    if (callbackMethod == nullptr) {
        return;
    }

    env->CallVoidMethod(callback, callbackMethod);
}

8. C/C++子线程访问Java对象方法

同上,先定义C/C++会触发调用的Callback以及JNI函数

public interface JNICallback {
    void callback();
}
 
public native void nativeThreadCallback(JNICallback callback);

C/C++子线程回调Java和非子线程回调Java最大的区别是在子线程里拿不到JNIEnv, 因此我们得想办法缓存一个JNIEnv。首先,缓存线程发起函数里的JNIEnv是不管用的,正确的办法是通过JNI_OnLoad来缓存,它是JNI初始化阶段必经之路。

我们先定义一个jvm.h,里面定义了一个全局JNIEnv, 用于缓存JNI_OnLoad()里的JNIEnv

#include <jni.h>

#ifndef LEARNJNI_JVM_H
#define LEARNJNI_JVM_H

static JavaVM *JAVA_VM = nullptr;

#ifdef __cplusplus
extern "C" {
#endif

void setJVM(JavaVM *vm) {
    JAVA_VM = vm;
}

JavaVM *getJavaVM() {
    return JAVA_VM;
}

#ifdef __cplusplus
}
#endif

#endif //LEARNJNI_JVM_H

然后在JNI函数入口文件里定义JNI_OnLoad(), 并缓存JNIEnv示例:

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
    JNIEnv *env = nullptr;
    if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_FALSE;
    }

    setJVM(vm);
    return JNI_VERSION_1_6;
}

JNI_VERSION最好返回最大的即可,否则可能会有兼容性问题

下面就是实际在线程函数里通过缓存的JNIEnv调用Java回调了:

void threadCallback(jobject callbackObject, jmethodID callbackMethod) {
    JavaVM *javaVM = getJavaVM();
    JNIEnv *env = nullptr;
    if (javaVM->AttachCurrentThread(&env, nullptr) == JNI_FALSE) {
        env->CallVoidMethod(callbackObject, callbackMethod);
        javaVM->DetachCurrentThread();
    }
}

extern "C"
JNIEXPORT void JNICALL
Java_com_learn_jni_MainActivity_nativeThreadCallback(JNIEnv *env, jobject thiz, jobject callback) {
    auto callbackObject = env->NewGlobalRef(callback);
    auto callbackClazz = env->GetObjectClass(callback);
    auto callbackMethod = env->GetMethodID(callbackClazz, "callback", "()V");

    std::thread thread(threadCallback, callbackObject, callbackMethod);
    thread.join();
}

9. JNI中三种引用类型

  • 全局引用: 类似对应java里的强引用
public native String globalRef(); //Java入口函数
// 对应native的实现
extern "C"
JNIEXPORT jstring JNICALL
Java_com_learn_jni_MainActivity_globalRef(JNIEnv *env, jobject thiz) {
    static jclass stringClazz = nullptr; // 作为全局静态变量缓存
    if (stringClazz == nullptr) {
        jclass clazz = env->FindClass("java/lang/String");
        stringClazz = static_cast<jclass>(env->NewGlobalRef(clazz));
        env->DeleteLocalRef(clazz); //及时删除localRef百利无一害
    } else {
        LOGD("global cache is used");
    }

    jmethodID method = env->GetMethodID(stringClazz, "<init>", "(Ljava/lang/String;)V");
    jstring str = env->NewStringUTF("hello world");
    return static_cast<jstring>(env->NewObject(stringClazz, method, str));
}

全局引用不会自动删除引用,当App关闭需要手动调用env->DeleteGlobalRef()删除引用,否则有可能会导致内存泄漏。

  • 局部引用: 类似对应java里的软应用
   public native String localRef();
extern "C"
JNIEXPORT jstring JNICALL
Java_com_learn_jni_MainActivity_localRef(JNIEnv *env, jobject thiz) {
    // jclass 当函数离开后会自动回收,但如果同一时刻超过512个会崩溃
    // 因此,在for循环中创建很多jclass,尽可能在循环内部及时释放
    // 释放api: env->DeleteLocalRef(clazz);
    // 创建localRef还有一个专用API: env->NewLocalRef(class), 但很少用到它
    jclass clazz = env->FindClass("java/lang/String");
    jmethodID method = env->GetMethodID(clazz, "<init>", "(Ljava/lang/String;)V");
    jstring str = env->NewStringUTF("hello world");
    return static_cast<jstring>(env->NewObject(clazz, method, str));
}
  • 弱引用: 类似对应java里的弱引用
public native String weekRef();
extern "C"
JNIEXPORT jstring JNICALL
Java_com_learn_jni_MainActivity_weekRef(JNIEnv *env, jobject thiz) {
    static jclass stringClazz = nullptr;

    if (stringClazz == nullptr) {
        jclass clazz = env->FindClass("java/lang/String");
        stringClazz = static_cast<jclass>(env->NewWeakGlobalRef(clazz));
        env->DeleteLocalRef(clazz);
    } else {
        LOGD("weak cache is used");
    }

    jmethodID method = env->GetMethodID(stringClazz, "<init>", "(Ljava/lang/String;)V");
    
    // 因为弱引用随时可能被回收,因此在使用它之前必须先判断是否已经回收了
    jboolean isGC = env->IsSameObject(stringClazz, nullptr);
    if (isGC) {
        return env->NewStringUTF("class is gc");
    }

    jstring str = env->NewStringUTF("hello world");
    return static_cast<jstring>(env->NewObject(stringClazz, method, str));
}

10. JNI中异常处理

C/C++调用Java代码出现异常内部自己处理

package com.learn.jni;

public class JNIException extends IllegalArgumentException{
   private int operation(){
       return 2/0;
   }

   public native void nativeInvokeException();
}

nativeInvokeException()的C/C++实现里会调用operation() API, 然后有可能会报错.

extern "C"
JNIEXPORT void JNICALL
Java_com_learn_jni_JNIException_nativeInvokeException(JNIEnv *env, jobject thiz) {
    jclass clazz = env->FindClass("com/learn/jni/JNIException");
    jmethodID operatorMethod = env->GetMethodID(clazz, "operation", "()I");
    jmethodID initMethod = env->GetMethodID(clazz, "<init>", "()V");
    jobject obj = env->NewObject(clazz, initMethod);
    env->CallIntMethod(obj, operatorMethod);

    // JNI 自己吞掉异常,不至于App崩溃
    jthrowable exception = env->ExceptionOccurred();
    if (exception) {
        env->ExceptionDescribe();
        env->ExceptionClear();
    }
}

很明显上面C/C++调用java的operation()方法会抛出异常,但C/C++层面拦截并清除了异常,以至于Java因为异常而崩溃,机制类似于Java Catch异常并内部消化了。

C/C++调用Java代码出现异常抛给Java处理

很多时候C/C++调用java方法出现的异常要抛给Java让Java自己处理,当然也可以拦截异常后以统一别的异常抛给Java,这跟Java里catch多种异常并统一返回自定义异常的用途类似。

package com.learn.jni;

public class JNIException extends IllegalArgumentException{
    private int operation(){
        return 2/0;
    }

    public native void nativeThrowException() throws IllegalArgumentException;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_learn_jni_JNIException_nativeThrowException(JNIEnv *env, jobject thiz) {
    jclass clazz = env->FindClass("java/lang/IllegalArgumentException");
    env->ThrowNew(clazz, "native throw exception");
}

11. JNI操作Bitmap

这里的案例是将Bitmap从Java传入C/C++,并且C/C++将bitmap图像的所有像素左右颠倒并返回颠倒后的bitmap

package com.learn.jni;

import android.graphics.Bitmap;

public class JNIBitmap {
    public native Bitmap callNativeBitmap(Bitmap bitmap);
}
extern "C"
JNIEXPORT jobject JNICALL
Java_com_learn_jni_JNIBitmap_callNativeBitmap(JNIEnv *env, jobject thiz, jobject bitmap) {
    // 获取简单的bitmap信息
    AndroidBitmapInfo info;
    if (AndroidBitmap_getInfo(env, bitmap, &info) == ANDROID_BITMAP_RESULT_SUCCESS) {
        LOGD("bitmap width: %d", info.width);
        LOGD("bitmap height: %d", info.height);
    } else {
        return nullptr;
    }

    // 锁定bitmap并将获得bitmap存储的地址
    void *bitmapPixels;
    AndroidBitmap_lockPixels(env, bitmap, &bitmapPixels);

    // 创建一个用于容纳新bitmap的空的bitmap数组
    // 从bitmap的底部往上面一行一行的摘取并放入新bitmap数组:左边摘取放对应的右边,右边摘取放对应的左边
    auto *newBitmapPixels = new uint32_t[info.width * info.height];
    int whereToGet = 0;
    for (int y = 0; y < info.height; ++y) {
        for (int x = (int)info.width - 1; x >= 0; x--) {
            uint32_t pixel = ((uint32_t *) bitmapPixels)[whereToGet++];
            newBitmapPixels[info.width * y + x] = pixel;
        }
    }
    AndroidBitmap_unlockPixels(env, bitmap);

    // 创建一个指定宽高的新bitmap对象
    jobject newBitmap = buildBitmap(env, info.width, info.height);

    // 锁定bitmap并获得像素存储地址
    void* resultBitmapPixels;
    AndroidBitmap_lockPixels(env, newBitmap, &resultBitmapPixels);
    // 将翻转的bitmap像素数据拷贝进入新的bitmap对象里
    memcpy((uint32_t*)resultBitmapPixels, newBitmapPixels, sizeof(uint32_t) * info.width * info.height);
    AndroidBitmap_unlockPixels(env, newBitmap);

    delete [] newBitmapPixels;
    return newBitmap;
}

// 指定宽高创建一个空bitmap
jobject buildBitmap(JNIEnv *env, jint width, jint height) {
    jclass bitmapClazz = env->FindClass("android/graphics/Bitmap");
    jmethodID createBitmapMethod = env->GetStaticMethodID(
            bitmapClazz,
            "createBitmap",
            "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
    jstring configName = env->NewStringUTF("ARGB_8888");
    jclass configClazz = env->FindClass("android/graphics/Bitmap$Config");
    jmethodID valueOfMethod = env->GetStaticMethodID(
            configClazz,
            "valueOf",
            "(Ljava/lang/String;)Landroid/graphics/Bitmap$Config;");
    jobject bitmapConfig = env->CallStaticObjectMethod(configClazz, valueOfMethod, configName);
    jobject bitmap = env->CallStaticObjectMethod(bitmapClazz, createBitmapMethod, width, height,
                                                 bitmapConfig);
    return bitmap;
}

相关文章

网友评论

      本文标题:Android JNI学习笔记

      本文链接:https://www.haomeiwen.com/subject/pzxudltx.html