JNI源碼分析(並實現JNI動態註冊)

本篇來自 微信公衆號郭霖 中 李樟清 的投稿,分析了Java和C++語言如何通過so文件交互的,希望對大家有所幫助!

李樟清 的博客地址: http://blog.csdn.net/urrjdg  

C/C++的編譯和鏈接


c/c++ ========= 二進制文件

對於C/C++ 一般分爲兩個階段

1. 編譯

xxx.c ——> windows .obj ; Linux .o –》 語法檢查

2. 鏈接

.o —–> log.so .dll .exe

舉例: 
a.c a.h b.c b.h 
a.c –>b.h(test方法)

在編譯階段只會去找b.h有沒有test方法,而在鏈接的階段,他會在b.o當中去找這個test方法

如果沒有test方法會 報 LinkErro 錯誤。而這個 LinkErro 錯誤一般是因爲我們在一個文件當中引入了一個.h文件,並且使用了這個文件當中的這個方法,而這個對應的.h文件對應的.o文件(中間文件)裏面沒有這個方法的實現體。


編譯器


將這個C/C++編譯鏈接生成二進制文件的這個過程是誰做的?

是 編譯器,編譯規則:

Eclipse

GUN編譯器  ----> 編譯規則 Android.mk (log.so是android自帶的)

Android Studio

LLVM編譯器 ----> 編譯規則 CMakeList.txt


使用Android Studio 創建工程


android studio 會給我們提供一個 exceptiosns support 異常支持

javah 生成頭文件

public class FileUtils { 

    public static native void diff(String path,String pattern_Path,int file_num); 

    public static void javaDiff(String path,String pattern_Path,int file_num){} 

    // Used to load the 'native-lib' library on application startup. 
    static { 
        System.loadLibrary("native-lib"); 
    } 
}

jvm 是虛擬機內存,C/C++ 是 native內存,並且這個 so庫 是放在 apk 的 lib 下面的

那這個so庫 ,系統是怎麼找到的?System.loadLibrary是怎麼來找到的?並且系統是如何來區分(JVM是怎麼來區分 native 方法(diff)和 javaDiff方法)

native 關鍵字起到什麼作用?loadLibrary 做了什麼?

當我們調用 javaDiff 的時候會到 Java虛擬機 的內存當中來處理找這個方法,而加了 native 關鍵字的時候他就會去到 C++ 的堆棧空間找這個 C++ 的實現。 

爲什麼 native 會這樣,起了什麼作用?

先在看聲明瞭 native 的方法和沒有聲明 native 方法之間的區別。

使用 javap -s -p -v FileUtils.class。找到這兩個方法,可以看到這兩個方法的區別在於 flag ,native 聲明的方法 多了個 ACC_NATIVE 的 flag。也就是說 java 在執行這個文件的時候 ,對於有 ACC_NATIVE 的 flag 的方法,他就會去 native 區間去找,如果沒有ACC_NATIVE 這個 flag 就在本地的虛擬機空間來找這個方法。

C:\Users\Zeking\Desktop\Lsn9\app\src\main\java\com\example\zeking\lsn9>javap -s -p -v FileUtils.class 
Classfile /C:/Users/Zeking/Desktop/Lsn9/app/src/main/java/com/example/zeking/lsn9/FileUtils.class 
  Last modified 2017-9-2; size 469 bytes 
  MD5 checksum 19201ed5479758e0dfffb63528653a65 
  Compiled from "FileUtils.java" 
public class com.example.zeking.lsn9.FileUtils 
  minor version: 0 
  major version: 52 
  flags: ACC_PUBLIC, ACC_SUPER 
Constant pool: 
   #1 = Methodref          #5.#16         // java/lang/Object."<init>":()V 
   #2 = String             #17            // native-lib 
   #3 = Methodref          #18.#19        // java/lang/System.loadLibrary:(Ljava/lang/String;)V 
   #4 = Class              #20            // com/example/zeking/lsn9/FileUtils 
   #5 = Class              #21            // java/lang/Object 
   #6 = Utf8               <init> 
   #7 = Utf8               ()V 
   #8 = Utf8               Code 
   #9 = Utf8               LineNumberTable 
  #10 = Utf8               diff 
  #11 = Utf8               (Ljava/lang/String;Ljava/lang/String;I)V 
  #12 = Utf8               javaDiff 
  #13 = Utf8               <clinit> 
  #14 = Utf8               SourceFile 
  #15 = Utf8               FileUtils.java 
  #16 = NameAndType        #6:#7          // "<init>":()V 
  #17 = Utf8               native-lib 
  #18 = Class              #22            // java/lang/System 
  #19 = NameAndType        #23:#24        // loadLibrary:(Ljava/lang/String;)V 
  #20 = Utf8               com/example/zeking/lsn9/FileUtils 
  #21 = Utf8               java/lang/Object 
  #22 = Utf8               java/lang/System 
  #23 = Utf8               loadLibrary 
  #24 = Utf8               (Ljava/lang/String;)V 
{ 
  public com.example.zeking.lsn9.FileUtils(); 
    descriptor: ()V 
    flags: ACC_PUBLIC 
    Code: 
      stack=1, locals=1, args_size=1 
         0: aload_0 
         1: invokespecial #1      // Method java/lang/Object."<init>":()V 
         4: return 
      LineNumberTable: 
        line 7: 0 

  public static native void diff(java.lang.String, java.lang.String, int); 
    descriptor: (Ljava/lang/String;Ljava/lang/String;I)V  
    flags: ACC_PUBLIC, ACC_STATIC, ACC_NATIVE  // 這邊多了個 ACC_NATIVE 代表是native 

  public static void javaDiff(java.lang.String, java.lang.String, int); 
    descriptor: (Ljava/lang/String;Ljava/lang/String;I)V 
    flags: ACC_PUBLIC, ACC_STATIC 
    Code: 
      stack=0, locals=3, args_size=3 
         0: return 
      LineNumberTable: 
        line 11: 0 

  static {}; 
    descriptor: ()V 
    flags: ACC_STATIC 
    Code: 
      stack=1, locals=0, args_size=0 
         0: ldc           #2         // String native-lib 
         2: invokestatic  #3         // Method java/lang/System.loadLibrary:(Ljava/lang/String;)V 
         5: return 
      LineNumberTable: 
        line 15: 0 
        line 16: 5 
} 
SourceFile: "FileUtils.java"


System.loadLibrary找到so庫文件分析


native的方法棧爲什麼能被jvm調用到?從 System.loadLibrary 入手

System.loadLibrary("native-lib");

System.java

public static void loadLibrary(String libname) { 
    Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}

Runtime.java

synchronized void loadLibrary0(ClassLoader loader, String libname) { 
    if (libname.indexOf((int)File.separatorChar) != -1) { 
        throw new UnsatisfiedLinkError( 
            "Directory separator should not appear in library name: " + libname); 
    } 
    String libraryName = libname; 
    if (loader != null) { 
        // 點進去發現是return null;找到so庫的全路徑 
        String filename = loader.findLibrary(libraryName); 
        if (filename == null) { 
            // It's not necessarily true that the ClassLoader used 
            // System.mapLibraryName, but the default setup does, and it's 
            // misleading to say we didn't find "libMyLibrary.so" when we 
            // actually searched for "liblibMyLibrary.so.so". 
            throw new UnsatisfiedLinkError(loader + " couldn't find \"" + 
                                           System.mapLibraryName(libraryName) + "\""); 
        } 
        String error = doLoad(filename, loader); 
        if (error != null) { 
            throw new UnsatisfiedLinkError(error); 
        } 
        return; 
    } 
    String filename = System.mapLibraryName(libraryName); 
    List<String> candidates = new ArrayList<String>(); 
    String lastError = null; 
    for (String directory : getLibPaths()) { 
        String candidate = directory + filename; 
        candidates.add(candidate); 
        if (IoUtils.canOpenReadOnly(candidate)) { 
            String error = doLoad(candidate, loader); 
            if (error == null) { 
                return; // We successfully loaded the library. Job done. 
            } 
            lastError = error; 
        } 
    } 
    if (lastError != null) { 
        throw new UnsatisfiedLinkError(lastError); 
    } 
    throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates); 
}

所以可以想到 應該是 ClassLoader的實現類去實現了這個 findLibrary方法。 
怎麼找是哪個實現類 實現的呢?

Log.i(TAG,this.getClassLoader().toString());

dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.zeking.lsn9-1/base.apk",
zip file "/data/app/com.example.zeking.lsn9-1/split_lib_dependencies_apk.apk",
zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_0_apk.apk", zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_1_apk.apk",
zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_2_apk.apk", zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_3_apk.apk", 
zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_4_apk.apk", zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_5_apk.apk",
zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_6_apk.apk", zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_7_apk.apk", zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_8_apk.apk", zip file "/data/app/com.example.zeking.lsn9-1/split_lib_slice_9_apk.apk"],
nativeLibraryDirectories=[/data/app/com.example.zeking.lsn9-1/lib/arm64, /data/app/com.example.zeking.lsn9-1/base.apk!/lib/arm64-v8a,
/data/app/com.example.zeking.lsn9-1/split_lib_dependencies_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_0_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_1_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_2_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_3_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_4_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_5_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_6_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_7_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_8_apk.apk!/lib/arm64-v8a, /data/app/com.example.zeking.lsn9-1/split_lib_slice_9_apk.apk!/lib/arm64-v8a,
/vendor/lib64, /system/lib64]]]

從上面可以看出是 PathClassLoader。PathClassLoader .java 這裏面沒有 findLibrary 繼續進到 BaseDexClassLoader

public class PathClassLoader extends BaseDexClassLoader {
    ......
}

BaseDexClassLoader.java

DexPathList.java

首先我們先來看

DexPathList .java 中的 String fileName = System.mapLibraryName(libraryName);

System.java 看註釋可以看出 ,是根據你的平臺來找你的 so庫

再繼續看 for (Element element : nativeLibraryPathElements)

DexPathList .java 可以看到 nativeLibraryPathElements 是在 DexPathList的構造函數裏面初始化的

public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) {  
    ......  
    // 找so庫是從兩個地方來找,  
    // 1.在BaseDexClassLoader初始化的時候傳入的目錄 這個目錄是 librarySearchPath,這個就是應用apk下面的解壓的lib目錄下  
    // 2. 在系統的環境變量裏面,System.getProperty("java.library.path"):這個目錄通過Log.i(TAG,System.getProperty("java.library.path"));  
    // 打印出來是/vendor/lib64:/system/lib64 或者  /vendor/lib:/system/lib  
    // dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.zeking.lsn9-1.apk"],  
    // nativeLibraryDirectories=[/data/app-lib/com.example.zeking.lsn9-1, /system/lib]]]  
    // /data/app-lib/com.example.zeking.lsn9-1,   
    // /system/lib  

    this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);  
    // 這個是系統裏面 java.library.path   
    this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true);  
    List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);  
    allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);  
    // 就是在這邊進行初始化的  
    this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories,suppressedExceptions,definingContext);  
    ......  
}


System.loadLibrary加載so庫分析


分析下他是怎麼加載so庫的

現在回到 Runtime.java 的 loadLibrary0 方法找到他的 doLoad 方法

synchronized void loadLibrary0(ClassLoader loader, String libname) { 
    if (libname.indexOf((int)File.separatorChar) != -1) { 
        throw new UnsatisfiedLinkError( 
        "Directory separator should not appear in library name: " + libname); 
    } 
    String libraryName = libname; 
    if (loader != null) { 
        String filename = loader.findLibrary(libraryName); // 找到so庫的全路徑 
        if (filename == null) { 
            // It's not necessarily true that the ClassLoader used 
            // System.mapLibraryName, but the default setup does, and it's 
            // misleading to say we didn't find "libMyLibrary.so" when we 
            // actually searched for "liblibMyLibrary.so.so". 
            throw new UnsatisfiedLinkError(loader + " couldn't find \"" + 
                                           System.mapLibraryName(libraryName) + "\""); 
        } 
        String error = doLoad(filename, loader); 
        if (error != null) { 
            throw new UnsatisfiedLinkError(error); 
        } 
        return; 
    } 

    String filename = System.mapLibraryName(libraryName); 
    List<String> candidates = new ArrayList<String>(); 
    String lastError = null; 
    for (String directory : getLibPaths()) { 
        String candidate = directory + filename; 
        candidates.add(candidate); 

        if (IoUtils.canOpenReadOnly(candidate)) { 
            String error = doLoad(candidate, loader); 
            if (error == null) { 
                return; // We successfully loaded the library. Job done. 
            } 
            lastError = error; 
        } 
    } 

    if (lastError != null) { 
        throw new UnsatisfiedLinkError(lastError); 
    } 
    throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates); 
}

doLoad 方法

private String doLoad(String name, ClassLoader loader) { 
    if (loader != null && loader instanceof BaseDexClassLoader) { 
        BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader; 
        librarySearchPath = dexClassLoader.getLdLibraryPath(); 
    } 
    synchronized (this) { 
        // 這一邊 
        return nativeLoad(name, loader, librarySearchPath); 
    } 
} 

// 這一邊
private static native String nativeLoad(String filename, ClassLoader loader,                                            String librarySearchPath);

nativeLoad 方法 要去 runtime.c(java_lang_Runtime.cc)android-7.1.0_r1.7z\android-7.1.0_r1\libcore\ojluni\src\main\native\runtime.c

以下是 Runtime.c 的源碼

#include "jni.h"
#include "jni_util.h"
#include "jvm.h"

#include "JNIHelp.h"

#define NATIVE_METHOD(className, functionName, signature) \ { #functionName, signature, (void*)(className ## _ ## functionName) } JNIEXPORT jlong JNICALL Runtime_freeMemory(JNIEnv *env, jobject this) {    return JVM_FreeMemory(); } JNIEXPORT jlong JNICALL Runtime_totalMemory(JNIEnv *env, jobject this) {    return JVM_TotalMemory(); } JNIEXPORT jlong JNICALL Runtime_maxMemory(JNIEnv *env, jobject this) {    return JVM_MaxMemory(); } JNIEXPORT void JNICALL Runtime_gc(JNIEnv *env, jobject this) {    JVM_GC(); } JNIEXPORT void JNICALL Runtime_nativeExit(JNIEnv *env, jclass this, jint status) {    JVM_Exit(status); } // 這個就是 nativeLoad 方法 的實現 JNIEXPORT jstring JNICALL Runtime_nativeLoad(JNIEnv *env, jclass ignored, jstring javaFilename,                   jobject javaLoader, jstring javaLibrarySearchPath) {    // JVM_NativeLoad 方法 在 OpenjdkJvm.cc 中    return JVM_NativeLoad(env, javaFilename, javaLoader, javaLibrarySearchPath); } static JNINativeMethod gMethods[] = {    // 使用了一個 NATIVE_METHOD 的 宏替換 ,這個宏替換在這個類的頂部    NATIVE_METHOD(Runtime, freeMemory, "!()J"),    NATIVE_METHOD(Runtime, totalMemory, "!()J"),    NATIVE_METHOD(Runtime, maxMemory, "!()J"),    NATIVE_METHOD(Runtime, gc, "()V"),    NATIVE_METHOD(Runtime, nativeExit, "(I)V"),    NATIVE_METHOD(Runtime, nativeLoad,           "(Ljava/lang/String;Ljava/lang/ClassLoader;Ljava/lang/String;)"                "Ljava/lang/String;"), }; void register_java_lang_Runtime(JNIEnv *env) {    jniRegisterNativeMethods(env, "java/lang/Runtime", gMethods, NELEM(gMethods)); }

下面就是 OpenjdkJvm.cc

JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env,   
                                 jstring javaFilename,   
                                 jobject javaLoader,   
                                 jstring javaLibrarySearchPath) {   
    ScopedUtfChars filename(env, javaFilename);   
    if (filename.c_str() == NULL) {   
        return NULL;   
    }   

    std::string error_msg;   

    // 這邊 有一個 JavaVMExt  , 這個方法的參數有一個 JNIEnv 。   
    // 那好,JavaVM* 和 JNIEnv 有什麼區別呢?   
    // JavaVM* : 一個android應用的進程,有且僅有一個javaVm   
    // JNIEnv :每個java線程都對應一個env的環境變量   
    // 虛擬機裏面jvm 是怎麼找到具體的so庫的堆棧的?   
    // 他調用了 JavaVM的loadNativeLibrary 方法裏面,   
    // 創建了一個結構體(這個結構體,包一個的指針,這個指針放我們真實加載完操作的文件地址)   
    // 在這個結構體裏面將我傳進來的動態庫()filename.c_str())加到結構體裏面,然後保存到VM裏面,   
    // 那麼對於我的android進程其他的地方,我只要拿到這個VM,就能找到這個結構體,   
    // 通過這個結構體,就能找到這個so庫裏面的方法棧和引用內存   
    art::JavaVMExt* vm = art::Runtime::Current()->GetJavaVM();   
    // vm->LoadNativeLibrary 方法 在  java_vm_ext.cc    
    bool success = vm->LoadNativeLibrary(env,   
                                         filename.c_str(),   
                                         javaLoader,   
                                         javaLibrarySearchPath,   
                                         &error_msg);   
    if (success) {   
      return nullptr;   
    }   
}

Java_vm_ext.h

關鍵是與JVM的聯繫:android進程,有且只有一個 JavaVMExt* 指針對象,當我們在 LoadNativeLibrary 的時候,new 了一個 SharedLibrary 的對象指針,而 SharedLibrary 保存了 handle 句柄,然後在找文件方法的時候,都是通過對象裏面的 handle 句柄來進行操作的,library 有一個 FindSymbol 來找方法,找到 JNI_OnLoad 方法去做具體的調用,這就是JNI設計的流程


JNI動態註冊


根據以上的分析進行實現

#include "com_example_zeking_FileUtils.h"
#include <android/log.h> #include <assert.h> //int __android_log_print(int prio, const char* tag, const char* fmt, ...)
#define TAG "Zeking_JNI"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0]))) /* * Class:     com_example_zekign_FileUtils * Method:    diff * Signature: (Ljava/lang/String;Ljava/lang/String;I)V */
JNIEXPORT void JNICALL native_diff      (JNIEnv *env, jclass clazz, jstring path, jstring pattern_Path, jint file_num)
{    LOGI("JNI begin 動態註冊的方法 "); } static const JNINativeMethod gMethods[] = {    {        "diff","(Ljava/lang/String;Ljava/lang/String;I)V",(void*)native_diff    } }; static int registerNatives(JNIEnv* engv) {    LOGI("registerNatives begin");    jclass  clazz;    clazz = (*engv) -> FindClass(engv, "com/example/zeking/FileUtils");    if (clazz == NULL) {        LOGI("clazz is null");        return JNI_FALSE;    }    if ((*engv) ->RegisterNatives(engv, clazz, gMethods, NELEM(gMethods)) < 0) {        LOGI("RegisterNatives error");        return JNI_FALSE;    }    return JNI_TRUE; } JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){    LOGI("jni_OnLoad begin");    JNIEnv* env = NULL;    jint result = -1;    if ((*vm)->GetEnv(vm,(void**) &env, JNI_VERSION_1_4) != JNI_OK) {        LOGI("ERROR: GetEnv failed\n");        return -1;    }    assert(env != NULL);    registerNatives(env);    return JNI_VERSION_1_4; }

靜態註冊:

每個 class 都需要使用 javah 生成一個頭文件,並且生成的名字很長書寫不便;初次調用時需要依據名字搜索對應的JNI層函數來建立關聯關係,會影響運行效率。用 javah 生成頭文件方便簡單

  1. javah 生成一個頭文件,操作簡單

  2. 名字很長,書寫不方便

  3. 初次調用的使用,需要依據名字搜索對應的 FindSymbol(具體看 Runctime.c) 
    來找到對應的方法,如果方法數較多的時候,效率不高

動態註冊:

  1. 第一次調用效率高

  2. 使用一種數據結構 JNINativeMethod 來記錄 java native函數 和 JNI函數 的對應關係

  3. 移植方便,便於維護(一個java文件中有多個native方法,只要修改下gMethods 的映射關係)

由於原文過長,本文進行了一些適當的修剪。想要閱讀完整文章的朋友,請點擊的下方閱讀原文,到作者的博客當中查看。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章