1. 程式人生 > >(七)JNI 原始碼分析、動態註冊

(七)JNI 原始碼分析、動態註冊

一、native 作用

JNITest :

public class JNITest {
    static {
        System.loadLibrary("native-lib");
    }
    public static native String getString();

    public static String getJavaString(){
        return "java string";
    }
}

我們知道,JNI 宣告的方法需要加上關鍵字 native,當呼叫到這個方法的時候,虛擬機器會去對應的 .so 檔案中查詢該方法,並呼叫。

在控制檯切換到 JNITest.java 所在的目錄下,使用 javac JNITest.java 命令進行編譯,生產 .class 檔案(eclipse 下會自動生產)。

這裡寫圖片描述

接著使用 javap -v JNITest 命令對 JNITest 進行反編譯,下拉找到 getString() 和 getJavaString()兩個方法。可以發現, native 的方法在 flags 多了一個 ACC_NATIVE 這個標誌。

這裡寫圖片描述

java 在執行到 JNITest 的時候,對於 flags 中有 ACC_NATIVE 這個標誌的方法,就會去 native 區間去尋找這個方法,沒有這個標誌的話就在本地虛擬機器中尋找該方法的實現。

二、so 庫

1.尋找 .so 庫

    static {
        System.loadLibrary("native-lib");
    }

這邊從載入庫檔案 System.loadLibrary(“native-lib”) 開始分析,點選檢視原始碼。

System.loadLibrary:

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

檢視 Runtime.getRuntime() 方法,非常典型的單例模式,返回一個 Runtime 。

Runtime 的 getRuntime:

    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

接著呼叫 Runtime 的 loadLibrary0 方法,傳進去兩個引數,VMStack.getCallingClassLoader() 是當前棧的類載入器,這個方法本身也是一個 native 的,不繼續深入。libname 就是我們傳進來的 .so 庫的名字。

Runtime 的 loadLibrary0:

   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);
            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;
        }

        ...
    }

正常情況下 loader 是不會為 null 的,所以後面流程不分析。繼續分析 loader 不為空的情況, loader.findLibrary(libraryName) 直接點進去會發現是返回一個 null,方法的引數是 ClassLoader,實際執行時候傳進來的是 ClassLoader 的子類。

在程式碼中新增日誌, 把實際執行中的 ClassLoader 打印出來,會發現是 PathClassLoader,但是 PathClassLoader 只是簡單的實現了一下,沒有重寫 findLibrary 這個方法,這個方法是在 PathClassLoader 的父類 BaseDexClassLoader 中。

Log.d(TAG, "onCreate: ClassLoader" + this.getClassLoader().toString());

這裡寫圖片描述

PathClassLoader:

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}

BaseDexClassLoader 中 findLibrary 是呼叫了 pathList 的 findLibrary 方法,pathList 是在 BaseDexClassLoader 的建構函式中進行初始化。

BaseDexClassLoader:

public class BaseDexClassLoader extends ClassLoader {

    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

    ...

    @Override
    public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }
    ...
}

DexPathList 的 findLibrary:

   public String findLibrary(String libraryName) {
        String fileName = System.mapLibraryName(libraryName);
        for (File directory : nativeLibraryDirectories) {
            String path = new File(directory, fileName).getPath();
            if (IoUtils.canOpenReadOnly(path)) {
                return path;
            }
        }
        return null;
   }

System.mapLibraryName(libraryName) 是一個native 方法,根據註釋可以知道,是根據執行的平臺獲取到檔案的名稱,我們原先載入庫的時候只傳入檔名,沒有後綴,在這裡會把字尾加上去。然後遍歷 nativeLibraryDirectories,nativeLibraryDirectories 是一個 File 陣列,在這些路徑下尋找對應的 fileName 檔案。

nativeLibraryDirectories 包含兩大路徑,一個是 BaseDexClassLoader 建構函式中傳遞進來的 libraryPath,這個路徑是 apk 下 lib 中新增的 so庫路勁,可以把一個apk解壓出來檢視 lib 檔案下的目錄。還有一個路徑是 System.getProperty(“java.library.path”),這個對應的是系統的環境變數裡面,可以用日誌打印出來。

apk 下新增的 so:

這裡寫圖片描述

System.getProperty(“java.library.path”):

這裡寫圖片描述

系統路徑又分為兩個,/vendor/lib 是廠商路徑,/system/lib 是系統路徑,中間用 : 隔開。

DexPathList :

final class DexPathList {

    private final File[] nativeLibraryDirectories;

    public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {

        ...

        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    }

    private static File[] splitLibraryPath(String path) {
        ArrayList<File> result = splitPaths(path, System.getProperty("java.library.path"), true);
        return result.toArray(new File[result.size()]);
    }

    private static ArrayList<File> splitPaths(String path1, String path2,
            boolean wantDirectories) {
        ArrayList<File> result = new ArrayList<File>();

        splitAndAdd(path1, wantDirectories, result);
        splitAndAdd(path2, wantDirectories, result);
        return result;
    }

    private static void splitAndAdd(String searchPath, boolean directoriesOnly,
            ArrayList<File> resultList) {
        if (searchPath == null) {
            return;
        }
        for (String path : searchPath.split(":")) {
            try {
                StructStat sb = Libcore.os.stat(path);
                if (!directoriesOnly || S_ISDIR(sb.st_mode)) {
                    resultList.add(new File(path));
                }
            } catch (ErrnoException ignored) {
            }
        }
    }
}

小結:在安卓環境中,JNI 執行時載入的 so 庫,一個是從 apk 中新增的 lib目錄下去搜索,一個是系統環境變數下搜尋。

上面列印 ClassLoader 的時候,會把 DexPathList 一起打印出來。

這裡寫圖片描述

2.載入 .so 庫

繼續 Runtime 的 loadLibrary0 方法往下,找到 .so 庫的路徑後,執行 doLoad(filename, loader) 方法。

Runtime 的 loadLibrary0:

   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);
            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;
        }

        ...
    }

Runtime 的 doLoad:

    private String doLoad(String name, ClassLoader loader) {
        // Android apps are forked from the zygote, so they can't have a custom LD_LIBRARY_PATH,
        // which means that by default an app's shared library directory isn't on LD_LIBRARY_PATH.

        // The PathClassLoader set up by frameworks/base knows the appropriate path, so we can load
        // libraries with no dependencies just fine, but an app that has multiple libraries that
        // depend on each other needed to load them in most-dependent-first order.

        // We added API to Android's dynamic linker so we can update the library path used for
        // the currently-running process. We pull the desired path out of the ClassLoader here
        // and pass it to nativeLoad so that it can call the private dynamic linker API.

        // We didn't just change frameworks/base to update the LD_LIBRARY_PATH once at the
        // beginning because multiple apks can run in the same process and third party code can
        // use its own BaseDexClassLoader.

        // We didn't just add a dlopen_with_custom_LD_LIBRARY_PATH call because we wanted any
        // dlopen(3) calls made from a .so's JNI_OnLoad to work too.

        // So, find out what the native library search path is for the ClassLoader in question...
        String librarySearchPath = null;
        if (loader != null && loader instanceof BaseDexClassLoader) {
            BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
            librarySearchPath = dexClassLoader.getLdLibraryPath();
        }
        // nativeLoad should be synchronized so there's only one LD_LIBRARY_PATH in use regardless
        // of how many ClassLoaders are in the system, but dalvik doesn't support synchronized
        // internal natives.
        synchronized (this) {
            return nativeLoad(name, loader, librarySearchPath);
        }
    }

在 Runtime 的 doLoad 的末尾,呼叫了 nativeLoad(name, loader, librarySearchPath) 這個方法去載入 so 庫,

nativeLoad 這個方法的實現是在 Android 系統原始碼,不是 Android 原始碼。在 /libcore/ojluni/src/main/native/Runtime.c 下。

nativeLoad 的 C 實現:

JNIEXPORT jstring JNICALL
Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename,
                   jobject javaLoader, jstring javaLibrarySearchPath)
{
    return JVM_NativeLoad(env, javaFilename, javaLoader, javaLibrarySearchPath);
}

注:nativeLoad 的 C 實現的方法名與我們前面要求的 JNI 方法名規則明顯不符,這邊採用的是動態註冊方式。

nativeLoad 呼叫了 JVM_NativeLoad 這個方法,這個是位於安卓系統原始碼的 art/runtime/openjdkjvm/OpenjdkJvm.cc 下。

JVM_NativeLoad:

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;
  {
    art::JavaVMExt* vm = art::Runtime::Current()->GetJavaVM();
    bool success = vm->LoadNativeLibrary(env,
                                         filename.c_str(),
                                         javaLoader,
                                         javaLibrarySearchPath,
                                         &error_msg);
    if (success) {
      return nullptr;
    }
  }

  // Don't let a pending exception from JNI_OnLoad cause a CheckJNI issue with NewStringUTF.
  env->ExceptionClear();
  return env->NewStringUTF(error_msg.c_str());
}

在 JVM_NativeLoad 中獲取到 javaVM,對於一個應用程式來說只有一個 javaVM,同事吧載入的 so 庫下的方法存放在 javaVM 中,這樣在其他地方呼叫 JNI 方法的時候,只要獲取到當前應用的 javaVM 即可獲取到要呼叫的方法。

三、動態註冊

動態註冊的實現主要在 JVM_NativeLoad 下的 LoadNativeLibrary 方法(程式碼較複雜,只提供具體思路)。
LoadNativeLibrary()

---->sym = library->FindSymbol("JNI_OnLoad", nullptr);

在我們要載入 so 庫中查詢是否含有 JNI_OnLoad 這個方法,如果沒有系統就認為是靜態註冊方式進行的,直接返回 true,代表 so 庫載入成功;如果有找到 JNI_OnLoad 認為是動態註冊的,然後呼叫JNI_OnLoad 方法,JNI_OnLoad 方法中一般存放的是方法註冊的函式。所以如果採用動態註冊就必須要實現 JNI_OnLoad 方法,否則呼叫 java 中申明的 native 方法時會丟擲異常。

動態載入時候, java 與 C/C++ 方法間的對映關係是使用 jni.h 中的 JNINativeMethod 結構。

typedef struct {
    const char* name; //java層函式名
    const char* signature; //函式的簽名信息
    void* fnPtr; //C/C++ 中對應的函式指標。
} JNINativeMethod;

下面是一個動態載入的 demo,這是模仿底層動態載入的過程進行載入。

C++:

#include <jni.h>
#include <string>
#include <android/log.h>
#include <assert.h>

#define TAG "JNITest"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)

//陣列大小
# define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))

extern "C"

JNIEXPORT jstring JNICALL native_getString
        (JNIEnv *env, jclass jclz){
    LOGI("JNI test 動態註冊");

    return env->NewStringUTF("JNI test return");
}

//gMethods 記錄所有動態註冊方法的對映關係
static const JNINativeMethod gMethods[] = {
        {
                "getString","()Ljava/lang/String;",(void*)native_getString
        }
};

static int registerNatives(JNIEnv *env)
{
    LOGI("registerNatives begin");
    jclass  clazz;
    clazz = env->FindClass("com/xiaoyue/jnidemo/JNITest");

    if (clazz == NULL) {
        LOGI("clazz is null");
        return JNI_FALSE;
    }

    if (env->RegisterNatives(clazz, gMethods, NELEM(gMethods)) < 0) {
        LOGI("RegisterNatives error");
        return JNI_FALSE;
    }

    return JNI_TRUE;
}

//會自動呼叫 JNI_OnLoad 方法
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
{

    LOGI("jni_OnLoad begin");

    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((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 生成標頭檔案方便簡單。

動態註冊:使用一種資料結構 JNINativeMethod 來記錄 java native 函式和 JNI 函式的對應關係,
移植方便(一個java檔案中有多個native方法,java檔案的包名更換後)。