文章目录
一、概述
在我们实际开发中,经常会调用so库的一些功能,那么他们是如何工作的呢? 本文我们就来分析一下 so库的加载原理。
Android SDK: Pie 9.0.0_r3
so的加载分为两个Java层、Native层两部分,下面我们以加载一个 native-lib.so
为例来具体分析一下它的加载流程。
二、Java 层
Java 加载 so 库的方式有两种:
- System.loadLibrary(“lib_name”)
- System.load(“full path”)
示例: 以加载一个 native-lib.so
为例。
//加载的是libnative-lib.so,注意的是这边只需要传入"native-lib"
System.loadLibrary("native-lib");
//传入的是so文件完整的绝对路径
System.load("/data/data/应用包名/lib/libnative-lib.so")
下面先来看下 System.load()
部分的调用流程。
2.1 System.load()
System.class
public static void load(String filename) {
Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}
Runtime.class
synchronized void load0(Class<?> fromClass, String filename) {
// 传入的文件名如果不是路径的话则抛出异常。
if (!(new File(filename).isAbsolute())) {
throw new UnsatisfiedLinkError("Error msg");
}
if (filename == null) {
throw new NullPointerException("filename == null");
}
// 调用native方法nativeLoad()进入JNI层。
String error = nativeLoad(filename, fromClass.getClassLoader());
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
}
private static String nativeLoad(String filename, ClassLoader loader) {
return nativeLoad(filename, loader, null);
}
// native代码
private static native String nativeLoad(String filename, ClassLoader loader, Class<?> caller);
System.load() 方法最终调用 Runtime.nativeLoad() 方法进入 JNI 层。
下面先来看下 System.loadLibrary()
部分的调用流程。
2.2 System.loadLibrary()
System.class
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}
public class Reflection {
@CallerSensitive
public static native Class<?> getCallerClass();
}
Runtime.class
void loadLibrary0(Class<?> fromClass, String libname) {
ClassLoader classLoader = ClassLoader.getClassLoader(fromClass);
loadLibrary0(classLoader, fromClass, libname);
}
Runtime.loadLibrary0()
private synchronized void loadLibrary0(ClassLoader loader, Class<?> callerClass, String libname) {
// 传入的文件名libName如果是路径(包含"/")的话,就抛出异常。
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError("Msg");
}
String libraryName = libname;
if (loader != null && !(loader instanceof BootClassLoader)) {
// classLoader存在时,调用loader.findLibrary()方法查询so库完整路径。
// loader实际为BaseDexClassLoader
String filename = loader.findLibrary(libraryName);
if (filename == null &&
(loader.getClass() == PathClassLoader.class ||
loader.getClass() == DelegateLastClassLoader.class)) {
// System.mapLibraryName()方法会在输入的libName前后加字符串。
// 例如:传入native-lib,返回libnative-lib.so。
filename = System.mapLibraryName(libraryName);
}
if (filename == null) {
throw new UnsatisfiedLinkError("Msg");
}
String error = nativeLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
// 执行到这里,说明ClassLoader为null。接下来通过getLibPaths方式获取到so路径。
getLibPaths();
String filename = System.mapLibraryName(libraryName);
String error = nativeLoad(filename, loader, callerClass);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
}
private static String nativeLoad(String filename, ClassLoader loader) {
return nativeLoad(filename, loader, null);
}
// native代码
private static native String nativeLoad(String filename, ClassLoader loader, Class<?> caller);
小结:
loadLibrary0()
方法主要做了3件事:
- classLoader存在时,通过classLoader.findLibrary(libraryName) 来获取存放指定so文件的路径。
- classLoader不存在时,则通过 getLibPaths() 接口来获取。
- 最后调用 nativeLoad 加载指定路径的so文件。
2.3 System.mapLibraryName()
下面分析一下 System.mapLibraryName()
的作用。
System.class
public static native String mapLibraryName(String libname);
对应的JNI层代码在 System.cc
中。
System.cc
// jvm_md.h
JNI_LIB_PREFIX = "lib"
JNI_LIB_SUFFIX = ".so"
// System.cc
JNIEXPORT jstring JNICALL
System_mapLibraryName(JNIEnv *env, jclass ign, jstring libname)
{
int len;
int prefix_len = (int) strlen(JNI_LIB_PREFIX);
int suffix_len = (int) strlen(JNI_LIB_SUFFIX);
jchar chars[256];
if (libname == NULL) {
JNU_ThrowNullPointerException(env, 0);
return NULL;
}
len = (*env)->GetStringLength(env, libname);
// libName名字太长也会报错。
if (len > 240) {
JNU_ThrowIllegalArgumentException(env, "name too long");
return NULL;
}
// 将JNI_LIB_PREFIX写入chars数组。
cpchars(chars, JNI_LIB_PREFIX, prefix_len);
// 将libname写入chars数组。
(*env)->GetStringRegion(env, libname, 0, len, chars + prefix_len);
len += prefix_len;
// 将JNI_LIB_SUFFIX写入chars数组。
cpchars(chars + len, JNI_LIB_SUFFIX, suffix_len);
len += suffix_len;
return (*env)->NewString(env, chars, len);
}
static void cpchars(jchar *dst, char *src, int n)
{
int i;
for (i = 0; i < n; i++) {
dst[i] = src[i];
}
}
小结:
System.mapLibraryName(libraryName)
方法会将libraryName名字前加上lib的前缀,在名字后面加上.so后缀。即输入 “native-lib”,输出 “libnative-lib.so”。
2.4 ClassLoader.findLibrary()
Android 中 ClassLoader 可以看文章:Android 中的 ClassLoader 体系。
通过代码,我们可以看到 BaseDexClassLoader 里面实现了 findLibrary() 方法。
BaseDexClassLoader.class
DexPathList pathList;
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
DexPathList.class
// List of native library path elements.
NativeLibraryElement[] nativeLibraryPathElements;
public String findLibrary(String libraryName) {
// 这里又调用System.mapLibraryName()方法。
String fileName = System.mapLibraryName(libraryName);
for (NativeLibraryElement element : nativeLibraryPathElements) {
String path = element.findNativeLibrary(fileName);
if (path != null) {
return path;
}
}
return null;
}
NativeLibraryElement.class
private final File path;
private final String zipDir;
private ClassPathURLStreamHandler urlHandler;
private boolean initialized;
public NativeLibraryElement(File dir) {
this.path = dir;
this.zipDir = null;
}
public String findNativeLibrary(String name) {
maybeInit();
// 非zip文件
if (zipDir == null) {
// 路径与文件名拼接
String entryPath = new File(path, name).getPath();
if (IoUtils.canOpenReadOnly(entryPath)) {
return entryPath;
}
} else if (urlHandler != null) {
// zip文件
String entryName = zipDir + '/' + name;
if (urlHandler.isEntryStored(entryName)) {
return path.getPath() + zipSeparator + entryName;
}
}
return null;
}
IoUtils.class
public static boolean canOpenReadOnly(String path) {
try {
// 通过只读模式打开文件,来判断文件是否存在。
FileDescriptor fd = Libcore.os.open(path, O_RDONLY, 0);
Libcore.os.close(fd);
return true;
} catch (ErrnoException errnoException) {
return false;
}
}
小结:
ClassLoader.findLibrary()
方法传入一个 libName 的文件名,返回 libName 文件的全路径。- 传入的是libName文件名,对应的文件路径是在 NativeLibraryElement 中保存的。相当于是拿着 libName 去 List 这些文件夹下进行匹配。
- 通过
IoUtils.canOpenReadOnly(fullPathName)
尝试能否打开文件来判断文件是否存在。
2.5 小结
- loadLibrary 传入编译脚本生成的 so文件名即可,而 load 需要传入完整的so文件路径。
- load 相比 loadLibrary 少了路径查找的过程。
- load 并不是随便路径都可以,只支持两类路径。
- 应用本地存储路径:
/data/data/${package-name}/
。 - 系统lib路径:
system/lib
。
- 应用本地存储路径:
- load 不支持直接加载 sdcard 路径的 so库。如果要加载,可以先将将sdcard下的so文件复制到应用本地存储路径下再进行加载。
- loadLibrary 加载的都是一开始就已经打包进Apk或系统的so文件;而 load 可以是一开始就打包进来的so文件,也可以加载从网络下载的外部导入的so文件。最终都是调用 nativeLoad 加载指定路径的so文件。
- so 的瘦包方案就是通过 System.load() 来实现so文件的动态化加载。
三、Native 层
接下来我们分析一下 so 加载的 JNI 部分。
在 NDK(五):JNI静态注册与动态注册 一文的动态化注册部分,我们介绍了 System.nativeLoad()
方法最终会调用到 JNI 层 Runtime.c
文件中的 Runtime_nativeLoad()
方法。
/libcore/ojluni/src/main/native/Runtime.c
JNIEXPORT jstring JNICALL
Runtime_nativeLoad(JNIEnv* env, jclass ignored,
jstring javaFilename, jobject javaLoader)
{
return JVM_NativeLoad(env, javaFilename, javaLoader);
}
JVM_NativeLoad 方法在
OpenjdkJvm.cc
中。
/art/openjdkjvm/OpenjdkJvm.cc
JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env,
jstring javaFilename,
jobject javaLoader) {
ScopedUtfChars filename(env, javaFilename);
if (filename.c_str() == NULL) {
return NULL;
}
std::string error_msg;
{
// 获取JavaVM实例
art::JavaVMExt* vm = art::Runtime::Current()->GetJavaVM();
// 实际执行so加载的操作调用
bool success = vm->LoadNativeLibrary(env,
filename.c_str(),
javaLoader,
&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());
}
OpenjdkJvm.cc 内部是通过 art 虚拟机实例调用
LoadNativeLibrary()
来实际加载so的。
/art/runtime/java_vm_ext.cc
bool JavaVMExt::LoadNativeLibrary(JNIEnv* env, const std::string& path,
jobject class_loader, std::string* error_msg) {
error_msg->clear();
SharedLibrary* library;
Thread* self = Thread::Current();
// 1.先尝试从缓存查找,如果找到缓存说明上次初始化过。
{
// TODO: move the locking (and more of this logic) into Libraries.
MutexLock mu(self, *Locks::jni_libraries_lock_);
// 从缓存中查找,避免多次重复初始化。
library = libraries_->Get(path);
}
// 2.查找加载so的ClassLoader。
void* class_loader_allocator = nullptr;
{
ScopedObjectAccess soa(env);
ObjPtr<mirror::ClassLoader> loader = soa.Decode<mirror::ClassLoader>(class_loader);
ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
// ClassLoader不能是BootClassLoader。
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);
}
// 3.如果library缓存存在,则还要比较前后两次加载so的ClassLoader是否相同。
if (library != nullptr) {
// 如果library被加载过,则继续校验ClassLoader是否相同。
if (library->GetClassLoaderAllocator() != class_loader_allocator) {
// ...略...
LOG(WARNING) << "当前的so已经被其它ClassLoader加载过,不能被其它ClassLoader再加载。";
return false;
}
// 校验上次加载是否成功。
if (!library->CheckOnLoadResult()) {
return false;
}
return true;
}
// 4.缓存不存在,则开始执行加载so的流程。
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;
// 5.实际调用android::OpenNativeLibrary()加载so。
// 参数patch_str传递的是动态库的全路径,之所以还要传递搜索路径,是因为可能包含它的依赖库。
void* handle = android::OpenNativeLibrary(env,
runtime_->GetTargetSdkVersion(),
path_str,
class_loader,
library_path.get(),
&needs_native_bridge,
error_msg);
if (handle == nullptr) {
// so加载失败
return false;
}
if (env->ExceptionCheck() == JNI_TRUE) {
LOG(ERROR) << "Unexpected exception:";
env->ExceptionDescribe();
env->ExceptionClear();
}
bool created_library = false;
{
// 6.创建一个SharedLibrary对象,并关联so的句柄handle,然后会将创建的对象添加到缓存池libraries_中。
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_中。
libraries_->Put(path, library);
// 标记设置为true。
created_library = true;
}
}
if (!created_library) {
return library->CheckOnLoadResult();
}
bool was_successful = false;
// 7.so加载成功后,去查看该so是否有"JNI_OnLoad"方法,有的话则进行调用。(so动态初始化的入口)
void* sym = library->FindSymbol("JNI_OnLoad", nullptr);
// so中没有JNI_OnLoad符号。
if (sym == nullptr) {
was_successful = true;
} else {
// so中有JNI_OnLoad符号,则调用JNI_OnLoad方法。
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*);
// 8.调用so库里的JNI_OnLoad方法。
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) {
// error msg
} else {
was_successful = true;
}
}
// 9.给library设置是否加载成功的状态,用于前面流程的校验。
library->SetResult(was_successful);
return was_successful;
}
小结:
- 1、先尝试从 libraries_ 缓存池中查找,如果找到缓存说明上次初始化过。
- 2、查找本次加载so的ClassLoader。
- 3、缓存存在:so被加载过,则进一步判断前后两次加载so的ClassLoader是否相同。
- ClassLoader 不同,则返回加载失败。
- ClassLoader 项同,则进一步判断上次 so 的加载状态(是否加载成功),并返回对应加载状态。
- 4、缓存不存在:
- 调用android::OpenNativeLibrary()加载so,返回一个操作so的句柄handle。
- 创建一个SharedLibrary对象,并关联so的句柄handle,然后将创建的SharedLibrary对象添加到缓存池libraries_中,用于前面的查询使用,避免同一个so被多次加载。
- so加载成功后,去查看该so是否有"JNI_OnLoad"符号,有的话则进行调用。(so动态初始化的入口)
- so中没有JNI_OnLoad符号,则直接进入下一步。
- so中有JNI_OnLoad符号,则调用JNI_OnLoad方法。
- 给library设置是否加载成功的状态,用于前面流程的校验。
- 返回当前so加载的状态:成功 or 失败。
四、小结
Java层:
- Java 支持两种 so 的加载方式:
System.loadLibrary()
、System.load()
- 两种方式最终都会调用
Runtime.nativeLoad()
这个 native 方法。 - load 并不是随便路径都可以,只支持两类路径。
- 应用本地存储路径:
/data/data/${package-name}/
。 - 系统lib路径:
system/lib
。
- 应用本地存储路径:
- 两者的差异:
- 参数差异:loadLibrary 传入 so文件名即可,而 load 需传入完整的so文件路径。
- 动态化差异:loadLibrary 只支持加载 Apk安装时就存在的so,以及系统的so库。 load 支持加载网络下载的 so库(需要将下载的so库复制到
/data/data/${package-name}/
目录)。
JNI 层
- 先进行缓存查找,如果找到缓存,还需要进一步比较 ClassLoader 是否相同。
- 缓存没有,则进行 so 初始化。
- 先加载 so,并返回一个handle 句柄。
- 构建一个 SharedLibrary 对象,然后与 so 的句柄 handle 进行关联。
- 将上面创建的 SharedLibrary 对象存放到缓存池 libraries_ 中。
- 查找 so 中是否有 "JNI_OnLoad"符号,如果有,则调用该方法。
- 设置 so 的加载状态给 SharedLibrary 对象。
- 返回 so 加载状态到上一步。
来源:https://blog.youkuaiyun.com/Love667767/article/details/129741512