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;
}
网友评论