Runtime.loadLibrary() 源碼分析
最近的用戶反饋威创,碰到一個(gè) loadLibrary() 失敗的問題病蛉,之前對(duì)這一個(gè)流程一直沒有進(jìn)行細(xì)致梳理杨帽,現(xiàn)在趁有空歹袁,梳理一下。
loadLibrary() 的流程
一般情況下靶瘸,通過 System.loadLibrary() 去加載你需要的 so 庫苫亦,如下:
System.loadLibrary("native-lib")
System 調(diào)用的代碼如下:
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
實(shí)際上毛肋,調(diào)用的是 Runtime 的 loadLibrary0() 函數(shù),Runtime 是每個(gè) Java 應(yīng)用的一個(gè)運(yùn)行時(shí)屋剑,并且getRutime() 獲得Runtime 對(duì)象是個(gè)單例:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
}
最終 loadLibrary0() 的源碼如下:
synchronized void loadLibrary0(ClassLoader loader, String libname) {
if (libname.indexOf((int)File.separatorChar) != -1) {//判斷so名稱是否包含文件分隔符润匙,如果包含文件分隔符,則拋出異常
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
String libraryName = libname;
if (loader != null) {
String filename = loader.findLibrary(libraryName);//通過 ClassLoader 去 findLibrary()
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 = nativeLoad(filename, loader);//調(diào)用 jni 方法唉匾,nativeLoad() 方法
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
String filename = System.mapLibraryName(libraryName);//使用 mapLibraryName() 方法查找實(shí)際的 so 庫的名稱
List<String> candidates = new ArrayList<String>();
String lastError = null;
for (String directory : getLibPaths()) {//遍歷so 庫目錄孕讳,嘗試加載 so
String candidate = directory + filename;
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
String error = nativeLoad(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);
}
首先注意到 loadLibrary0() 方法是 synchronize 的,所以如果多個(gè)線程調(diào)用巍膘,則需要等待厂财。 loadLibrary0() 方法主要分為以下幾步調(diào)用流程:
- 檢查 so 庫名稱,是否包含了文件分割符峡懈,如果包含則直接拋出異常璃饱。
- 如果 ClassLoader 不為空,調(diào)用 ClassLoader.findLibrary() 去查找so肪康,如果找不到則拋出 XXClassLoader couldn't find XXX.so.
- 如果 ClassLoader 不為空,調(diào)用 nativeLoad() 方法荚恶,去加載 so。源碼在libcore/ojluni/src/main/native/Runtime.c磷支,找到則返回谒撼。
后面的分支是 ClassLoader 為空的情況
- 如果 ClassLoader 為空,調(diào)用 System.mapLibraryName() 去獲取so 庫的完整名稱
- 如果 ClassLoader 為空,遍歷 getLibPaths()去查找 so齐唆,并且獲取路徑嗤栓。
- 調(diào)用 nativeLoad() 方法,使用完整 so 路徑去加載 so 庫
綜上箍邮,我們只要分析 ClassLoader.findLibrary() 和 nativeLoad() 方法即可茉帅。
loader.findLibrary() 方法
以包名為com.rockets.livedemo 為例子,對(duì)應(yīng)的 ClassLoader 為 PathClassLoader 如下:
dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.rockets.livedemo-ABxsabDzcYLzAaEA5wukSw==/base.apk"],nativeLibraryDirectories=[/data/app/com.rockets.livedemo-ABxsabDzcYLzAaEA5wukSw==/lib/arm, /data/app/com.rockets.livedemo-ABxsabDzcYLzAaEA5wukSw==/base.apk!/lib/armeabi-v7a, /system/lib]]]
其中锭弊,fileName 為:
/data/app/com.rockets.livedemo-ABxsabDzcYLzAaEA5wukSw==/lib/arm/libnative-lib.so
這里使用的 ClassLoader 是 PatchClassLoader ,其并沒有實(shí)現(xiàn) findLibrary() 方法堪澎,而是由其子類 BaseDexClassLoader 實(shí)現(xiàn)的,源碼如下:
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
這里交由 DexPathList 的 findLibrary() 方法去實(shí)現(xiàn)味滞,如下:
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);
for (NativeLibraryElement element : nativeLibraryPathElements) {
String path = element.findNativeLibrary(fileName);
if (path != null) {
return path;
}
}
return null;
}
這里會(huì)先調(diào)用System.mapLibraryName() 去查找 so 庫的完整名稱樱蛤,具體的可以看后面針對(duì)這個(gè)函數(shù)的分析,總之這里會(huì)從你傳入的 native-lib 名稱剑鞍,變成 libnative-lib.so 這樣完整的名稱昨凡。
拿到完整的 so 庫的名稱之后,會(huì)通過遍歷 nativeLibraryPathElements 去查找 so 庫蚁署,那么 nativeLibraryPathElemts 是怎么初始化的便脊?如下:
NativeLibraryElement[] nativeLibraryPathElements;
/** List of application native library directories. */
private final List<File> nativeLibraryDirectories;
/** List of system native library directories. */
private final List<File> systemNativeLibraryDirectories;
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);
nativeLibraryDirectories 的獲取路徑是在安裝過程,賦值到 ApplicationInfo 中去的光戈,如下:
public class ApplicationInfo extends PackageItemInfo implements Parcelable {
/**
* Full path to the directory where native JNI libraries are stored.
*/
public String nativeLibraryDir;
}
具體細(xì)節(jié)不分析哪痰,總的來說遂赠,如果 apk 是系統(tǒng)應(yīng)用,則 nativeLibraryDir 為 /system/lib/xxx 或者 system/app/xxx/lib晌杰。但是對(duì)于我們自己的應(yīng)用來說跷睦,這個(gè)值為 data/app/包名/lib。
以華為手機(jī)肋演,安裝抖音極速版為例:
抖音極速版的lib 目錄為 /data/data/com.ss.android.ugc.aweme.lite/lib抑诸,其中 com.ss.android.ugc.aweme.lite 為包名。
systemNativeLibraryDirectories 是通過靜態(tài)方法 getProperty() 去獲取惋啃,這里的代碼比較復(fù)雜哼鬓,直接跳過,最終我們獲取到的路徑是:
//http://androidxref.com/9.0.0_r3/xref/bionic/linker/linker.cpp
#if defined(__LP64__)
static const char* const kSystemLibDir = "/system/lib64";
static const char* const kOdmLibDir = "/odm/lib64";
static const char* const kVendorLibDir = "/vendor/lib64";
static const char* const kAsanSystemLibDir = "/data/asan/system/lib64";
static const char* const kAsanOdmLibDir = "/data/asan/odm/lib64";
static const char* const kAsanVendorLibDir = "/data/asan/vendor/lib64";
#else
static const char* const kSystemLibDir = "/system/lib";
static const char* const kOdmLibDir = "/odm/lib";
static const char* const kVendorLibDir = "/vendor/lib";
static const char* const kAsanSystemLibDir = "/data/asan/system/lib";
static const char* const kAsanOdmLibDir = "/data/asan/odm/lib";
static const char* const kAsanVendorLibDir = "/data/asan/vendor/lib";
#endif
static const char* const kAsanLibDirPrefix = "/data/asan";
static const char* const kDefaultLdPaths[] = {
kSystemLibDir,
kOdmLibDir,
kVendorLibDir,
nullptr
};
也就是說边灭,如果是有宏定義 LP64 則定義為 /system/lib64,/odm/lib64健盒,/vendor/lib64绒瘦,如果沒有這個(gè)宏定義則為 /system/lib,/odm/lib扣癣,/odm/lib惰帽。這里沒去細(xì)究 LP64 的定義,從字面理解就是 64 位的系統(tǒng)父虑,會(huì)有這個(gè)宏定義该酗。
所以,如果是 64 位系統(tǒng)士嚎,systemNativeLibraryDirectories 的值為 /system/lib64呜魄,/odm/lib64,/vendor/lib64莱衩,接著會(huì)調(diào)用 makePathElements(this.systemNativeLibraryDirectories) 去構(gòu)造 nativeLibraryPathElements,最終構(gòu)造了一系列的 NativeLibaryElemt爵嗅,其實(shí) NativeLibraryElemt 就是包括了兩個(gè)字段 File zip, String zipDir,大概如下:
public NativeLibraryElement(File dir) {
this.path = dir;
this.zipDir = null;
}
- NativeLibraryElemt(File(data/app/包名/lib));
- NativeLibraryElemt(File(/system/lib64));
- NativeLibraryElemt(File(/odm/lib64));
- NativeLibraryElemt(File(/vendor/lib64));
最終,調(diào)用 NativeLibraryElemt 的 findLibrary()方法笨蚁,如下:
public String findNativeLibrary(String name) {
maybeInit();
if (zipDir == null) {//這里為 null
String entryPath = new File(path, name).getPath();
if (IoUtils.canOpenReadOnly(entryPath)) {
return entryPath;
}
} else if (urlHandler != null) {
// Having a urlHandler means the element has a zip file.
// In this case Android supports loading the library iff
// it is stored in the zip uncompressed.
String entryName = zipDir + '/' + name;
if (urlHandler.isEntryStored(entryName)) {
return path.getPath() + zipSeparator + entryName;
}
}
return null;
}
由于 zipDir 為空睹晒,所以這里最后返回的就是 File(path, name).getPath(),也就是默認(rèn)的文件夾名+完整的so庫名括细,比如 /data/data/包名/libnative-lib.so 伪很。
nativeLoad() 方法
從上一步拿到的完整路徑之后。
nativeLoad() 方法位于 Runtime.c 文件中奋单,如下:
JNIEXPORT jstring JNICALL
Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename,
jobject javaLoader)
{
return JVM_NativeLoad(env, javaFilename, javaLoader);
}
最終锉试,會(huì)調(diào)用到 java_vm_ext.cc 中的,LoadNativeLibrary() 方法中
bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
const std::string& path,
jobject class_loader,
std::string* error_msg) {
error_msg->clear();
// See if we've already loaded this library. If we have, and the class loader
// matches, return successfully without doing anything.
// TODO: for better results we should canonicalize the pathname (or even compare
// inodes). This implementation is fine if everybody is using System.loadLibrary.
SharedLibrary* library;
Thread* self = Thread::Current();
{
// TODO: move the locking (and more of this logic) into Libraries.
MutexLock mu(self, *Locks::jni_libraries_lock_);
library = libraries_->Get(path);
}
void* class_loader_allocator = nullptr;
{
ScopedObjectAccess soa(env);
// As the incoming class loader is reachable/alive during the call of this function,
// it's okay to decode it without worrying about unexpectedly marking it alive.
ObjPtr<mirror::ClassLoader> loader = soa.Decode<mirror::ClassLoader>(class_loader);
ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
if (class_linker->IsBootClassLoader(soa, loader.Ptr())) {
loader = nullptr;
class_loader = nullptr;
}
class_loader_allocator = class_linker->GetAllocatorForClassLoader(loader.Ptr());
CHECK(class_loader_allocator != nullptr);
}
if (library != nullptr) {
// Use the allocator pointers for class loader equality to avoid unnecessary weak root decode.
if (library->GetClassLoaderAllocator() != class_loader_allocator) {
// The library will be associated with class_loader. The JNI
// spec says we can't load the same library into more than one
// class loader.
//
// This isn't very common. So spend some time to get a readable message.
auto call_to_string = [&](jobject obj) -> std::string {
if (obj == nullptr) {
return "null";
}
// Handle jweaks. Ignore double local-ref.
ScopedLocalRef<jobject> local_ref(env, env->NewLocalRef(obj));
if (local_ref != nullptr) {
ScopedLocalRef<jclass> local_class(env, env->GetObjectClass(local_ref.get()));
jmethodID to_string = env->GetMethodID(local_class.get(),
"toString",
"()Ljava/lang/String;");
DCHECK(to_string != nullptr);
ScopedLocalRef<jobject> local_string(env,
env->CallObjectMethod(local_ref.get(), to_string));
if (local_string != nullptr) {
ScopedUtfChars utf(env, reinterpret_cast<jstring>(local_string.get()));
if (utf.c_str() != nullptr) {
return utf.c_str();
}
}
env->ExceptionClear();
return "(Error calling toString)";
}
return "null";
};
std::string old_class_loader = call_to_string(library->GetClassLoader());
std::string new_class_loader = call_to_string(class_loader);
StringAppendF(error_msg, "Shared library \"%s\" already opened by "
"ClassLoader %p(%s); can't open in ClassLoader %p(%s)",
path.c_str(),
library->GetClassLoader(),
old_class_loader.c_str(),
class_loader,
new_class_loader.c_str());
LOG(WARNING) << *error_msg;
return false;
}
VLOG(jni) << "[Shared library \"" << path << "\" already loaded in "
<< " ClassLoader " << class_loader << "]";
if (!library->CheckOnLoadResult()) {
StringAppendF(error_msg, "JNI_OnLoad failed on a previous attempt "
"to load \"%s\"", path.c_str());
return false;
}
return true;
}
// Open the shared library. Because we're using a full path, the system
// doesn't have to search through LD_LIBRARY_PATH. (It may do so to
// resolve this library's dependencies though.)
// Failures here are expected when java.library.path has several entries
// and we have to hunt for the lib.
// Below we dlopen but there is no paired dlclose, this would be necessary if we supported
// class unloading. Libraries will only be unloaded when the reference count (incremented by
// dlopen) becomes zero from dlclose.
// Retrieve the library path from the classloader, if necessary.
ScopedLocalRef<jstring> library_path(env, GetLibrarySearchPath(env, class_loader));
Locks::mutator_lock_->AssertNotHeld(self);
const char* path_str = path.empty() ? nullptr : path.c_str();
bool needs_native_bridge = false;
void* handle = android::OpenNativeLibrary(env,
runtime_->GetTargetSdkVersion(),
path_str,
class_loader,
library_path.get(),
&needs_native_bridge,
error_msg);
VLOG(jni) << "[Call to dlopen(\"" << path << "\", RTLD_NOW) returned " << handle << "]";
if (handle == nullptr) {
VLOG(jni) << "dlopen(\"" << path << "\", RTLD_NOW) failed: " << *error_msg;
return false;
}
if (env->ExceptionCheck() == JNI_TRUE) {
LOG(ERROR) << "Unexpected exception:";
env->ExceptionDescribe();
env->ExceptionClear();
}
// Create a new entry.
// TODO: move the locking (and more of this logic) into Libraries.
bool created_library = false;
{
// Create SharedLibrary ahead of taking the libraries lock to maintain lock ordering.
std::unique_ptr<SharedLibrary> new_library(
new SharedLibrary(env,
self,
path,
handle,
needs_native_bridge,
class_loader,
class_loader_allocator));
MutexLock mu(self, *Locks::jni_libraries_lock_);
library = libraries_->Get(path);
if (library == nullptr) { // We won race to get libraries_lock.
library = new_library.release();
libraries_->Put(path, library);
created_library = true;
}
}
if (!created_library) {
LOG(INFO) << "WOW: we lost a race to add shared library: "
<< "\"" << path << "\" ClassLoader=" << class_loader;
return library->CheckOnLoadResult();
}
VLOG(jni) << "[Added shared library \"" << path << "\" for ClassLoader " << class_loader << "]";
bool was_successful = false;
void* sym = library->FindSymbol("JNI_OnLoad", nullptr);
if (sym == nullptr) {
VLOG(jni) << "[No JNI_OnLoad found in \"" << path << "\"]";
was_successful = true;
} else {
// Call JNI_OnLoad. We have to override the current class
// loader, which will always be "null" since the stuff at the
// top of the stack is around Runtime.loadLibrary(). (See
// the comments in the JNI FindClass function.)
ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
self->SetClassLoaderOverride(class_loader);
VLOG(jni) << "[Calling JNI_OnLoad in \"" << path << "\"]";
typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
int version = (*jni_on_load)(this, nullptr);
if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) {
// Make sure that sigchain owns SIGSEGV.
EnsureFrontOfChain(SIGSEGV);
}
self->SetClassLoaderOverride(old_class_loader.get());
if (version == JNI_ERR) {
StringAppendF(error_msg, "JNI_ERR returned from JNI_OnLoad in \"%s\"", path.c_str());
} else if (JavaVMExt::IsBadJniVersion(version)) {
StringAppendF(error_msg, "Bad JNI version returned from JNI_OnLoad in \"%s\": %d",
path.c_str(), version);
// It's unwise to call dlclose() here, but we can mark it
// as bad and ensure that future load attempts will fail.
// We don't know how far JNI_OnLoad got, so there could
// be some partially-initialized stuff accessible through
// newly-registered native method calls. We could try to
// unregister them, but that doesn't seem worthwhile.
} else {
was_successful = true;
}
VLOG(jni) << "[Returned " << (was_successful ? "successfully" : "failure")
<< " from JNI_OnLoad in \"" << path << "\"]";
}
library->SetResult(was_successful);
return was_successful;
}
上述流程比較復(fù)雜,可以簡單概述為以下幾個(gè)步驟:
- 先判斷是否已經(jīng)加載過 so 庫辱匿,并且判斷加載 so 的 ClassLoader 是不是同一個(gè) ClassLoader键痛。
- 調(diào)用 android::OpenNativeLibrary() 去打開 so
上面的邏輯關(guān)注點(diǎn)在 OpenNativeLibrary() 這里炫彩,會(huì)將完整的路徑,ClassLoader 的引用傳遞進(jìn)去絮短,打開 so江兢。
void* handle = android::OpenNativeLibrary(env,
runtime_->GetTargetSdkVersion(),
path_str,
class_loader,
library_path.get(),
&needs_native_bridge,
error_msg);
最終,經(jīng)過層層調(diào)用會(huì)進(jìn)去到 linker.cpp 中丁频,調(diào)用 dl_open() 方法杉允,將so 加載到內(nèi)存中,
mapLibrary()
這個(gè)函數(shù)主要作用是使用傳入的名稱席里,拼接完整的 so 庫的名稱(不包含路徑)叔磷,具體的實(shí)現(xiàn)源碼在 System.c 文件中,如下:
JNIEXPORT jstring JNICALL
System_mapLibraryName(JNIEnv *env, jclass ign, jstring libname)
{
int len;
int prefix_len = (int) strlen(JNI_LIB_PREFIX);//JNI_LIB_PREFIX 就是 lib
int suffix_len = (int) strlen(JNI_LIB_SUFFIX);//JNI_LIB_SUFFIX 就是 .so
jchar chars[256];
if (libname == NULL) {
JNU_ThrowNullPointerException(env, 0);
return NULL;
}
len = (*env)->GetStringLength(env, libname);
if (len > 240) {//so 名稱不能大于 240
JNU_ThrowIllegalArgumentException(env, "name too long");
return NULL;
}
cpchars(chars, JNI_LIB_PREFIX, prefix_len);
(*env)->GetStringRegion(env, libname, 0, len, chars + prefix_len);
len += prefix_len;
cpchars(chars + len, JNI_LIB_SUFFIX, suffix_len);
len += suffix_len;
return (*env)->NewString(env, chars, len);
}
其中奖磁,涉及到的宏定義如下,在源碼 jvm_md.h 中:
#define JNI_LIB_PREFIX "lib"
#ifdef __APPLE__
#define JNI_LIB_SUFFIX ".dylib"
#define VERSIONED_JNI_LIB_NAME(NAME, VERSION) JNI_LIB_PREFIX NAME "." VERSION JNI_LIB_SUFFIX
#else
#define JNI_LIB_SUFFIX ".so"
#define VERSIONED_JNI_LIB_NAME(NAME, VERSION) JNI_LIB_PREFIX NAME JNI_LIB_SUFFIX "." VERSION
#endif
#define JNI_LIB_NAME(NAME) JNI_LIB_PREFIX NAME JNI_LIB_SUFFIX
這里判斷 so 名稱不能多于 240 個(gè)字符改基。
上述方法將傳入的 so 名稱進(jìn)行拼接,例如傳入 audio_shared 名稱咖为,最后會(huì)生成 libaudio_shared.so 這個(gè)完整名稱秕狰。