初识 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 ~

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

推荐阅读更多精彩内容

  • 什么是JNI? JNI 是java本地开发接口.JNI 是一个协议,这个协议用来沟通java代码和外部的本地代码(...
    a_tomcat阅读 2,768评论 0 54
  • JNI Java JNI的本意是Java Native Interface(Java本地接口),它是为了方便Jav...
    帝王鲨kingcp阅读 222评论 1 1
  • 0.要素1.类操作2.异常操作3.全局及局部引用4.对象操作5.字符串操作6.数组操作7.访问对象的属性和方法7....
    MagicalGuy阅读 1,278评论 0 2
  • 移步系列Android跨进程通信IPC系列 1 相关代码 1.1 代码位置 1.2 代码链接 AndroidRun...
    凯玲之恋阅读 1,533评论 0 7
  • 宝宝刚刚长出的乳牙是十分脆弱的,常使用奶瓶喝奶或喜欢含着奶嘴的宝宝,如果没有做好充足的口腔护理,那么食物中的糖会与...
    856e773a73d2阅读 264评论 0 2