(七)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檔案的包名更換後)。