初识 JNI
JNI
作为 Java/Kotlin
(原生端) 同 C/C++
端交互的工具,是学习 ffmpeg
的一个前提,这边做一个学习过程中的记录。通过 Android Studio
可以快速创建一个 JNI
项目(创建时候选择 Native C++
即可,会自动配置 CMakeList
等文件),该文基于 AS 3.5
loadLiabry
src
文件夹下相比一般的 AS
项目多了 cpp
文件夹,该文件夹下有一个 .cpp
文件和 CMakeLists.txt
文件,.cpp
文件用来写 native
端实现的方法,CMakeLists
用来做些 cpp
的配置,目前可以忽略
main
│ AndroidManifest.xml
├─cpp
│ native-lib.cpp
│ CMakeLists.txt
├─java
│
├─res
接着在 MainActivity
中有这么一行代码
companion object {
init {
System.loadLibrary("native-lib")
}
}
通过 loadLibrary
方法,加载编译的链接 so
库,so
库的源码就是前面提到的 native-lib.cpp
文件了
原生调用 cpp 方法
那么在 Kotlin
中如何调用 cpp
的方法呢,可以看到 MainActivity
中有一个使用 external
修饰的方法(如果是 java
则使用 native
关键词修饰)
external fun stringFromJNI(): String
通过该方法,会去调用 cpp
层的 native
方法,可以看下 native-lib.cpp
文件,内部定义了一个方法
extern "C" JNIEXPORT jstring JNICALL
Java_com_xxx_MainActivity_stringFromJNI(JNIEnv *env, jobject/*this*/) {
std:string hello = "Hello from c++";
return env->NewStringUTF(hello.c_str());
}
可以看到该方法的命名方式为 Java_包名_类名_方法名
(包名的 .
替换成 _
即可),通过这种命名方式来查找 Kotlin
层的调用方法,该方法中 extern "C"
的作用是让 C++
支持 C
的方法,JNIEXPORT xxx JNICALL
代表这是一个 JNI
方法,xxx
表示返回的方法类型,在 JNI
中,都有 Kotlin
对应的数据类型
JNI 数据类型
JNI
对应 Java
的数据类型如下,也可以直接查看 jni.h
头文件
JNI类型 | Java类型 | 类型描述 |
---|---|---|
jboolean | boolean | 无符号8位 |
jbyte | byte | 无符号8位 |
jchar | char | 无符号16位 |
jshort | short | 有符号16位 |
jint | int | 有符号32位 |
jlong | long | 有符号64位 |
jfloat | float | 有符号32位 |
jdouble | double | 有符号64位 |
因为 String
不属于基本类型,所以不定义在这,需要返回 jsrting
类型,只能通过 char *
进行相应的转换,所以上述的函数中,使用 env->NewStringUTF(hello.c_str())
方法,生成 jstring
并返回,然后在 Kotlin
层通过调用 stringFromJNI
方法就可以将 native
层返回的字符串显示出来,JNI
的基本使用就这么多啦,接着通过一些使用,熟悉一些方法,比如实现字符串的拼接
external fun stringCat(a: String, b: String): String
回到 c++
层做具体的实现,前面提到因为在 C++
中字符串拼接不能直接通过 jstring
相加实现,需要通过 char *
进行拼接,所以就需要封装一个 jstring2Char
的方法进行转换
char *jstring2Char(JNIEnv *env, jstring jstr) {
char *rtn = nullptr;
jclass clazz = env->FindClass("java/lang/String");
jstring strenCode = env->NewStringUTF("UTF-8");
jmethodID mid = env->GetMethodID(clazz, "getBytes", "(Ljava/lang/String;)[B");
auto barr = (jbyteArray) (env->CallObjectMethod(jstr, mid, strenCode));
jsize alen = env->GetArrayLength(barr);
jbyte *ba = env->GetByteArrayElements(barr, JNI_FALSE);
if (alen > 0) {
// malloc(bytes) 方法分配 bytes 字节,并返回这块内存的指针,
// malloc 分配的内存记得使用 free 进行释放,否则会内存泄漏
rtn = static_cast<char *>(malloc(static_cast<size_t>(alen + 1)));
// memcpy(void*dest, const void *src, size_t n)
// 由 src 指向地址为起始地址的连续 n 个字节的数据复制到以 destin 指向地址为起始地址的空间内
memcpy(rtn, ba, static_cast<size_t>(alen));
rtn[alen] = 0;
}
env->ReleaseByteArrayElements(barr, ba, 0);
return rtn;
}
定义完转换方法,直接调用即可,记得释放内存
extern "C" JNIEXPORT jstring JNICALL
Java_com_xxx_MainActivity_stringCat(JNIEnv *env, jobject, jstring a, jstring b){
char *first = jstring2Char(env, a);
char *second = jstring2Char(env, b);
std::strcat(first, second);
free(first);
free(second);
return env->NewStringUTF(first);
}
静态 JNI 方法
在很多情况下,都不会将 JNI
方法直接定义在 Activity
,而是封装到公共方法中,方便调用,那么在公共方法类调用除了通过该类的实例,调用相应方法,还有就是设置该方法为静态方法,那么这种情况和上述有啥区别呢,其实区别不是很大,只需要将 native
端的方法中的参数 jobject
替换成 jclass
即可,但是在 Kotlin
端,除了在半生对象中声明该 native
方法,还需要增加 JvmStatic
注解才行,例如有如下的一个方法
class JniUtils {
companion object {
@JvmStatic
external fun plus(a: Int, b: Int): Int
}
}
那么在 native
端生成 JNI
方法和前面提到的类似,只需替换参数类型即可
extern "C" JNIEXPORT jint JNICALL
Java_com_xxx_JniUtils_plus(JNIEnv *env, jclass, jint , jint b){
return a + b;
}
C++ 调用 Kotlin 方法
前面介绍了如何在 Kotlin
中调用 native
方法,当然,在 c++
层也可以调用 Kotlin
层的方法。假设在 MainActivity
中有一个 callMe(message: String)
和 call(message:String)
方法,在调用 call
的时候,同时内部调用 callMe
方法,当然直接调用很简单,这边通过 JNI
来实现
fun callMe(message: String){
Log.e(TAG, message) // 只做简单的打印
}
external fun call(message: String)
native
实现 call
方法上面已经介绍了,接下来介绍在 JNI
内部调用 callMe
方法
extern "C" JNIEXPORT void JNICALL
Java_com_xxx_MainActivity_call(JNIEnv *env, jobject instance, jstring msg){
const char *methodName = "callMe"; // 指定需要调用的方法名
jclass clazz = env->FindClass("com.xxx.MainActivity"); //查找对应的类,指定对应的包名和类
// 根据所在类和方法名查找方法的 ID,最后一个参数为方法的签名,稍后做解释
jmethodID mid = env->GetMethodId(clazz, methodName, "(Ljava/lang/String;)V");
env->CallVoidMethod(instance, mid, msg); // 根据返回的类型,调用方法,传入相应参数
}
当 Kotlin
层调用 call
方法的时候,就会通过 JNI
调用 callMe
方法,执行 callMe
的内部逻辑。在上面提到了「签名」这个东西,这边列出签名的表示方法
类型 | 签名 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void | V |
数组 | [ |
String/Object | Ljava/lang/String; Ljava/lang/Object; |
普通类(com.example.className) | Lcom/example/className; |
嵌套类(com.example.className.Inner) | Lcom/example/className$Inner; |
所以方法的签名的规则就是根据传入的参数类型和返回的类型,替换成相应的签名即可,例如:call(Student s, int a): String
方法的签名为 (Lcom/xxx/Student;I)Ljava/lang/String;
如果是内部类则使用 $ 表示嵌套
C++ 获取 Kotlin 的内部参数
假设我们在 MainActivity
有个私有参数 name
,如果外部有个类需要获取这个参数,可以通过 MainActivty
内部的共有方法来获取,假如没有这个共有方法该咋办呢,当然我们可以通过 JNI
来获取
extern "C" JNIEXPORT jstring JNICALL
Java_com_xxx_MainActivity_getField(JNIEnv *env, jobjcet instance){
jclass clazz = env->FindClass("com.xxx.MainActivity"); // 根据类的包名来查找相应的类
// 根据类和参数名来获取该参数,第三个参数为参数的签名,即类型在 JNI 对应的签名
jfieldID fid = env->GetFieldID(clazz, "name", "Ljava/lang/String;");
// 因为 String 不是基本类型,所以只能通过 GetObjectField 进行获取,然后进行强转
// 如果是 int 等基本类型,提供了 GetIntField 等获取方法,auto 为可自行根据结果判断类型
auto name = (jstring)(env->GetObjectField(instance, fid));
return name;
}
当在外部通过 getField
方法即可获取到该私有属性,这个例子仅为例子而已...
C++ 获取普通类的参数信息
假设我们有一个类,例如 Student
里面有一些名字,年龄等属性,然后通过 JNI
将这些属性转成 String
返回,那么就需要涉及到获取参数的字段信息了
// 定义一个普通类 Student
data class Student(val firstName: String, val lastName: String, val age: Int)
// 在 MAinActivity 定义一个转换的方法
external fun printStudent(Student student): String
那么在 C++
层就需要将 student
内部的信息都获取出来,并拼接到字符串,然后返回
extern "C" JNIEXPORT jstring JNICALL
Java_com_xxx_MainActivity_printStudent(JNIEnv *env, jobject, jobject student){
jcalss clazz = env->GetObjectClass(student); // 获取传入参数对应的类
// 通过参数名和签名,去对应的 class 获取相应的 FieldID,
// 然后根据 FiedlID 通过 GetObjectField 方法获取对应的属性
auto firstName = (jstring)(env->GetObjectField(student, env->GetFieldID(clazz, "firstName", "Ljava/lang/String;")));
auto lastName = (jstring)(env->GetObjectField(student, env->GetFieldID(clazz, "lastName", "Ljava/lang/String;")));
// int 为基本类型,可直接通过获取对应类型属性的方法获取
auto age = env->GetIntField(student, env->GetFieldID(clazz, "age", "I"));
char *cFirstName = jstring2Char(firstName);
char *cLastName = jstring2Char(lastName);
std::string cAge = std::to_string(age);
strcat(cFirstName, " ");
strcat(cFirstName, cLastName);
strcat(cFirstName, " is ");
strcat(cFirstName, cAge.c_str());
strcat(cFirstName, " years old");
free(cFirstName);
free(cLastName);
return env->NewStringUTF(cFirstName);
}
当外部调用 printStudent
方法的时候就会将 student
的属性打印出来
动态注册
在前面的 JNI
方法中,每个方法都需要写很长的一段类名,非常容易出错,那么能不能省略包名呢,当然是可以,通过动态注册就可以让这个麻烦的方法名变得简略
动态注册,需要指定一个方法列表,用来存放同个包名下的方法,存放的方式如下:
{ Kotlin 层方法名, 方法前面, JNI 函数指针} // 函数指针固定为 ```(void *) JNI 方法名```
例如我们前面提到的方法,放到一个列表中
static JNINativeMethod jniMethods[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void *) stringFromJNI},
{"stringCat", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", (void *) stringCat},
{"call", "(Ljava/lang/String;)V", (void *) call},
{"getField", "()Ljava/lang/String;", (void *) getField},
{"printStudent", "(Lcom/xxx/Student;)Ljava/lang/String;", (void *) printStudent},
};
接着就是需要注册这些方法了,封装一个通用的方法,注册成功返回 JNI_TRUE
否则 JNI_FALSE
static int registerNativeMethods(JNIEnv *env, const char *className,
JNINativeMethod *getMethods, int sumNum){
jclass clazz = env->FindClass(className); // 根据类名去查找相应类,包含 JNINativeMethod 列表所有方法
if (clazz == nullptr) return JNI_FALSE; // 未找到 class 则认为注册失败
// 根据所有的方法名和数量进行注册,如果结果返回小于 0 则认为注册失败
if (env->RegisterNatives(clazz, getMethods, methodSum) < 0) return JNI_FALSE;
return JNI_TRUE;
}
接着就需要实现 JNI_OnLoad
方法(定义在 jni.h
头文件中),对上述的方法进行注册,该方法会返回一个版本号
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reversed) {
JNIEnv *env = nullptr;
// 检测环境失败返回 -1
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
assert(env != nullptr);
// 注册失败返回 -1
if (!registerNativeMethods(
env, jniClazz, jniMethods, sizeof(jniMethods) / sizeof(jniMethods[0]))) {
return -1;
}
return JNI_VERSION_1_6;
}
这样几步就完成了 JNI
方法的动态注册,只需要全局定义 className
即可,不需要每次都在方法声明完整包路径
内存释放
在 C++
中,非常重要的一步就是内存释放,否则就会造成内存泄漏,分分钟给你炸开
哪些需要手动释放
- 不需要手动释放(基本类型):jint,jlong 等等
- 需要手动释放(引用类型,数组家族):jstring,jobject ,jobjectArray,jintArray ,jclass ,jmethodID
释放方法(该部分参考自《JNI手动释放内存》)
-
jstring & char *
// 创建 jstring 和 char* jstring jstr = (jstring)(jniEnv->CallObjectMethod(jniEnv, mPerson, getName)); char* cstr = (char*) jniEnv->GetStringUTFChars(jniEnv,jstr, 0); // 释放 jniEnv->ReleaseStringUTFChars(jniEnv, jstr, cstr); jniEnv->DeleteLocalRef(jniEnv, jstr);jbyteArray audioArray = jnienv->NewByteArray(frameSize); jnienv->DeleteLocalRef(audioArray)
-
jobject,jobjectArray,jclass ,jmethodID 等引用类型
jniEnv->DeleteLocalRef(jniEnv, XXX);
-
jbyteArray
jbyteArray arr = jnienv->NewByteArray(frameSize); jnienv->DeleteLocalRef(arr);
-
GetByteArrayElements
jbyte* array= jniEnv->GetByteArrayElements(env,jarray,&isCopy); jniEnv->ReleaseByteArrayElements(env,jarray,array,0);
-
NewGlobalRef
jobject ref= env->NewGlobalRef(customObj); env->DeleteGlobalRef(customObj);
举个例子
在 Android
中,经常需要用到 Context
获取一些相关的信息,这边举个获取屏幕信息的例子
#include <jni.h>
#include <string>
#include <iostream>
#include <android/log.h>
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "JNI", __VA_ARGS__)
// 获取当前的 Context
jobject getAndroidApplication(JNIEnv *env) {
jclass activityThreadClazz = env->FindClass("android/app/ActivityThread");
jmethodID jCurrentActivityThread =
env->GetStaticMethodID(activityThreadClazz,
"currentActivityThread", "()Landroid/app/ActivityThread;");
jobject currentActivityThread =
env->CallStaticObjectMethod(activityThreadClazz, jCurrentActivityThread);
jmethodID jGetApplication =
env->GetMethodID(activityThreadClazz, "getApplication", "()Landroid/app/Application;");
return env->CallObjectMethod(currentActivityThread, jGetApplication);
}
extern "C" JNIEXPORT void JNICALL
Java_com_demo_kuky_jniwidth_MainActivity_jniDensity(JNIEnv *env, jobject) {
jobject instance = getAndroidApplication(env);
jclass contextClazz = env->GetObjectClass(instance);
// 获取 `getResources` 方法
jmethodID getResources = env->GetMethodID(contextClazz, "getResources",
"()Landroid/content/res/Resources;");
jobject resourceInstance = env->CallObjectMethod(instance, getResources);
jclass resourceClazz = env->GetObjectClass(resourceInstance);
// 获取 Resources 下的 `getDisplayMetrics` 方法
jmethodID getDisplayMetrics = env->GetMethodID(resourceClazz, "getDisplayMetrics",
"()Landroid/util/DisplayMetrics;");
jobject metricsInstance = env->CallObjectMethod(resourceInstance, getDisplayMetrics);
jclass metricsClazz = env->GetObjectClass(metricsInstance);
// 获取 DisplayMetrics 下的一些参数
jfieldID densityId = env->GetFieldID(metricsClazz, "density", "F");
jfloat density = env->GetFloatField(metricsInstance, densityId);
jfieldID widthId = env->GetFieldID(metricsClazz, "widthPixels", "I");
jint width = env->GetIntField(metricsInstance, widthId);
jfieldID heightId = env->GetFieldID(metricsClazz, "heightPixels", "I");
jint height = env->GetIntField(metricsInstance, heightId);
LOGE("get density: %f, width: %d, height: %d", density, width, height);
}
目前使用到的就那么多啦,后面有更多的方法涉及到,再进行添加,Enjoy it ~