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

「音视频直播技术」JNI注意事项(一)_第1张图片

前言

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(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 引用

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


「音视频直播技术」JNI注意事项(一)_第2张图片
微信公众号

你可能感兴趣的:(「音视频直播技术」JNI注意事项(一))