专栏首页音视频直播技术专家「音视频直播技术」JNI注意事项(一)

「音视频直播技术」JNI注意事项(一)

前言

Android手机现在已经占据全球智能手机市场第一位了。但Android手机广为大家所诟病的就是运行速度越来越慢。于是各App都在想尽办法进行优化,以提升用户感受。

其中一个可以大幅提升性能的的办法就是使用JNI技术。也就是说将一些复杂的,占CPU比较多的模块、函数使用 C/C++来实现,Java再通过 JNI 接口调用 C/C++函数从而达到优化的目的。

目前市面上的大多数游戏,音视频直播的App都采用这种方法。今天我们就为大家讲讲使用JNI都需要注意些什么。

JNI

JNI(Java Native Interface), 用于 Java 代码与 C/C++ 代码之间的相互调用。之所以使用 JNI 主要还是从效率的角度出发。尤其对于音视频的编解码,如果使用软编的话,都会使用 JNI。

JavaVM 和 JNIEnv

JNI定义了两种重要的数据结构 JavaVM 和 JNIEnv。他们都是指向函数表指针的指针。 JavaVM提供了调用接口的函数,它允许你创建或销毁JavaVM。理论上在同一个进程中你可以有多个JavaVM,但 Android 只支持一个。

JNIEnv提供了大部分 JNI 函数。你自己的 Native 函数的第一个参数就是 JNIEnv。

JNIEnv用于线程本地存储。所以你不能在线程间共享JNIEnv。如果一段代码无法得到JNIEnv,你应该通过 JavaVM 的 GetEnv 方法获取。

C 声明 JavaVM 和 JNIEev 与 C++ 的声明不一样。jni.h 头文件根据你是C代码还是C++代码提供了两种类型声明,所以最好不要在头文件中包括 JNIEnv 类型参数。

换句话说,如果在头文件中需要 #ifdef __cplusplus,你在头文件中又有JNIEnv类型,那么你很可能会遇到麻烦。

Threads

所有的线程都是 Linux 线程。他们一般情况下是从 Thread.start启动的。但它可以在任何地方创建,然后再绑定到 JavaVM上。例如,pthread_create创建的线程,可以通过 AttachCurrentThread 或 AttachCurrentThreadAsDaemon 函数绑到 JavaVM上。在绑定之前,它拿不到 JNIEnv 也不能做 JNI调用。

绑定本地创建的线程时会构造 java.lang.Thread对象,并把它添加到 "main"线程组(ThreadGroup)中,使得 debugger 可以知道它。如果线程已经绑定过了,再调用AttachCurrentThread函数时,它什么也不做。

Android不会暂停正在执行Native代码的线程。如果GC正在做回收,或者debugger发起了暂停的请求,Android将在下一次进行JNI调用时暂停该线程。

也就是说Native代码必一次性执行完,Android没有打断Native代码执行的方法。

通过JNI绑定的线程在退出前,必须调用DetachCurrentThread函数。如果你觉得直接这样做很不舒服,在Android2.0之后,你可用pthread_key_create函数定义一个析构函数,它会在线程退出之前被调用, 并在析构函数里调用DetachCurrentThread。

使用同样的key, 用pthread_setspecific将 JNIEnv 存到线程本地存储中,这样它将作为参数传给你的析构函数。

jclass, jmethodID 和 jfieldID

如查你想通过Native代码访问java对象里的域,你可按如下步骤做:

  • 使用 FindClass 得到类对象的引用。
  • 通过 GetFieldID 得到 field ID。
  • 通过适当的方法得到 field 的内容,如 GetIntField。

调用方法也是相似的,首先要得到类对象的引用,然后是方法ID。ID通常是指向内部运行时数据结构的指针。查找他们可能需要几个字符串的比较,但一旦你获得他们之后,调用是非常快的。

如果性能是非常重要的,那么把结果缓存在你的Native代码中就非常有必要了。另外,因为每个进程只能有一个 JavaVM 的限制,所以需要将数据存放在静态本地结构中是合理的。

类的引用(jclass),fieldID, methodID在类卸载前都是有效的。所有与ClassLoader关联的类被GC回收之前类是不会被卸载的。类被卸载的情况很少出现,但在Android下还是有可能发生的。为了保证万无一失,jclass必须通过调用NewGlobalRef进行保护。

如果加载class后,你喜欢把它缓存起来,并且在它被卸载或重新加载时自动更新缓存,那么,初始化ID的正确方法是添加一段像下面这样的代码:

    /*
     * We use a class initializer to allow the native code to cache some
     * field offsets. This native function looks up and caches interesting
     * class/field/method IDs. Throws on failure.
     */
    private static native void nativeInit();

    static {
        nativeInit();
    }

在你的 C/C++ 代码中创建 nativeClassInit 方法,执行 ID 查找。该代码仅在类初始化时执行一次。如果类被卸或重新加载了,它会再次执行。

Local 和 Global 引用

传给Native方法的每个参数和几乎由JNI函数返回的每个对象都是一个本地引用。这意味着它在当前线程,当前Native方法里是有效的。在从Native方法返回后,虽然对象本身还存活着,但它的引用已经失效了。

这个规则适用于jobject所有的子类,包括jclass, jstring和 jarray。

得到非本地引用的唯一方法是通过 NewGlobalRef 和 NewWeakGlobalRef方法。

如果你想更长时间的持有一个引用,你必须使用 "global" 引用。NewGlobalRef函数使用本地引用作为参数,返回全局引用。全局引用一直是有效的,除非你主动调用DeleteGlobalRef。

下面是缓存从 FindClass 返回的jclass的通用方法:

jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));

所有的 JNI 方法都可以接受本地或全局引用作为参数。引用同一个对象有两个不同的引用值也是有可能的。例如,在同一对象上连续调用NewGlobalRef的返回值可能不同。查看两个引用是否指向同一个对象必须使用 IsSameObject 方法。千万别使用 “==” 比较两个引用。

一个后果是,在本地代码中你不能假定对象引用是不变的或唯一的。这次方法的调用与下次方法调用返回的32位对象值可能是不同的,并且两个不同对象可能在连续调用后具有相同的32位值是可能的。千万不要使用jobject值作为键。

作为开发人员,不要过度分配本地引用。也就是说如果你创建了大量的本地引用,你应该手动调用DeleteLocalRef释放它们,而不是等着让JNI做这件事儿。具本的实现是预留16个本地引用槽,如果你需要更多的,你应该删除之前的,或使用EnsureLocalCapacity / PushLocalFrame 。

注意,jfieldID 和 jmethodID 不是对像引用。它们不应该作为参数传给NewGlobalRef。由函数返回的原始数据指针,如GetStringUTFChars和GetByteArrayElements也不是对象。

原如数据可以在线程间传递。它们一直有效,除非调用了匹配的释放函数。

另外一个特别需要注意的地方是,如果用AttachCurrentThread绑定的Native线程,除非它解绑本地线程,否则运行的代码将永远不会自动释放本地引用。任何你创建的本地引用都必须手动删除。通常,任何在Native代码中创建的本地引用也需要手动删除。

小结

今天主要介绍下面的内容:

  1. JavaVM、JNIEnv
  2. 线程
  3. jclass, jmethodID 和 jfieldID
  4. Local 和 Global 引用

后面还会有一篇文章,请大家继续观注。谢谢!


本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 音视频直播--深度理解Handler 与 HandlerThread

    大家好,今天我为大家讲解一下Android系统下的Handler机制。做过Android系统开发的人都清楚,App应用程序的主线程是决对不能被阻塞的,因为它的主...

    音视频_李超
  • Nodejs+socket.io搭建WebRTC信令服务器

    我们在学习 WebRTC 时,首先要把实验环境搭建好,这样我们就可以在上面做各种实验了。

    音视频_李超
  • 最通俗易懂的H264基本原理

    H264视频压缩算法现在无疑是所有视频压缩技术中使用最广泛,最流行的。随着 x264/openh264以及ffmpeg等开源库的推出,大多数使用者无需再对H26...

    音视频_李超
  • 一日一技:uwsgi默认buffersize太小可能导致问题

    这是由于,uWsgi默认的buffersize 为4096,如果http请求数据超过这个量,就会报错,为了解决这个问题,就需要修改uwsgi的配置文件,增加一项...

    青南
  • 领英Hadoop数据丢失事故,我们从中得到了哪些教训?

    对企业而言,失败往往比成功更具有启发性。另外,如果团队行动太快,又无法以完全透明的方式处理问题,那么失败所带来的影响有可能长期困扰整个团队。我们在 Linked...

    深度学习与Python
  • 关于直播系统开发中直播架构的重要组成部分

    直播的推流和拉流主要是由五个部分组成的,分别是:(音视频)采集、(数据)编码、(数据)传输、解码(数据)、播放显示。开发直播 app,直播源码是一个非常重要的存...

    布谷安妮
  • 编码篇-数据管理者Model

          Model是数据管理者和持有者,是数据解析层剥离ViewConyroller的关键所在。同是也是cell滑动不卡(省去每次解析)的好方式。

    進无尽
  • 给女朋友讲解什么是代理模式

    Java3y
  • 任务态fMRI研究:在低强度和高强度运动下皮层和皮层下脑区的调节作用

    在开始读之前,还是一个以前谈过的观点。不要因为路径依赖而受到局限,还记得上次商业顶刊那篇关于创业者演讲激情对投资者投资兴趣的文章吗?今天带来的文章更加有...

    用户1279583
  • 如何修改dedecms专题目录默认名称special

      专题有一个聚合的效果,一般会比普通的文章页更符合用户需求。如果用dedecms建专题的话,默认的目录是special,怎么修改修改dedecms专题目录名称...

    ytkah

扫码关注云+社区

领取腾讯云代金券