Android音视频开发实战02-Jni

一 JNI

1.1 什么是JNI

JNI是Java Native Interface的缩写,是Java提供的一种机制,用于在Java代码中调用本地(C/C++)代码。它允许Java代码与本地代码进行交互,通过JNI,Java应用程序可以调用一些原生库或者操作系统API,以获取更好的性能和更强的功能支持。

使用JNI需要编写一些Native方法,并将其实现在本地代码(如C/C++)中。这些本地方法可以直接从Java代码中调用,从而获得更高的性能和更灵活的控制权。通常情况下,为了方便和可维护性,我们会将所有本地方法的实现都封装到一个动态链接库文件中,然后在Java代码中加载并调用其中的函数。

JNI为Java程序员提供了一种无缝集成本地代码的方式,使得Java应用程序可以更加高效地利用系统资源,从而提升性能和扩展性。

1.2 JNI的优劣势

JNI的优势:

  1. 快速的执行效率:通过JNI,可以将Java应用程序与本地代码结合起来,从而获得更高的执行效率。因为本地代码是通过C/C++等编程语言编写的,这些语言通常比Java更接近硬件和操作系统,因此本地代码在执行时可以更加快速和高效。

  2. 可以访问底层系统资源:通过JNI,Java应用程序可以直接调用操作系统的API,从而获得对系统底层资源(如文件、网络、内存等)的直接访问能力。这使得Java程序员可以实现更加灵活和复杂的功能,同时也可以提高程序的运行效率。

  3. 跨平台性:虽然本地代码必须针对每个平台进行编译,但是一旦编译完成,它们就可以在任何支持JNI的平台上运行。这意味着开发人员可以使用最适合自己的工具和环境来编写本地代码,而不需要考虑跨平台问题。

JNI的劣势:

  1. 复杂度较高:JNI需要开发人员熟悉Java和本地代码两种编程语言,并且需要掌握JNI规范及其相关的工具和技术。这使得JNI的学习曲线较陡峭,初学者可能需要花费较长时间才能掌握。

  2. 可移植性差:尽管JNI可以跨平台运行,但是在不同的平台上,本地代码必须重新编译和链接,这可能会带来一些兼容性问题。此外,由于Java虚拟机(JVM)的不同实现可能存在差异,因此在某些情况下,JNI代码也可能无法在不同的JVM上正确运行。

  3. 方法调用成本高,相对于Java调用Java代码,Java调用Jni的性能开销会更大一些,因此只有在执行特别耗时的代码的时候才会使用Jni,用C++代码的执行效率来覆盖掉方法调用的性能开销
    4.数据类型不通用,C/C++的数据类型与Java并不通用,在相互调用的时候需要做转化
    5.C/C++调用Java代码很复杂,很不方便

1.3 JNI常见的数据类型

JNI支持的数据类型可以分为两类:基本类型和引用类型。

基本类型包括:

  • jboolean:布尔类型,取值范围是true或false。
  • jbyte:字节类型,取值范围是-128到127。
  • jchar:字符类型,取值范围是0到65535。(Java中的char类型被映射为C语言的unsigned short类型)
  • jshort:短整型,取值范围是-32768到32767。
  • jint:整型,取值范围与平台相关,通常是32位有符号整数。
  • jlong:长整型,取值范围与平台相关,通常是64位有符号整数。
  • jfloat:单精度浮点型,取值范围约为1.4E-45到3.4E+38。
  • jdouble:双精度浮点型,取值范围约为4.9E-324到1.8E+308。

引用类型包括:

  • jobject:表示一个Java对象的引用。
  • jclass:表示一个Java类的引用。
  • jstring:表示一个Java字符串的引用。
  • jarray:表示一个Java数组的引用。
  • jthrowable:表示一个Java异常的引用。

基本数据类型是不需要手动释放,但是和Java不一样的是引用类型是需要手动调用释放的

二 JNI常见的数据处理方式

2.1 基本数据处理方式

基本数据在Java Jni C/C++之间差别不大,详见下面的对应关系:

Java JNI C/C++ 占用内存大小 备注
boolean jboolean uint8_t unsigned 8 bits
byte jbyte int8_t signed 8 bits
char jchar uint16_t unsigned 16 bits
short jshort int16_t signed 16 bits
int jint int32_t signed 32 bits
long jlong int64_t signed 64 bits 在Native是用int64_t去承接long,不要用Native自身的long类型去承接
float jfloat float 32-bit IEEE 754 IEEE 754是一种标准,由IEEE制定
double jdouble double 64-bit IEEE 754

2.2 ​String相互传递

2.2.1获取Java的Sting

extern "C"
JNIEXPORT void JNICALL
Java_com_luoye_bzmedia_BZMedia_testString(JNIEnv *env, jclass clazz, jstring video_path_) {
    const char *video_path = env->GetStringUTFChars(video_path_, JNI_FALSE);
    ...
    //必须要释放
    env->ReleaseStringUTFChars(video_path_, video_path);
}

2.2.2 C/C++的数据转换为String

extern "C"
JNIEXPORT jstring JNICALL
Java_com_luoye_bzmedia_BZMedia_getString(JNIEnv *env, jclass clazz) {
    const char *imagePath="hello world!";
    return env->NewStringUTF(imagePath);
}

2.3 ​数组相互传递

2.3.1获取数组

extern "C" JNIEXPORT jint JNICALL
Java_com_luoye_bzmedia_BZMedia_mergeVideoOrAudio(JNIEnv *env, jclass type,
        jobjectArray inputPaths,
        jstring outPutPath) {

    size_t arrayLength = static_cast<size_t>(env->GetArrayLength(inputPaths));
    char **pstr = (char **) malloc(arrayLength * sizeof(char *));
    memset(pstr, 0, arrayLength * sizeof(char *));

    for (int i = 0; i < arrayLength; i++) {
        jstring javaPath = (jstring) env->GetObjectArrayElement(inputPaths, i);
        const char *path = env->GetStringUTFChars(javaPath, JNI_FALSE);
        size_t length = strlen(path) + 1;
        char *buffer = static_cast<char *>(malloc(length));
        memset(buffer, 0, length);
        sprintf(buffer, "%s", path);
        env->ReleaseStringUTFChars(javaPath, path);

        pstr[i] = buffer;
    }

    for (int i = 0; i < arrayLength; i++) {
        free(pstr[i]);
    }
    free(pstr);

    return 0;
}

2.3.2返回数组

extern "C"
JNIEXPORT jintArray JNICALL
Java_com_luoye_bzmedia_BZMedia_getVideoSize(JNIEnv *env, jclass clazz, jstring video_path_) {

    if (nullptr == video_path_) {
        BZLogUtil::logE("getVideoSize nullptr == video_path_");
        return nullptr;
    }
    const char *inputPath = env->GetStringUTFChars(video_path_, 0);
    int *ret = VideoUtil::getVideoSize(inputPath);
    env->ReleaseStringUTFChars(video_path_, inputPath);
    if (nullptr == ret) {
        return nullptr;
    }
    jintArray size = env->NewIntArray(2);
    env->SetIntArrayRegion(size, 0, 2, ret);
    delete[]ret;
    return size;
}

​2.4Java对象相互传递

Java对象相互传递需要特别注意的是,字段和方法签名,Java中的字段和方法签名包含了相应成员的名称、类型和参数列表等信息,可以唯一地标识一个成员,获取方式如下:
javap -s class_name
由于是正对class的所以我们需要在build目录下执行这个命令而不是Java源文件,样例如下:

public class com.luoye.bzmedia.bean.VideoInfo {
  public int width;
    descriptor: I
  public com.luoye.bzmedia.bean.VideoInfo();
    descriptor: ()V

  public int getHeight();
    descriptor: ()I

  public void setHeight(int);
    descriptor: (I)V

  public long getDuration();
    descriptor: ()J

  public void setDuration(long);
    descriptor: (J)V

}

只有public类型的字段和方法才能获取到签名

2.4.1获取Java对象

extern "C"
JNIEXPORT jlong JNICALL
Java_com_luoye_bzmedia_BZMedia_startRecord(JNIEnv *env, jclass clazz,
                                           jobject videoRecordParamsObj) {
    VideoRecordParams videoRecordParams;
    jclass videoRecordParamsClass = env->GetObjectClass(videoRecordParamsObj);
    jint srcWidth = env->GetIntField(videoRecordParamsObj,
                                     env->GetFieldID(videoRecordParamsClass, "inputWidth", "I"));
    videoRecordParams.inputWidth = srcWidth;
}

2.4.2返回Java对象

extern "C"
JNIEXPORT jobject JNICALL
Java_com_luoye_bzmedia_BZMedia_getVideoInfo(JNIEnv *env, jclass clazz, jstring video_path_) {

    if (nullptr == video_path_) {
        BZLogUtil::logE("getVideoSize nullptr == video_path_");
        return nullptr;
    }
    const char *inputPath = env->GetStringUTFChars(video_path_, 0);
    VideoInfo *pInfo = VideoUtil::getVideoInfo(inputPath);
    env->ReleaseStringUTFChars(video_path_, inputPath);
    if (nullptr == pInfo) {
        return nullptr;
    }
    jclass videoInfoClass = env->FindClass("com/luoye/bzmedia/bean/VideoInfo");
    jmethodID constructorMid = env->GetMethodID(videoInfoClass, "<init>", "()V");
    jobject videoInfoObj = env->NewObject(videoInfoClass, constructorMid);
    env->CallVoidMethod(videoInfoObj, env->GetMethodID(videoInfoClass, "setWidth", "(I)V"), pInfo->width);
    env->CallVoidMethod(videoInfoObj, env->GetMethodID(videoInfoClass, "setHeight", "(I)V"), pInfo->height);
    env->CallVoidMethod(videoInfoObj, env->GetMethodID(videoInfoClass, "setDuration", "(J)V"), pInfo->duration);
    env->CallVoidMethod(videoInfoObj, env->GetMethodID(videoInfoClass, "setRotate", "(I)V"), pInfo->rotate);
    env->CallVoidMethod(videoInfoObj, env->GetMethodID(videoInfoClass, "setFrameRate", "(D)V"), pInfo->frameRate);
    env->CallVoidMethod(videoInfoObj, env->GetMethodID(videoInfoClass, "setBitRate", "(J)V"), pInfo->bitRate);
    delete pInfo;
    return videoInfoObj;
}

2.5 ​Bitmap的数据相传递

2.5.1获取Bitmap的数据

extern "C" JNIEXPORT jlong JNICALL
Java_com_luoye_bzmedia_BZMedia_addVideoData4Bitmap(JNIEnv *env, jclass type,
                                                   jlong nativeHandle, jobject bitmap,
                                                   jint width, jint height) {
    int ret = 0;
    void *pixelscolor = NULL;

    if ((ret = AndroidBitmap_lockPixels(env, bitmap, &pixelscolor)) < 0) {
        BZLogUtil::logE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
        return ret;
    }
    frame_RGBA->data[0] = (uint8_t *) pixelscolor;
    AVFrame *videoFrame = VideoUtil::allocVideoFrame(AV_PIX_FMT_YUV420P, width, height);
    sws_scale(sws_video_to_YUV,
              (const uint8_t *const *) frame_RGBA->data,
              frame_RGBA->linesize, 0, videoFrame->height,
              videoFrame->data,
              videoFrame->linesize);

    AndroidBitmap_unlockPixels(env, bitmap);
    return addVideoData(nativeHandle, videoFrame);
}

2.5.2返回Bitmap的数据

extern "C" JNIEXPORT jobject JNICALL
Java_com_luoye_bzmedia_BZMedia_bzReadPixelsNative(JNIEnv *env, jclass type,
                                                  jint startX,
                                                  jint startY, jint width,
                                                  jint height) {
    if (width < 1 || height < 1) {
        BZLogUtil::logE("params is error width<1||height<1");
        return nullptr;
    }

    JNIEnv *jniEnv = nullptr;
    bool needDetach = JvmManager::getJNIEnv(&jniEnv);
    int ret;
    void *targetPixels;
    jclass bitmapCls = jniEnv->FindClass("android/graphics/Bitmap");

    jobject newBitmap;
    jmethodID createBitmapFunctionMethodID = jniEnv->GetStaticMethodID(bitmapCls, "createBitmap",
                                                                       "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
    jstring configName = jniEnv->NewStringUTF("ARGB_8888");
    jclass bitmapConfigClass = jniEnv->FindClass("android/graphics/Bitmap$Config");

    jmethodID valueOfBitmapConfigFunctionMethodID = jniEnv->GetStaticMethodID(bitmapConfigClass,
                                                                              "valueOf",
                                                                              "(Ljava/lang/String;)Landroid/graphics/Bitmap$Config;");
    jobject bitmapConfigObj = jniEnv->CallStaticObjectMethod(bitmapConfigClass,
                                                             valueOfBitmapConfigFunctionMethodID,
                                                             configName);

    newBitmap = jniEnv->CallStaticObjectMethod(bitmapCls, createBitmapFunctionMethodID, width,
                                               height, bitmapConfigObj);

    if ((ret = AndroidBitmap_lockPixels(jniEnv, newBitmap, &targetPixels)) < 0) {
        BZLogUtil::logE("gifDataCallBack AndroidBitmap_lockPixels() targetPixels failed ! error=%d",
                        ret);
    }
    if (ret >= 0) {
        char *data = new char[width * height * 4];
        glReadPixels(startX, startY, width, height, GL_RGBA, GL_UNSIGNED_BYTE, data);
        memcpy(targetPixels, data, (size_t) (width * height * 4));
        AndroidBitmap_unlockPixels(jniEnv, newBitmap);
        delete[](data);
    }

    jniEnv->DeleteLocalRef(bitmapCls);
    jniEnv->DeleteLocalRef(configName);
    jniEnv->DeleteLocalRef(bitmapConfigObj);
    jniEnv->DeleteLocalRef(bitmapConfigClass);

    if (needDetach)JvmManager::getJavaVM()->DetachCurrentThread();

    return newBitmap;
}

2.6 Java与C/C++类的对象相互持有

如果是纯Java对象或者纯C/C++要想持有另外一个对象是一件很容易的事情,但是要想在Java与C/C++类的对象中相互持有彼此的实例,就显得很无助
这里就涉及到内存存储对象的机制了,内存一般分为栈内存与堆内存,栈内存存储基本数据类型与变量名,堆内存存储实际的对象,这个对象有一个唯一的地址,通过内存地址可以访问它,在Java中叫引用,在C/C++中叫指针,那么问题就转换到在Java中怎么存储C++对象的指针,在C++中怎么存储Java对象的引用
我们知道机器一般分为32位机器和64位机器,这个本质上说的是内存寻址能力的范围,32位机器的寻址能力为2的32次方,64位机器的寻址能力为2的64次方,也就是说64位机器最大有2的64次方个内存地址,我们只需要用一个变量来记录这个内存地址就好了,无疑用long/int64_t 类型是最合适的,他们有8字节,64bit的大小,刚好可以存下这些内存地址

2.6.1 Java 存储C/C++对象

//返回C/C++对象,强转为jlong
extern "C"
JNIEXPORT jlong JNICALL
Java_com_luoye_bzmedia_BZMedia_startRecord(JNIEnv *env, jclass clazz,
                                           jobject videoRecordParamsObj) {
    VideoRecorder *videoRecorder = new VideoRecorder();
    return reinterpret_cast<int64_t>(videoRecorder);
    }
//Java调用方式
    long nativeHandle = BZMedia.startRecord(videoRecordParams);
//传入C/C++对象的地址
    long ret = BZMedia.addYUV420Data(nativeHandle, buffer, pts);
//在C/C++代码中获取内存地址,并转换为C/C++对象,内存地址=0的时候是NULL,内存地址也可能为负值,这个需要注意一下
    extern "C"
JNIEXPORT jlong JNICALL
Java_com_luoye_bzmedia_BZMedia_addYUV420Data(JNIEnv *env, jclass clazz, jlong native_handle,
                                             jbyteArray data_, jlong pts) {
    if (native_handle == 0) {
        return -1;
    }
    VideoRecorder *videoRecorder = reinterpret_cast<VideoRecorder *>(native_handle);
    unsigned char *buffer = (unsigned char *) env->GetByteArrayElements(data_, nullptr);
    long ret = videoRecorder->addVideoData(buffer, pts);
    env->ReleaseByteArrayElements(data_, reinterpret_cast<jbyte *>(buffer), 0);
    return ret;
}

2.6.3 C/C++ 存储Java对象

同上,Java对象的引用也可以转换为一个int64_t的内存地址,因此也可以很方便的在C/C++对象中存储,在需要调用Java方法的时候就可以很方便

三 回调方法的写法

结合2.6的对象相互存储的技术就可以实现C++回调Java方法,至于Java回调C++同理,由于这种场景比较少我们以C++回调Java方法为例
大致步骤如下:

  1. 在Java层定义回调接口
  2. Java调用Native方法并传入接口对象
  3. Native获取Java接口对象并存储到对象中
  4. 在C++对象中通过函数指针调用C函数,并传入Java接口对象的地址
  5. C函数使用JNI调用Java接口函数

给一个完整的Demo:
Java 方法:

    {
        testCallBack(new OnActionListener() {
            @Override
            public void progress(float progress) {
                Log.d(TAG, "native call back progress=" + progress);
            }
        });
    }

    public native void testCallBack(OnActionListener onActionListener);

    public interface OnActionListener {
        void progress(float progress);
    }

Native方法:

void callBackGateway(int64_t methodHandle, float progress) {
    JNIEnv *jniEnv = nullptr;
    bool needDetach = JvmManager::getJNIEnv(&jniEnv);
    if (methodHandle == 0 || nullptr == jniEnv) {
        jniEnv = nullptr;
        if (needDetach)
            JvmManager::getJavaVM()->DetachCurrentThread();
        return;
    }
    auto *videoPlayerMethodInfo = (ActionMethodInfo *) methodHandle;

    jniEnv->CallVoidMethod(videoPlayerMethodInfo->listenerObj,
                           videoPlayerMethodInfo->onProgressMethodId, progress);
    jniEnv = nullptr;
    if (needDetach)
        JvmManager::getJavaVM()->DetachCurrentThread();
}

extern "C"
JNIEXPORT void JNICALL
Java_com_example_ffmpegtest_MainActivity_testCallBack(JNIEnv *env, jobject thiz, jobject on_action_listener) {
    CallBackTest callBackTest;

    auto *videoPlayerMethodInfo = new ActionMethodInfo();
    //CallBack一般涉及到多线程交互,一般通过NewGlobalRef作为全局变量
    jobject listenerObj = env->NewGlobalRef(on_action_listener);
    videoPlayerMethodInfo->listenerObj = listenerObj;
    jclass listenerClass = env->GetObjectClass(on_action_listener);
    videoPlayerMethodInfo->onProgressMethodId = env->GetMethodID(listenerClass,
                                                                        "progress",
                                                                        "(F)V");
    env->DeleteLocalRef(listenerClass);
    callBackTest.init(reinterpret_cast<int64_t>(videoPlayerMethodInfo), callBackGateway);
    env->DeleteGlobalRef(listenerObj);
    delete videoPlayerMethodInfo;
}

//CallBackTest.h
class CallBackTest {
public:
    int init(int64_t handle, void (*progressCallBack)(int64_t javaHandle, float progress));
};

//CallBackTest.cpp
int CallBackTest::init(int64_t handle, void (*progressCallBack)(int64_t, float)) {
    progressCallBack(handle, 0);
    progressCallBack(handle, 0.2);
    progressCallBack(handle, 0.4);
    progressCallBack(handle, 0.6);
    progressCallBack(handle, 0.8);
    progressCallBack(handle, 1);
    return 0;
}

//JvmManager.h
class JvmManager {
public:
    static JavaVM *getJavaVM();

    static bool getJNIEnv(JNIEnv **pJNIEnv);

    static int32_t JNI_VERSION;
};

//JvmManager.cpp
#include <android/log.h>
#include "JvmManager.h"

extern "C" {
#include <libavcodec/ffmpeg_jni.h>
}
JavaVM *bzJavaVM = nullptr;

int32_t JvmManager::JNI_VERSION = JNI_VERSION_1_6;

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    bzJavaVM = vm;
    __android_log_print(ANDROID_LOG_DEBUG, "bz_", "JNI_OnLoad success");
    av_jni_set_java_vm(vm, NULL);
    if (vm->GetEnv(reinterpret_cast<void **>(&vm), JNI_VERSION_1_6) != JNI_OK) {
        JvmManager::JNI_VERSION = JNI_VERSION_1_4;
        return JNI_VERSION_1_4;
    }
    return JNI_VERSION_1_6;
}

bool JvmManager::getJNIEnv(JNIEnv **pJNIEnv) {
    if (NULL == bzJavaVM) {
        return false;
    }
    bzJavaVM->GetEnv((void **) pJNIEnv, JNI_VERSION);
    if (NULL != *pJNIEnv) {
        return false;
    } else {
        bzJavaVM->AttachCurrentThread(pJNIEnv, NULL);
        return true;
    }
}

JavaVM *JvmManager::getJavaVM() {
    return bzJavaVM;
}

四 Jni资源回收

C++,Java的对象都有自身的新建与回收的机制与方法,Jni也不例外主要是涉及到引用的释放,只要父类是jobject的都涉及到手动释放,主要的释放方法为:

  1. DeleteLocalRef
  2. DeleteGlobalRef
  3. DeleteWeakGlobalRef
  4. ReleaseStringUTFChars
  5. ReleaseStringChars
  6. ReleasePrimitiveArrayCritical
  7. ReleaseIntArrayElements
  8. GetDirectBufferAddress
  9. FreeDirectByteBuffer

常见新建与回收的情况如下:

jstring:

env->GetStringUTFChars
env->ReleaseStringUTFChars
如:
const char *mediaPath = env->GetStringUTFChars(media_path, 0);
env->ReleaseStringUTFChars(media_path, mediaPath);

jclass

env->GetObjectClass
env->DeleteLocalRef
如:
jclass listenerClass = env->GetObjectClass(listener);
env->DeleteLocalRef(listenerClass);

jobject

env->NewGlobalRef
env->DeleteGlobalRef
如:
jobject actionObj = env->NewGlobalRef(obj);
env->DeleteLocalRef(actionClass);

数组

env->GetIntArrayElements
env->ReleaseIntArrayElements
如:
jint *elems = env->GetIntArrayElements(arr, 0);
env->ReleaseIntArrayElements(arr, elems, 0);

五 如何防止SO被脱裤

在Android侧SO的调用是通过JNI调入的,也就是说我们只要知道Java的native方法就可以直接使用SO的能力,再通过二进制修改掉native方法路径,以及SO的名字就可以完完全全把SO的能力直接复用,而且基本发现不了,因此我们需要对SO进行处理,也就是需要做鉴权处理。
SO需要做鉴权就需要拿到比较唯一的身份识别,在Android侧能做为身份识别的有Android包名以及APK签名,一般做鉴权都是基于这两者来做的,但是系统方法是可以被hook掉的,一般来说我们无法通过获取包名,以及签名来做鉴权。
我这里提供的一种方案是通过加密包名,提前放到SO里面,然后遍历包名下的私有目录,通过判断是否有文件权限的方式来鉴权,这样可以不调Java获取包名以及签名的方法就可以做好鉴权
相关Demo详见:https://github.com/bookzhan/bzmedia/tree/master/bzmedialib/src/main/cpp/permission 相关的代码

此条目发表在Android, FFmpeg分类目录。将固定链接加入收藏夹。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注