Android - 深入理解 JNI

fmms 13年前發布 | 93K 次閱讀 Android Android開發 移動開發

JNI : Java Native Interface 即JAVA本地調用,為何需要這種技術呢?原因有二:
1、運行JAVA程序的虛擬機是用Native語言編寫的,而虛擬機運行在具體的平臺上,所以虛擬機本身無法做到平臺無關,而利用JNI技術即可對JAVA層屏蔽不同操作系統平臺之間的差異,如file,socket等
2、在JAVA語言誕生前,很多程序使用Native語言編寫,JAVA直接利用JNI使用,避免造重復輪子的壞名聲。而且JNI的運行效率和速度會更高


第一大部分: JAVA如何調用Native函數


通過MediaScanner進行完整解釋其工作原理:


調用層次關系,圖示:

Android - 深入理解 JNI


JAVA層代碼分析:MediaScanner.java
public class MediaScanner
{
    static {  //static 語句,前面的 JAVA學習系列(一) 有說明過這個問題
    //加載對應用JNI庫,linux上即調用libmedai_jni.so,而windows平臺上調用libmedia_jni.dll
        System.loadLibrary("media_jni");
        native_init(); //調用native函數
    }


//非native函數
public void scanDirectories(String[] directories, String volumeName) 


//聲明native函數,native為JAVA語言的關鍵字
private static native final void native_init();
    private native final void native_setup();
    private native final void native_finalize();
    private native void processDirectory(String path, String extensions, MediaScannerClient client);
}
總結: 一個是加載jni動態庫,二是聲明java的native函數


JNI層 android_media_MediaScanner.cpp 分析:


首先來找下java層的native_init函數對應jni層的函數?
首先native_init函數位于android.media包中,它的全路徑:android.media.MediaScanner.native_init,由于
在native語言中,"."有特殊的意義,所以用"_"來替換:即上面路徑這為:android_media_MediaScanner_native_init
ok,函數名就如此關聯起來了。而JNI函數注冊有兩種方式:
1、靜態注冊方法
a、編寫java代碼,然后編譯生成.class文件
b、使用javah -o output packagename.classname生成jni層頭文件
調用時先加載動態庫,然后查找native_init函數的jni函數:android_media_MediaScanner_native_init
如果找到則建立這兩個函數的關聯關系,即保存jni層函數的函數指針,這個由虛擬機完成。
缺點:javah生成的函數名特別長,不利于書寫,且第一次調用時需要根據函數名字搜索建立關聯關系。

2、動態注冊方法
   利用JNINativeMethod結構保存其關系
   typedef struct 
   {
    //JAVA中native函數名字
    const char name;
    //簽名信息,用字符串表示,參數類型及返回值類型的組合
    const char
signature;
    ///JNI層函數函數指針,轉換為void類型
    void
fnPtr;
   };
   
static JNINativeMethod gMethods[] = {
    {"processDirectory",  "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",    
                                                        (void )android_media_MediaScanner_processDirectory},
    {"processFile",       "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",    
                                                        (void
)android_media_MediaScanner_processFile},
    {"setLocale",         "(Ljava/lang/String;)V",      (void )android_media_MediaScanner_setLocale},
    {"extractAlbumArt",   "(Ljava/io/FileDescriptor;)[B",     (void
)android_media_MediaScanner_extractAlbumArt},
    {"native_init",        "()V",                      (void )android_media_MediaScanner_native_init},
    {"native_setup",        "()V",                      (void
)android_media_MediaScanner_native_setup},
    {"native_finalize",     "()V",                      (void )android_media_MediaScanner_native_finalize},
};


這里說明一下簽名信息:
因為JAVA支持函數重載,可以定義同名但不同參數的函數,但直接根據函數名是沒法找到具體函數的,因此利用參數類型及返回類型
組成簽名信息。


常用類型標識符:
類型標識   JAVA類型    字長
  Z        boolean      8位
  B        byte         8位
  C        char         16位 -- 注意喲
  S        short        16位
  I        int          32位
  J        long         64位
  F        float        32位
  D        double       64位
  L/java/languageString  String
  [I       int[]        int數組
  [L/java/lang/object    Object[] 對象數組


// This function only registers the native methods, and is called from
// JNI_OnLoad in android_media_MediaPlayer.cpp
int register_android_media_MediaScanner(JNIEnv
env)
{
//利用registerNativeMethods注冊JNI函數
    return AndroidRuntime::registerNativeMethods(env,
                "android/media/MediaScanner", gMethods, NELEM(gMethods));
}


// 加載jni庫,查找該庫中的JNI_OnLoad函數完成動態注冊工作
jint JNI_OnLoad(JavaVM vm, void reserved)
{
...
if (register_android_media_MediaScanner(env) < 0) {
        LOGE("ERROR: MediaScanner native registration failed\n");
        goto bail;
    }
}


static void
android_media_MediaScanner_processDirectory(JNIEnv env, jobject thiz, jstring path, jstring extensions, jobject client)
{
    MyMediaScannerClient myClient(env, client);
    mp->processDirectory(pathStr, extensionsStr, myClient, ExceptionCheck, env);
...
}


NATIVE庫實現:MediaScanner.cpp (編譯成libmedia.so庫)
status_t MediaScanner::processDirectory(
        const char
path, const char extensions,
        MediaScannerClient &client,
        ExceptionCheck exceptionCheck, void
exceptionEnv) {
    ...
    client.setLocale(locale());


    status_t result =
        doProcessDirectory(
                pathBuffer, pathRemaining, extensions, client,
                exceptionCheck, exceptionEnv);


    free(pathBuffer);
    ...
}
如此整個流程調用邏輯就講完了。


第二大部分:JNIEnv 介紹


JNIEnv 是一個與線程相關的變量,由于線程相關,所以線程B中不能使用線程A中的JNIEnv函數。
那個多個線程由誰來保存并保證每個線程的JNIEnv結構體正確呢?
Android - 深入理解 JNI

jint JNI_OnLoad(JavaVM vm, void reserved)
全進程只有一個JavaVM對象,可以保存并在任何地方使用沒有問題,獨此一份。
利用JavaVM中的 AttachCurrentThread函數,就可以得到這個線程的 JNIEnv結構體,利用用DetachCurrnetThread釋放相應資源


a、通過 JNIEnv 操作 jobject


jfieldID   操作成員變量
jmethodID  操作成員函數


/
 
Get a field ID (instance fields).
 /
static jfieldID GetFieldID(JNIEnv
env, jclass jclazz,
    const char name, const char sig)


/
 
Get a method ID for an instance method.
 
 
JNI defines as an instance method, but Dalvik considers it a
 * "direct" method, so we have to special-case it here.
 *
 * Dalvik also puts all private methods into the "direct" list, so we
 * really need to just search both lists.
 */
static jmethodID GetMethodID(JNIEnv* env, jclass jclazz, const char* name,
    const char* sig)
    
具體實現參考:dalvik\vm\jni.c的實現:重要的函數還有 JNI_CreateJavaVM [Create a new VM instance]


舉例說明JNI如何調用JAVA層函數:
JNIEnv *mEnv;
jmethodID mScanFileMethodID; 
jmethodID mHandleStringTagMethodID; 


MyMediaScannerClient(JNIEnv *env, jobject client)
    :   mEnv(env),
        mClient(env->NewGlobalRef(client)),
        mScanFileMethodID(0),
        mHandleStringTagMethodID(0),
        mSetMimeTypeMethodID(0)
{
...

// 找到 android.media.MediaScannerClient類在JNI層對應用用jclass實例
jclass mediaScannerClientInterface = env->FindClass("android/media/MediaScannerClient");

//依此取出 MediaScannerClient 類中的函數 scanFile/handleStringTag 的 jMethodID
    mScanFileMethodID = env->GetMethodID(mediaScannerClientInterface, "scanFile",
                                             "(Ljava/lang/String;JJ)V");
    mHandleStringTagMethodID = env->GetMethodID(mediaScannerClientInterface, "handleStringTag",
                                             "(Ljava/lang/String;Ljava/lang/String;)V");


}


// returns true if it succeeded, false if an exception occured in the Java code
virtual bool scanFile(const char* path, long long lastModified, long long fileSize)
{
// 調用JNIEnv的CallVoidMethod函數:
// 第一個參數代表 MediaScannerClient 的 jobject 對象,第二個參數代表 scanFile的 jMethodID
// 后面的是JAVA層 ScanFile 函數參數 
    mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);
}


實現上JNIEnv提供了一系列類似的函數調用JAVA函數:


NativeType CallMethod(JNIEnv *env, jobject obj,jmethodID methodID,...)


同時也可以設定或獲取JAVA層的成員變量值,定義如下:
NativeType GetField(JNIEnv *env,jobject obj,jfieldID fieldID);
NativeTYpe SetField(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value);
</span>

 本文由用戶 fmms 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!