Runtime.loadLibrary() 源码分析
最近的用户反馈,碰到一个 loadLibrary() 失败的问题,之前对这一个流程一直没有进行细致梳理,现在趁有空,梳理一下。
loadLibrary() 的流程
一般情况下,通过 System.loadLibrary() 去加载你需要的 so 库,如下:
System.loadLibrary("native-lib")
System 调用的代码如下:
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
实际上,调用的是 Runtime 的 loadLibrary0() 函数,Runtime 是每个 Java 应用的一个运行时,并且getRutime() 获得Runtime 对象是个单例:
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);//调用 jni 方法,nativeLoad() 方法
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
String filename = System.mapLibraryName(libraryName);//使用 mapLibraryName() 方法查找实际的 so 库的名称
List candidates = new ArrayList();
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 的,所以如果多个线程调用,则需要等待。 loadLibrary0() 方法主要分为以下几步调用流程:
- 检查 so 库名称,是否包含了文件分割符,如果包含则直接抛出异常。
- 如果 ClassLoader 不为空,调用 ClassLoader.findLibrary() 去查找so,如果找不到则抛出 XXClassLoader couldn't find XXX.so.
- 如果 ClassLoader 不为空,调用 nativeLoad() 方法,去加载 so。源码在libcore/ojluni/src/main/native/Runtime.c,找到则返回。
后面的分支是 ClassLoader 为空的情况
- 如果 ClassLoader 为空,调用 System.mapLibraryName() 去获取so 库的完整名称
- 如果 ClassLoader 为空,遍历 getLibPaths()去查找 so,并且获取路径。
- 调用 nativeLoad() 方法,使用完整 so 路径去加载 so 库
综上,我们只要分析 ClassLoader.findLibrary() 和 nativeLoad() 方法即可。
loader.findLibrary() 方法
以包名为com.rockets.livedemo 为例子,对应的 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 ,其并没有实现 findLibrary() 方法,而是由其子类 BaseDexClassLoader 实现的,源码如下:
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
这里交由 DexPathList 的 findLibrary() 方法去实现,如下:
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;
}
这里会先调用System.mapLibraryName() 去查找 so 库的完整名称,具体的可以看后面针对这个函数的分析,总之这里会从你传入的 native-lib 名称,变成 libnative-lib.so 这样完整的名称。
拿到完整的 so 库的名称之后,会通过遍历 nativeLibraryPathElements 去查找 so 库,那么 nativeLibraryPathElemts 是怎么初始化的?如下:
NativeLibraryElement[] nativeLibraryPathElements;
/** List of application native library directories. */
private final List nativeLibraryDirectories;
/** List of system native library directories. */
private final List systemNativeLibraryDirectories;
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
List 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;
}
具体细节不分析,总的来说,如果 apk 是系统应用,则 nativeLibraryDir 为 /system/lib/xxx 或者 system/app/xxx/lib。但是对于我们自己的应用来说,这个值为 data/app/包名/lib。
以华为手机,安装抖音极速版为例:
抖音极速版的lib 目录为 /data/data/com.ss.android.ugc.aweme.lite/lib,其中 com.ss.android.ugc.aweme.lite 为包名。
systemNativeLibraryDirectories 是通过静态方法 getProperty() 去获取,这里的代码比较复杂,直接跳过,最终我们获取到的路径是:
//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,如果没有这个宏定义则为 /system/lib,/odm/lib,/odm/lib。这里没去细究 LP64 的定义,从字面理解就是 64 位的系统,会有这个宏定义。
所以,如果是 64 位系统,systemNativeLibraryDirectories 的值为 /system/lib64,/odm/lib64,/vendor/lib64,接着会调用 makePathElements(this.systemNativeLibraryDirectories) 去构造 nativeLibraryPathElements,最终构造了一系列的 NativeLibaryElemt,其实 NativeLibraryElemt 就是包括了两个字段 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));
最终,调用 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(),也就是默认的文件夹名+完整的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);
}
最终,会调用到 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 loader = soa.Decode(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 local_ref(env, env->NewLocalRef(obj));
if (local_ref != nullptr) {
ScopedLocalRef 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 local_string(env,
env->CallObjectMethod(local_ref.get(), to_string));
if (local_string != nullptr) {
ScopedUtfChars utf(env, reinterpret_cast(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 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 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 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(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;
}
上述流程比较复杂,可以简单概述为以下几个步骤:
- 先判断是否已经加载过 so 库,并且判断加载 so 的 ClassLoader 是不是同一个 ClassLoader。
- 调用 android::OpenNativeLibrary() 去打开 so
上面的逻辑关注点在 OpenNativeLibrary() 这里,会将完整的路径,ClassLoader 的引用传递进去,打开 so。
void* handle = android::OpenNativeLibrary(env,
runtime_->GetTargetSdkVersion(),
path_str,
class_loader,
library_path.get(),
&needs_native_bridge,
error_msg);
最终,经过层层调用会进去到 linker.cpp 中,调用 dl_open() 方法,将so 加载到内存中,
mapLibrary()
这个函数主要作用是使用传入的名称,拼接完整的 so 库的名称(不包含路径),具体的实现源码在 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 个字符。
上述方法将传入的 so 名称进行拼接,例如传入 audio_shared 名称,最后会生成 libaudio_shared.so 这个完整名称。