最基础的创建JNI
接口的操作,可以直接看这篇文章 : 第一个Android JNI工程,
本文会基于掌握创建JNI接口的操作
的基础之上,来入门JNI/NDK
。
记得CMake
中有log
模块,不然编译不过
target_link_libraries(
#...省略
android
log)
#include
#define LOG_TAG "CPPLOG"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG , __VA_ARGS__) // 定义LOGD类型
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG , __VA_ARGS__) // 定义LOGE类型
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG , __VA_ARGS__) // 定义LOGE类型
LOGD("java int value is %p", value);
JNI
和Java
基础类型可以直接进行转换
在jni.h
中我们可以看到JNI
的基础类型有这些,比如jint
其实就是对应C++
中的int32_t
类型
/* Primitive types that match up with Java equivalents. */
typedef uint8_t jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
在C++中,_t是一种命名约定,表示某个类型。通常在命名中使用_t作为类型的后缀,以便区分该名称是一个类型而不是其他实体(例如变量或函数)。
我把它整理成了一个表格,Java
基础类型和JNI
基础类型相对应
Java | Native |
---|---|
boolean | jboolean |
byte | jbyte |
char | jchar |
short | jshort |
int | jint |
long | jlong |
float | jfloat |
double | jdouble |
在Java类中编写JNI方法
external fun callNativeInt(value:Int) : Int
external fun callNativeByte(value:Byte) : Byte
external fun callNativeChar(value:Char) : Char
external fun callNativeLong(value:Long) : Long
external fun callNativeFloat(value:Float) : Float
external fun callNativeDouble(value:Double) : Double
extern "C"
JNIEXPORT jint JNICALL
Java_com_heiko_myopencvtest2023_NativeLib_callNativeInt(JNIEnv *env, jobject thiz, jint value) {
LOGD("value:%d", value);
return value + 1;
}
extern "C"
JNIEXPORT jbyte JNICALL
Java_com_heiko_myopencvtest2023_NativeLib_callNativeByte(JNIEnv *env, jobject thiz, jbyte value) {
LOGD("value:%d", value);
return value + 1;
}
extern "C"
JNIEXPORT jchar JNICALL
Java_com_heiko_myopencvtest2023_NativeLib_callNativeChar(JNIEnv *env, jobject thiz, jchar value) {
LOGD("value:%d", value);
return value + 1;
}
extern "C"
JNIEXPORT jlong JNICALL
Java_com_heiko_myopencvtest2023_NativeLib_callNativeLong(JNIEnv *env, jobject thiz, jlong value) {
LOGD("value:%d", value);
return value + 1;
}
extern "C"
JNIEXPORT jfloat JNICALL
Java_com_heiko_myopencvtest2023_NativeLib_callNativeFloat(JNIEnv *env, jobject thiz, jfloat value) {
LOGD("value:%f", value);
return value + 1.0;
}
extern "C"
JNIEXPORT jdouble JNICALL
Java_com_heiko_myopencvtest2023_NativeLib_callNativeDouble(JNIEnv *env, jobject thiz,
jdouble value) {
LOGD("value:%f", value);
return value + 1.0;
}
Log.i(TAG, "result:${nativeLib.callNativeInt(1)}")
Log.i(TAG, "result:${nativeLib.callNativeByte(2)}")
Log.i(TAG, "result:${nativeLib.callNativeChar('c')}")
Log.i(TAG, "result:${nativeLib.callNativeLong(4)}")
Log.i(TAG, "result:${nativeLib.callNativeFloat(5F)}")
Log.i(TAG, "result:${nativeLib.callNativeDouble(6.0)}")
打印日志如下
10:16:36.815 D value:1
10:16:36.815 I result:2
10:16:36.815 D value:2
10:16:36.815 I result:3
10:16:36.815 D value:99
10:16:36.815 I result:d
10:16:36.815 D value:4
10:16:36.815 I result:5
10:16:36.815 D value:5.000000
10:16:36.815 I result:6.0
10:16:36.816 D value:6.000000
10:16:36.816 I result:7.0
Java
字符串转成Native
的字符串,并不能直接做转换,需要调用env->GetStringUTFChars()
,
对应的,需要调用env->ReleaseStringUTFChars()
来释放资源。
默认情况下,Java都是UTF编码,如果不是UTF编码,则需要调用
env->GetStringChars()
external fun callNativeString(value:String) : String
extern "C"
JNIEXPORT jstring JNICALL
Java_com_heiko_myopencvtest2023_NativeLib_callNativeString(JNIEnv *env, jobject thiz,
jstring value) {
//Java字符串转成Native的字符串,并不能直接做转换
const char *str = env->GetStringUTFChars(value, NULL); //Java的字符串是UTF编码的
//env->GetStringChars(); //如果不是UTF编码,就用这个
LOGD("str:%s", str);
env->ReleaseStringUTFChars(value, str);
jstring result = env->NewStringUTF("hello world!");
return result;
}
进行调用
Log.i(TAG, "result:${nativeLib.callNativeString("你好呀")}")
nativeLib.stringMethod("hello world!")
执行结果
10:45:45.849 D str:你好呀
10:45:45.849 I result:hello world!
定义JNI
接口
external fun stringMethod(value:String)
实现C++
方法
extern "C"
JNIEXPORT void JNICALL
Java_com_heiko_myopencvtest2023_NativeLib_stringMethod(JNIEnv *env, jobject thiz, jstring value) {
const char *str = env->GetStringUTFChars(value, 0);
int length = env->GetStringLength(value);
LOGD("length:%d", length);
char buf[256];
env->GetStringUTFRegion(value, 0, length, buf); //拷贝字符串数据到char[]中
LOGD("text:%s", buf);
env->ReleaseStringUTFChars(value, str);
}
进行调用
Log.i(TAG, "result:${nativeLib.callNativeString("你好呀")}")
nativeLib.stringMethod("hello world!")
执行结果
10:45:45.849 D length:12
10:45:45.849 D text:hello world!
这里列出了Java
引用类型和JNI
应用类型的对应关系。
值得注意的是,不是所有的Java
引用类型都有对应的JNI
的引用类型。
比如Java
中的字符串数组String[]
,就没有相对应的JNI
的引用类型,这种情况下,都会统一归类为jobject
。
Java Reference | Native |
---|---|
All objects | jobject |
java.lang.Class | jclass |
java.lang.String | jstring |
Object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
java.lang.Throwable | jthrowable |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
Java层传递一个字符串数组,然后C++
层接收到后,获取这个字符串数组的第一个字符串,并打印出来。
定义JNI
接口
external fun callNativeStringArray(array:Array<String>)
实现C++
方法,这里因为是字符串数组,JNI中没有相对应的类型,所以需要先通过env->GetObjectArrayElement()
获取到Object
数组中的第一个索引的Object
,再将其强转为jstring
类型。
如果是JNI
有对应类型的,按直接调用相关API
就可以了,比如env->GetIntArrayElements()
、env->GetFloatArrayElements()
extern "C"
JNIEXPORT void JNICALL
Java_com_heiko_myopencvtest2023_NativeLib_callNativeStringArray(JNIEnv *env, jobject thiz,
jobjectArray array) {
int len = env->GetArrayLength(array);
LOGD("len:%d",len);
//env->GetIntArrayElements() //获取Int数组
//env->GetFloatArrayElements() //获得Float数组
//env->GetObjectArrayElement() //获得JNI数组
jstring result = static_cast<jstring>(env->GetObjectArrayElement(array, 0)); //获取index为0的值
const char * str = env->GetStringUTFChars(result,NULL);
LOGD("text[0]:%s",str);
env->ReleaseStringUTFChars(result,str);
}
static_cast
是进行类型的强转
进行调用
val array = arrayOf("ABC", "DEF", "GHI", "JKL", "MNO")
nativeLib.callNativeStringArray(array)
执行结果
13:27:06.865 D len:5
13:27:06.865 D text[0]:ABC
这里我们以镜像Bitmap
图片为例,传递Bitmap
图片到JNI
层,然后进行镜像操作,并将镜像后的Bitmap
图片返回给Java
层
调用AndroidBitmap_getInfo()
,用来获取Bitmap的信息。
AndroidBitmapInfo bitmapInfo;
AndroidBitmap_getInfo(env, bitmap, &bitmapInfo);
调用AndroidBitmap_lockPixels()
,用来获取Bitmap
的像素内容。
同时,记得需要调用AndroidBitmap_unlockPixels()
来释放资源,这两个API
是配对使用的。
//拿到像素内容
void *bitmapPixels;
AndroidBitmap_lockPixels(env, bitmap, &bitmapPixels);
//释放资源
AndroidBitmap_unlockPixels(env, bitmap);
直接复制这个封装好的方法,进行调用就好
jobject generateBitmap(JNIEnv *env, uint32_t width, uint32_t height) {
// 获取Bitmap类引用
jclass bitmapCls = env->FindClass("android/graphics/Bitmap");
// 获取Bitmap构造方法的引用
jmethodID createBitmapFunction = env->GetStaticMethodID(bitmapCls, "createBitmap",
"(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
jstring configName = env->NewStringUTF("ARGB_8888");
jclass bitmapConfigClass = env->FindClass("android/graphics/Bitmap$Config");
jmethodID valueOfBitmapConfigFunction = env->GetStaticMethodID(
bitmapConfigClass, "valueOf", "(Ljava/lang/String;)Landroid/graphics/Bitmap$Config;"
);
jobject bitmapConfig = env->CallStaticObjectMethod(bitmapConfigClass,
valueOfBitmapConfigFunction, configName);
jobject newBitmap = env->CallStaticObjectMethod(bitmapCls, createBitmapFunction, width, height,
bitmapConfig);
return newBitmap;
}
定义JNI
external fun mirrorBitmap(bitmap: Bitmap) : Bitmap
实现C++
代码
extern "C"
JNIEXPORT jobject JNICALL
Java_com_heiko_myncnnlib_NcnnNativeLib_mirrorBitmap(JNIEnv *env, jobject thiz, jobject bitmap) {
AndroidBitmapInfo bitmapInfo;
AndroidBitmap_getInfo(env, bitmap, &bitmapInfo);
__android_log_print(ANDROID_LOG_DEBUG, "jniBitmap", "width:%d,height:%d", bitmapInfo.width,
bitmapInfo.height);
//拿到像素内容
void *bitmapPixels;
AndroidBitmap_lockPixels(env, bitmap, &bitmapPixels);
uint32_t newWidth = bitmapInfo.width;
uint32_t newHeight = bitmapInfo.height;
uint32_t *newBitmapPixels = new uint32_t[newWidth * newHeight];
int index = 0;
//遍历Bitmap像素,将左右的像素进行互换 (镜像操作)
for (int y = 0; y < newHeight; y++) {
for (int x = newWidth - 1; x >= 0; x--) {
uint32_t pixel = ((uint32_t *) bitmapPixels)[index++];
newBitmapPixels[newWidth * y + x] = pixel;
}
}
AndroidBitmap_unlockPixels(env, bitmap);
//生成新的Bitmap
jobject newBitmap = generateBitmap(env, newWidth, newHeight);
void *resultBitmapPixels;
AndroidBitmap_lockPixels(env, newBitmap, &resultBitmapPixels);
//拷贝
memcpy((uint32_t *)resultBitmapPixels, newBitmapPixels, sizeof(uint32_t) * newWidth * newHeight);
AndroidBitmap_unlockPixels(env,newBitmap);
delete [] newBitmapPixels;
return newBitmap;
}
进行调用
var bitmap = BitmapFactory.decodeResource(resources,R.drawable.img_test)
binding.img1.setImageBitmap(bitmap)
binding.btnMirrorImage.setOnClickListener {
bitmap = nativeLib.mirrorBitmap(bitmap)
binding.img1.setImageBitmap(bitmap)
}
进行程序,点击Button
,可以发现图片执行了镜像操作。
关于CMake
可以看我的另一篇博客 : Android NDK CMakeLists.txt 常用命令说明
感谢 Android CMake以及NDK实践基础