「音影片直播技術」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 引用

後面還會有一篇文章,請大家繼續觀注。謝謝!


Tags: