音视频开发系列(42) JNI与NDK的学习和使用

一、什么是JNI、NDK?

JNI:Java Native Interface(java本地接口),使得Java与本地语言(
C、CPP)相互调用

NDK:Native Development Kit,是Android的一个工具开发包,帮助开发者快速开发C、CPP动态库,自动将动态库打包进入APK。

通过JNI实现Java和Native的交互,在Android上通过NDK实现JNI的功能。

二、Java和Native交互流程

JNI

  1. 在Java类中通过native关键字声明Native方法

  2. javac命令编译Java类得到class文件

  3. 通过javah命令(javah -jni class名称)导出JNI的头文件(.h文件)

  4. 实现native方法

  5. 编译生成动态库(.so文件)

  6. 实现Java和C、CPP的相互调用

NDK

  1. 配置NDK环境、创建Natvie CPP项目)

  2. 在Java类中通过native关键字声明Native方法

  3. 自动生成native方法,实现native方法

  4. 通过ndk-build或者cmake编译产生动态库

  5. 实现Java和C、CPP的相互调用

三、通过AS创建Native CPP简单的项目

1. 如何配置NDK环境

SDK Manager —》SDK Tools中下载选中NDK,LLDB和CMake。
其中NDK是Native开发工具包,
LLDB是调试Native代码用
CMake是编译工具

配置好环境后,我们就可以开始创建Native CPP项目了

2. 通过AS创建CPP项目

AS New Project 选中 Native CPP项目。即可自动创建一个demo项目。

Java通过调用native方法stringFromJNI获取一个字符串。

//Java 代码
public class MainActivity extends AppCompatActivity {

    ...
    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();

    public native static String stringFromJNIStatic();
}

//JNI 代码
#include <jni.h>
#include <string>

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

extern "C"
JNIEXPORT jstring JNICALL
Java_com_av_mediajourney_MainActivity_stringFromJNIStatic(JNIEnv *env, jclass clazz) {

}

代码比较简单,但是麻雀虽小,五脏俱全。我们看到这个小小的JNI方法可能会有以下疑问。

  1. stringFromJNI生成的关联的Native方法名称为什么是Java_com_av_mediajourney_MainActivity_stringFromJNI?可以是其他的吗?

  2. JNI方法的参数 JNIEnv* 和 jobject代码什么意思?*

  3. Java需要的是String类型,为什么JNI返回的是一个jstring类型?

  4. extern "C" 是什么意思?

  5. JNIEXPORT和JNICALL又是什么意思?

这涉及到JNI的基本知识,我们通过对JNI基本知识的学习来解决上面的疑惑。

四、JNI基本知识

本小节分如下内容

  1. JNIEnv和jobject jclass

  2. Java 语言中的数据类型是如何映射到 c/cpp本地语言中的

  3. java的属性和方法在JNI 签名

4.1 JNIEnv 和 jobject 、 jclass

JNIEnv:是指线程上下文环境,每个线程有且只有一个JNIEnv实例
JNIEnv 结构包括 JNI 函数表

图片来源于:《JNI编程指南》

第二个参数的意义取决于该方法是静态还是实例方法(static or an instance method)。当本地方法作为一个实例方法时,第二个参数相当于对象本身,即 this. 当本地方法作为一个静态方法时,指向所在类.

关注+后台私信我,领取2022最新最全学习提升资料+面试题+学习视频,内容包括(C/C++,Linux,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)等等

 

4.2 Java 语言中的数据类型是如何映射到 c/cpp本地语言中的

在 Java 中有两类数据类型:基本数据类型,如,boolean,int, float, char;另一种为引用数据类型,如,类,实例,数组。

Java和JNI的基本数据类型映射关系如下

相比基本类型,对象类型的传递要复杂很多。Java 层对象作为指针传递到 JNI 层,它指向 JavaVM 内部数据结构。使用这种指针的目的是:不希望 JNI 用户了解 JavaVM 内部数据结构。对引用类型指针所指结构的操作,都要通过 JNI 方法进行,比如,"java.lang.String"对象,JNI 层对应的类型为 jstring,对该 类型 的操作要通过 JNIEnv-> NewStringUTF 进行。

针对字符串对象
通过字符串拼接来展示

//Java代码新增字符串拼接的native方法

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        tv.setText(stringFromJNI()+appendString("hengheng","hahah"));
    }

    ...

    //新加字符串拼接方法
    public native String appendString(String str1,String str2);
}


//对应JNI的实现

extern "C"
JNIEXPORT jstring JNICALL
Java_com_av_mediajourney_MainActivity_appendString(JNIEnv *env, jobject thiz, jstring str1,
                                                   jstring str2) {
    //使用对应的 JNI 函数把 jstring 转成 C/C++字串
    //Unicode 以 16-bits 值编码;UTF-8 是一种以字节为单位变长格式的字符编码,并与 7-bits
    //ASCII 码兼容。UTF-8 字串与 C 字串一样,以 NULL('\0')做结束符
    //调用 GetStringUTFChars,把一个 Unicode 字串转成 UTF-8 格式字串
    const char *string1 = env->GetStringUTFChars(str1, NULL);

    //调用该函数会有内存分配操作,失败后,该函数返回 NULL,并抛 OutOfMemoryError 异常。
    if(string1 == NULL){
        return NULL;
    }
    const char *string2 = env->GetStringUTFChars(str2, NULL);
    if(string2 == NULL){
        return NULL;
    }

    //string:string是STL当中的一个容器,对其进行了封装,所以操作起来非常方便。
    //char*:char *是一个指针,可以指向一个字符串数组,至于这个数组可以在栈上分配,也可以在堆上分配,堆得话就要你手动释放了。
    std::string const cc = std::string(string1) + std::string(string2);

    //调用 ReleaseStringUTFChars 释放 GetStringUTFChars 中分配的内存(Unicode -> UTF-8转换的原因)。
    env->ReleaseStringUTFChars(str1,string1);
    env->ReleaseStringUTFChars(str2,string2);

    //使用 JNIEnv->NewStringUTF 构造 java.lang.String;
    return env->NewStringUTF(cc.c_str());

}

JNI String函数汇总表如下:

图片来源于:《JNI编程指南》

针对数组
通过求和int类型数组来展示

//java代码修改
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        int[] intarry = new int[10];
        for (int i = 0; i < 10; i++) {
            intarry[i] = i;
        }
        tv.setText(stringFromJNI()+appendString("hengheng","hahah")+sumArray(intarry));
    }

    ...

    public native int sumArray(int[] intarray);


}


//JNI实现

extern "C"
JNIEXPORT jint JNICALL
Java_com_av_mediajourney_MainActivity_sumArray(JNIEnv *env, jobject thiz, jintArray intarray) {

    jint sum;
    jsize length = env->GetArrayLength(intarray);

    //方案一 通过GetIntArrayRegion指定buf赋值范围
//    jint *buf ;
//
//    env->GetIntArrayRegion(intarray, 0, length, buf);
//    for (jint i = 0; i < length; ++i) {
//        sum += buf[i];
//    }

    //方案二:通过GetIntArrayElements和ReleaseIntArrayElements,
    //返回 Java 数组的一个拷贝(实现优良的VM,会返回指向 Java 数组的一个直接的指针,并标记该内存区域,不允许被 GC)。
    jint *pInt = env->GetIntArrayElements(intarray, NULL);
    for (jint i = 0; i < length; ++i) {
        sum += pInt[i];
    }
    env->ReleaseIntArrayElements(intarray,pInt,0);
    return sum;
}

JNI Array 函数汇总表如下:

图片来源于:《JNI编程指南》

针对对象(非String和数组的对象)和对象数组

通过FindClass 获取到jclass,这块内容会涉及到引用的类型,比如GlobalRef 和 LocalRef,我们在下一篇会详细学习实践。而对Java对象的描述又涉及到 java的属性和方法在JNI 签名相关知识,我们来一起学习下。

4.3 java的属性和方法在JNI 签名

我们这一小节,学习Java熟悉和方法在JNI的签名,实践从本地代码访问Java对象成员、调用 Java 方法。

签名的作用:为了准确描述一件事物.
Java Vm 定义了类签名,方法签名;其中方法签名是为了支持方法重载。

Java 语言支持两种成员(field):(static)静态成员和对象成员. 在 JNI 获取和赋值成员的方法是不同的. 同样的,方法也是两种方法: 静态方法和对象方法。

我们先看下了解下java的属性和方法在JNI 签名对应关系,然后通过通过native修改java成员值以及调用java方法为例对其进行了解熟悉。

其中要特别注意的是:

  1. 类描述符开头的'L'与结尾的';'必须要有

  2. 数组描述符,开头的'['必须有.

  3. 方法描述符规则: "(各参数描述符)返回值描述符",其中参数描述符间没有任何分隔 符号
    描述符很重要,请烂熟于心. 写 JNI,对于错误的签名一定要特别敏感,此时编译器帮不 上忙,执行 make 前仔细检查你的代码。

下面我们开始通过修改native修改java 成员变量和和调用方法修改修改java变量的值。

访问对象成员分三步,
1. 通过 GetObjectClass 从 obj 对象得到 cls.
2. 通过 GetFieldID 得到对象成员 ID, 如下:
fid = (*env)->GetFieldID(env, cls, "s", "Ljava/lang/String;");
3. 通过在对象上调用下述方法获得成员的值:
jstr = (*env)->GetObjectField(env, obj, fid);
此外 JNI 还提供Get/SetIntField,Get/SetFloatField 访问不同类型成员。

先来看下通过JNI访问修改java属性的例子

//Java 代码 定义两个成员变量:静态变量和实例变量

public class MainActivity extends AppCompatActivity {

   
    private String value = "123";
    private static String value_static = "321";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        tv.setText(stringFromJNI()+appendString("hengheng","hahah")+sumArray(intarry)
        +"\n"+"value="+value+" value_static="+value_static);
    }
}

//JNI层
void accessField(JNIEnv *pEnv, jobject pJobject);

extern "C" JNIEXPORT jstring JNICALL
Java_com_av_mediajourney_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject obj) {
    ...

    accessField(env,obj);

    return tmp;

}

void accessField(JNIEnv *env, jobject obj) {
    
    //1. 获取class
    jclass jclazz = env->GetObjectClass(obj);
    
    //2. 通过GetFieldID获取fieldid
    // 签名一定要熟悉,否则在运行时直接会导致崩溃。比如String对应签名时"Ljava/lang/String;"
    jfieldID fieldId = env->GetFieldID(jclazz, "value", "Ljava/lang/String;");


    jstring jst = static_cast<jstring>(env->GetObjectField(obj, fieldId));

    jst = env->NewStringUTF("456");

    //3. 通过SetObjectField,修改fieldId的值
    env->SetObjectField(obj, fieldId, jst);

    
    
    ///下面来修改静态成员

    //1. 通过GetStaticFieldID获取静态成员fieledid
    jfieldID fielId2 = env->GetStaticFieldID(jclazz, "value_static", "Ljava/lang/String;");

    jstring jst2 = env->NewStringUTF("789");
    //2. 通过SetStaticObjectField给静态成员变量赋值
    env->SetStaticObjectField(jclazz,fielId2,jst2);
    
}

接着我们来看下 JNI调用Java的方法

JNI访问Java方法的步骤:
1.通过 GetMethodID 在给定类中查询方法. 查询基于方法名称和签名
2.本地方法调用 Call<Return Value Type>Method

方法签名由各参数类型签名和返回值签名构成. 参数签名在前,并用小括号括
起.

public class MainActivity extends AppCompatActivity {

    ...

    //定义实例方法
    private void setValue(String value) {
        this.value = value;
    }

    //定义静态方法
    private static void setValue_static(String value_static) {
        MainActivity.value_static = value_static;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        tv.setText(stringFromJNI()+appendString("hengheng","hahah")+sumArray(intarry)
        +"\n"+"value="+value+" value_static="+value_static);
    }
}


//JNI实现

void accessMethod(JNIEnv *env, jobject obj) {
    jclass clazz = env->GetObjectClass(obj);

    jmethodID methodId = env->GetMethodID(clazz, "setValue", "(Ljava/lang/String;)V");
    if (methodId == NULL) {
        return;
    }
    //这里一定要注意 不能直接env->CallVoidMethod(obj,methodId,"set by native method");
    //JNI中对象类型的使用一定用通过env方法来操作,比如生成string:env->NewStringUTF
    jstring jst = env->NewStringUTF("set by native method");
    env->CallVoidMethod(obj,methodId,jst);
    
    
    //调用静态方法
    jmethodID methodId1= env->GetStaticMethodID(clazz, "setValue_static", "(Ljava/lang/String;)V");
    if(methodId1== NULL){
        return;
    }
    jstring jst1 = env->NewStringUTF("\n set by native method staic");

    env->CallStaticVoidMethod(clazz,methodId1,jst1);

}

这里遇到了一个问题,折腾了好一阵。问题是:
使用 env->CallVoidMethod(obj,methodId, "set by native method");运行时报错
JNI DETECTED ERROR IN APPLICATION: use of deleted global reference 0x71749eae2e

检查签名没有错,通过java -p class 也确认了签名的正确性,但是为什么报错呐?
[How can I call Java Methods containing String parameter(s) using JNI?]: http://www.jguru.com/faq/view.jsp?EID=226786

看到了正确的用法,突然意识到在JNI中java的String不是基本数据类型,数据的生成要通过env对应的方法获取。
改为

jstring jst = env->NewStringUTF("set by native method");
env->CallVoidMethod(obj,methodId,jst);

另外还可以通过 JNI 调用 Java 类的构造方法和父类的方法

4.4 性能优化

执行一个 Java/native 调用要比 Java/Java 调用慢 2-3 倍. 也可能有一些 VM 实现,Java/native 调用性能与 Java/Java 相当。(此种虚拟机,Java/native 使用 Java/Java相同的调用约定)。
native/Java 调用效率可能与 Java/Java 有 10 倍的差距,因为 VM 一般不会做 Callback 的优化。

通过 FindClass 、GetFieldID、GetMethodID 去找到对应的信息是很耗时的,如果方法被频繁调用,那么肯定不能每次都去查找对应的信息,有必要将它们缓存起来,在下一次调用时,直接使用缓存内容就好了

可以在项目中加一套 Hash 表, 封装 FindClass,GetMethodID,GetFieldID等函数,查询的所有操作,都对 Hash 表操作,如首次 FindClass 一个类,这时可以把一个类的所有成员缓存到 Hash 表中,用名字+签名做键值。
引入了这个优化,项目的执行效率有 100 倍的提高;

  1. 用一个 Hash 表,还是每个类一个 Hash 表

  2. 首次 FindClass 类时,一次缓存所有的成员,还是用时缓存
    最终做的选择是:为了降低冲突,每个类一个 Hash 表,并且一次缓存一个类的所有成员。

五、Java和Native的相互调用

上面两个小节中在对Java 语言中的数据类型是如何映射到 c/cpp本地语言中的
以及 java的属性和方法在JNI 签名的学习实践已经充分的展示了相互调用。如果还有不清晰,请回看上一小节。

这里我们再来回顾下,这篇的目标 以及疑惑

JNI方法的参数 JNIEnv 和 jobject代码什么意思
Java中需要的是String类型,为什么JNI返回的是一个jstring类型?
—》这个两个问题,相信通过上面的学习实践,已经有很好的理解

我们再来其他几个问题
stringFromJNI生成的关联的Native方法名称为什么是Java_com_av_mediajourney_MainActivity_stringFromJNI?可以是其他的吗?

JNI的方法名称是根据java的全包名+类名,并且把”.”替换为”_”,为规则生成的。这是静态注册的方式,当然也有动态注册的方式,这个我们下一篇再来详细学习实践。

extern "C" 是什么意思?
extern “C”的作用是避免编译器按照CPP的方式编译C函数
C语言不支持函数的重载,编译之后函数名称不变
CPP支持函数的重载,编译之后函数名称会发生变化。调用的时候导致找不到JNI的实现

JNIEXPORT和JNICALL又是什么意思?

在jni.h中可以看到这两个宏的定义
//JNIEXPORT表示 该函数是否可以导出
#define JNIEXPORT  __attribute__ ((visibility ("default")))
//调用规范
#define JNICALL

收获

通过对JNI和NDK的学习实践,

  1. 了解了JNI和NDK是什么,以及两者之间的关系;

  2. Android如何配置进行NDK的开发

  3. JNI基本知识介绍(JNIEnv、数据类型对应关系、属性和方法签名等)

  4. 实现Android中Java和Native的相互调用

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值