前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >「音视频直播技术」看ijkplayer如何使用JNI

「音视频直播技术」看ijkplayer如何使用JNI

作者头像
音视频_李超
发布2020-04-01 20:41:45
2K0
发布2020-04-01 20:41:45
举报

前言

ijklayer可以说是目前最火的一款移动端播放器了。它同时支持Android和iOS,是由著名的B站开源的播放器库,在GitHub上有15.2K的 start。

它之所以如此流行,主要是代码写的太美了,我认为把它当作艺术品也不过分。没看过它代码的同学可以去了解一下。

ijkplayer为了提高性能做了大量的优化,其中一个关键点是使用了JNI。播边器里最关键的部分全部由C来实现。

今天我们就来看看 jikplayer 是如何使用JNI的。

导入动态库

ijkplayer 创建 IJKMediaPlayer 对象时,在其构造函数里会调到 loadLibrariesOnce 方法。代码如下:

代码语言:javascript
复制
public static void loadLibrariesOnce(IjkLibLoader libLoader) {
    synchronized (IjkMediaPlayer.class) {
        if (!mIsLibLoaded) {
            if (libLoader == null)
                libLoader = sLocalLibLoader;

            //加载三个动态库
            libLoader.loadLibrary("ijkffmpeg"); //加载 ffmpeg 用于解码
            libLoader.loadLibrary("ijksdl"); // 加载 sdk 用于渲染
            libLoader.loadLibrary("ijkplayer"); // 加载 ijkplayer 核心播放器
            mIsLibLoaded = true;
        }
    }
}

其中,loadLibrariesOnce方法中的libLoader是ijkplayer定义的IjkLibLoader类对象。该对象的 loadLibrary 方法最终会调用 System.loadLibrary 函数完成共享库的加载。

经过上面操作后 ijkffmpeg、ijdsdk及ijkplayer就被加载到JavaVM里了。

在Android系统下,每一个进程只能有一个JavaVM。

我们第一步看下在ijkplayer里,如何通过 Java代码调到 C/C++接口。

在Java层定义本地方法

想通过 Java 代码调用 C/C++ 代码,首先需要让 Java 程序知道都有哪些 C/C++ 接口可以使用。这有点像C/C++中常说的符号表(名子与地址的对应关系表)。如何能做到这点呢?方法很简单,就是在 Java 类方法的前边加上 "native" 关键字。我们看一下 IJKPlayer 都提供了哪些本地方法吧:

代码语言:javascript
复制
......

private native void _setDataSourceFd(int fd);
private native void _setDataSource(IMediaDataSource mediaDataSource);
public native void _prepareAsync() throws IllegalStateException;
private native void _start() throws IllegalStateException;
private native void _stop() throws IllegalStateException;
private native void _pause() throws IllegalStateException;
public native void seekTo(long msec) throws IllegalStateException;
public native long getCurrentPosition();
public native long getDuration();
private native void _release();
private native void _reset();
public native void setVolume(float leftVolume, float rightVolume);
......
private static native void native_init();
private native void native_setup(Object IjkMediaPlayer_this);
private native void native_finalize();
......

这一步是不是非常简单?

当然,只做到这一步还无法调用 C/C++接口,因为你还没告诉JavaVM你的C/C++接口在哪儿呢。下面我们开始第二步。

注册C/C++方法

仅在Java层定义本地方法只完成了工作的一半。当Java代码真正调用 “native” 方法时,JavaVM虚拟机会在自己的符号表中查找有没有 Java 程序想调用的函数。如果此时还没有的话,JavaVM 就会报错。所以现在我们要将 C/C++ 提供的接口注册到 JavaVM中。

首先,建好函数对应表。此表中的每一项都包括三个元素,分别是 外部调用的接口名、signature、内部真正的实现函数

signature 后面有专门的讲解。

代码如下:

代码语言:javascript
复制
static JNINativeMethod g_methods[] = {

    ......
     
    { "_setDataSourceFd",       "(I)V",     (void *) IjkMediaPlayer_setDataSourceFd },
    { "_setDataSource",         "(Ltv/danmaku/ijk/media/player/misc/IMediaDataSource;)V", (void *)IjkMediaPlayer_setDataSourceCallback },
    { "_prepareAsync",          "()V",      (void *) IjkMediaPlayer_prepareAsync },
    { "_start",                 "()V",      (void *) IjkMediaPlayer_start },
    { "_stop",                  "()V",      (void *) IjkMediaPlayer_stop },
    { "seekTo",                 "(J)V",     (void *) IjkMediaPlayer_seekTo },
    { "_pause",                 "()V",      (void *) IjkMediaPlayer_pause },
    { "isPlaying",              "()Z",      (void *) IjkMediaPlayer_isPlaying },
    { "getCurrentPosition",     "()J",      (void *) IjkMediaPlayer_getCurrentPosition },
    { "getDuration",            "()J",      (void *) IjkMediaPlayer_getDuration },
    { "_release",               "()V",      (void *) IjkMediaPlayer_release },
    { "_reset",                 "()V",      (void *) IjkMediaPlayer_reset },
    { "setVolume",              "(FF)V",    (void *) IjkMediaPlayer_setVolume },
    { "native_init",            "()V",      (void *) IjkMediaPlayer_native_init },
    { "native_setup",           "(Ljava/lang/Object;)V", (void *) IjkMediaPlayer_native_setup },
    { "native_finalize",        "()V",      (void *) IjkMediaPlayer_native_finalize },
    
    ......
};  

看看这里的外部调用函数名是不是与上面在 Java 层定义的方法名是一样的呢?只有一样它们之前才能建立起对应关系来。

然后,将上面表中的方法注册到JavaVM中。代码如下:

代码语言:javascript
复制
......

#define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))

......

#define JNI_CLASS_IJKPLAYER     "tv/danmaku/ijk/media/player/IjkMediaPlayer"

......

#define IJK_FIND_JAVA_CLASS(env__, var__, classsign__) \
do { \
    jclass clazz = (*env__)->FindClass(env__, classsign__); \
    if (J4A_ExceptionCheck__catchAll(env) || !(clazz)) { \
        ALOGE("FindClass failed: %s", classsign__); \
        return -1; \
    } \
    var__ = (*env__)->NewGlobalRef(env__, clazz); \
    if (J4A_ExceptionCheck__catchAll(env) || !(var__)) { \
        ALOGE("FindClass::NewGlobalRef failed: %s", classsign__); \
        (*env__)->DeleteLocalRef(env__, clazz); \
        return -1; \
    } \
    (*env__)->DeleteLocalRef(env__, clazz); \
} while(0);

......

//拿到 tv/danmaku/ijk/media/player/IjkMediaPlayer 类的对象,
//返回 Globle 引用。
IJK_FIND_JAVA_CLASS(env, g_clazz.clazz, JNI_CLASS_IJKPLAYER);

//注册native方法,并与 IjkMediaPlayer 关联起来。
(*env)->RegisterNatives(env, g_clazz.clazz, g_methods, NELEM(g_methods) );

......

上面的代码是不是看着有点混乱?尤其是 IJK_FIND_JAVA_CLASS 这个宏定义?其实没关系,只要我们知道上面代码核心就是,通过FindClass找到定义本地方法的java类,再通过RegisterNatives函数将C/C++接口注册到JavaVM中,并与FindClass找到的类绑定就好了。

在哪儿注册最好

上面我们知道了如何注册C/C++方法,那么在什么地方注册好呢?答案是在 JNI_OnLoad 函数中。在加载动态链接库时,JavaVM会主动调用JNI_OnLoad(JavaVM * jvm, void * reserved)(如果你实现在JNI_OnLoad函数),所以在这里注册是最好的地方。看一下ijkplayer的实现:

代码语言:javascript
复制
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
    JNIEnv* env = NULL;

    g_jvm = vm;
    if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        return -1;
    }
    ......

    // FindClass returns GlobleReference
    IJK_FIND_JAVA_CLASS(env, g_clazz.clazz, JNI_CLASS_IJKPLAYER);
    (*env)->RegisterNatives(env, g_clazz.clazz, g_methods, NELEM(g_methods) );

    ......

    return JNI_VERSION_1_4;
}

JNIEXPORT void JNI_OnUnload(JavaVM *jvm, void *reserved)
{
    ......
}

在 JNI_OnLoad 函数中首先获取 JNIEnv,之后找到 IjkMediaPlayer 类,最后注册C/C++方法,并将注册的方法与IjkMediaPlayer类关联起来。

当然,有了 JNI_OnLoad 还要有 JNI_OnUnload 函数。它在共享库被卸载时调用,可以在这里释放一些资源。

通过上面的操作我们就可以从 Java 调用 C++的代码了。有没有赶快去试试的冲动?先别急,现在只介绍了如何从 Java 调用 C/C++的方法。那么反回来如何从 C/C++ 调 Java 代码呢?

C/C++调用Java方法

在 ijkplayer 中,它会使用C调用android下的 MediaCodec类中的方法。我们就以这个为例子看一下它是如何从C调用的java方法吧。

首先,通过 FindClass 拿到想要处理类的 jclass 对象。然后获得该对象的全局引用,并将本地引用删除。

这些方法的调用都要做异常判断,如果出现异常所有的结果都是无效的值。

代码语言:javascript
复制
......

//对异常的处理
bool J4A_ExceptionCheck__catchAll(JNIEnv *env)
{
    if ((*env)->ExceptionCheck(env)) {
        (*env)->ExceptionDescribe(env);
        (*env)->ExceptionClear(env);
        return true;
    }

    return false;
}

//通过 FindClass 得到jclass对象
jclass J4A_FindClass__catchAll(JNIEnv *env, const char *class_sign)
{
    jclass clazz = (*env)->FindClass(env, class_sign);
    ......
}

//拿到 jclass 对象,并设置其为全局引用
jclass J4A_FindClass__asGlobalRef__catchAll(JNIEnv *env, const char *class_sign)
{
    jclass clazz_global = NULL;
    jclass clazz = J4A_FindClass__catchAll(env, class_sign);
     
    ......
    
    //设置为全局引用
    clazz_global = J4A_NewGlobalRef__catchAll(env, clazz);
    ......

fail:
    J4A_DeleteLocalRef__p(env, &clazz);
    return clazz_global;
}
......

//设置要获取的类名
sign = "android/media/MediaCodec";
class_J4AC_android_media_MediaCodec.id =
            J4A_FindClass__asGlobalRef__catchAll(env, sign);
            
......

获得了 jclass 后,就可以通过 Get<Type>MethodID 获取类方法的jmethodID对象。

代码语言:javascript
复制
......

jmethodID J4A_GetStaticMethodID__catchAll(JNIEnv *env, jclass clazz, const char *method_name, const char *method_sign)
{   
    jmethodID method_id = (*env)->GetStaticMethodID(env, clazz, method_name, method_sign);
    ......
    
fail:
    return method_id;
}

......

class_id = class_J4AC_android_media_MediaCodec.id; //jclass
name     = "createByCodecName"; //方法名
sign     = "(Ljava/lang/String;)Landroid/media/MediaCodec;"; //signature
class_J4AC_android_media_MediaCodec.method_createByCodecName = 
            J4A_GetStaticMethodID__catchAll(env, class_id, name, sign);
            
......

最后,通过 Call<Type>Method 调用 java 方法。

代码语言:javascript
复制
......

jobject J4AC_android_media_MediaCodec__createByCodecName(JNIEnv *env, jstring name)
{
    return (*env)->CallStaticObjectMethod(env,
             class_J4AC_android_media_MediaCodec.id,
             class_J4AC_android_media_MediaCodec.method_createByCodecName, 
             name);
}
......

现在 C/C++ 也可以调用 Java 方法了。_

最后,我们再来看一下C/C++如何访问 java 的字段吧,这个就更简单了。

C/C++访问Java字段

有了 C/C++访问Java的基础,再看访问Java字段就容易多了。它也是先获取 jclass, 之后通过 jclass 得到 jfieldID,最终 Get/Set java 字段。jclass的获取我们就不讲了,重点说说获取 jfieldID 和 Get/Set。

代码语言:javascript
复制
......

jfieldID J4A_GetFieldID__catchAll(JNIEnv *env, jclass clazz, const char *field_name, const char *field_sign)
{       
     //获得 jfieldID
    jfieldID field_id = (*env)->GetFieldID(env, clazz, field_name, field_sign);
    
    //异常判断
    if (J4A_ExceptionCheck__catchAll(env) || !field_id) {
        ......
    }
        
fail:
    return field_id;
}   
 
......
class_id = class_J4AC_android_media_MediaCodec__BufferInfo.id; //jclass
name     = "flags"; // java 字段名
sign     = "I"; // signature 
class_J4AC_android_media_MediaCodec__BufferInfo.field_flags =
             J4A_GetFieldID__catchAll(env, class_id, name, sign);
......

上面的代码通过GetFieldID方法就得到了我们想要的 jfieldID。下一步看看如何进行 Get/Set。

代码语言:javascript
复制
......
jint J4AC_android_media_MediaCodec__BufferInfo__flags__get(JNIEnv *env, jobject thiz)
{
    return (*env)->GetIntField(env, thiz, class_J4AC_android_media_MediaCodec__BufferInfo.field_flags);
}
......

void J4AC_android_media_MediaCodec__BufferInfo__flags__set(JNIEnv *env, jobject thiz, jint value)
{
    (*env)->SetIntField(env, thiz, class_J4AC_android_media_MediaCodec__BufferInfo.field_flags, value);
}
......

非常简单,JNI调用 Get<Type>Field或Set<Type>Field方法获取或设置Java的字段。

至此我们就分析完了 ijkplayer 对 JNI的使用。后面附上 Signature 的说明。

Signature

在JNI中Signature主要用于操作Java类中的方法。Signature一般由两部分组成:方法参数;方法返回值。

  • 方法参数包含在“()”中,返回值在括号外!
  • 方法参数个数较多时会依次以“;”隔开。
  • 当参数或者返回值是基本数据类型时,必须用其在JNI中的描述符表示。下表就是Java基本数据类型对应的JNI中的描述符。

Java类型

符号

boolean

Z

byte

B

char

C

short

S

int

I

long

L

float

F

doubl

D

void

V

  • 方法参数或者返回值为java中的对象时,必须以“L”加上其路径,不过路径必须以“/”分开,自定义的对象也使用本规则,不在包中时直接“L”加上类名称。
  • 当参数或者返回值为数组时,前面必须加上“[”。

以上就是Signature表示方法的规则!

看看下面一些Signature,你能一个个转换为相应的方法吗?

  • ([LStudent;)[LStudent;
  • ([I[Ljava/lang/String;[LStudent;)Ljava/lang/Object;
  • ([LStudent;[LStudent;)[LStudent;
  • ([Ljava/util/Iterator;)[Ljava/util/Enumeration;
  • ([Ljava/lang/Object;)[Ljava/lang/Object;
  • ([Ljava/lang/String;)[Ljava/lang/String;
  • (LStudent;)LStudent;

小结

本篇文章介绍了ijkplayer是如何使用JNI的,主要包以下几点内容:

  1. Java 如何调用 C/C++ 接口。
  2. C/C++ 如何调用 Java 方法。
  3. C/C++ 如何设置/获取 Java 字段的值。
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 导入动态库
  • 在Java层定义本地方法
  • 注册C/C++方法
  • 在哪儿注册最好
  • C/C++调用Java方法
  • C/C++访问Java字段
  • Signature
  • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档