[toc]
前言
最近在做 Android 项目的时候,需要在 NativeActivity 中动态加载 so 。运行的时候,抛出了异常
Caused by: java.lang.IllegalArgumentException: Unable to find native library using classloader: dalvik.system.PathClassLoader
在 NativeActivity 中可以看到
1 |
|
在上面的源码中,可以知道,NativeActivity 要是通过 BaseDexClassLoader#findLibrary 方法去查找 so 的路径,然后进行判空。上面提到的异常,就是因为找不到这个 so 的 path, 抛处理的异常。
在这个异常之后有几个疑问
- 第一个问题是: so 的加载过程是怎样的,so 是如何和现在运行的代码联系起来的?
- 第二个问题是: classLoader 通过 findLibrary 方法去查找 so 的 path, 这些path 有什么,是怎样来的?
- 第三个问题是:classLoader 是怎么来的,在哪里生成的?
带着这三个问题去找答案,下面就是对三个问题答案的寻找过程。
正文
本文通过讲述 Android 动态链接库 so 的加载过来,已经 so 的加载原理,可以对加载的整个流程有个清晰的认识,有助于对后续学习热修复有比较好的帮助。
下面代码分析的源码都是以 Android 9.0 版。
1 Android So 的加载过程
在 Android 添加 so 有两种方式,一种是调用 load(String filename)
方法,传递进去的是路径;另一种是调用 loadLibrary(String libname)
方式,传递进去的是 so 的名称
System.load(“/storage/emulated/0/libnative-lib.so”) 全路径
System.loadLibrary(“native-lib”); so 的名字
1.1 System#loadLibrary
[> java/lang/System.java]
1 | public static void loadLibrary(String libname) { |
1.2 Runtime#loadLibrary0
[>java/lang/Runtime.java]
1 | private synchronized void loadLibrary0(ClassLoader loader, Class<?> callerClass, String libname) { |
- 当 loader 不为空是,通过 ClassLoader#findLibrary() 查看 so 是否存在
- 当 loader 为空是,则从默认目录 mLibPaths 中查找
1.3 Runtime#getLibPaths
[>java/lang/Runtime.java]
1 | // 获取 Lib 默认路径 |
initLibPaths
路径是默认的 lib 路径 返回的路径是
/system/lib/
/vendor/lib/
/product/lib/
1.4 BaseDexClassLoader.findLibrary
通过 ClassLoader 查找 so
[> libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java]
1 |
|
pathList 是 DexPathList
1.5 DexPathList#findLibrary
[>libcore/dalvik/src/main/java/dalvik/system/DexPathList.java]
1 | public String findLibrary(String libraryName) { |
例如System.mapLibraryName(native-lib)
返回的是 libnative-lib.so
nativeLibraryPathElements
是 native library 路径的集合, 它的是 DexPathList
初始化的时候赋值,详见 1.7节
1.6 DexPathList$NativeLibraryElement#findNativeLibrary
[>libcore/dalvik/src/main/java/dalvik/system/DexPathList.java]
1 | public String findNativeLibrary(String name) { |
1.7 DexPathList#DexPathList
[>libcore/dalvik/src/main/java/dalvik/system/DexPathList.java]
1 | DexPathList(ClassLoader definingContext, String dexPath, |
在 DexPathList
的构造函数中,我们可以知道 nativeLibraryPathElements
是所有 Native Library 的集合。
DexPathList
是在 ActivityThread 中创建,ActivityThread 是在 App 启动时候创建的。关于 App 启动的启动流程,可以去找这方面的资料,自行查看。
总结一些 Native Library 的路径来源:
一个是 Native 库的原始路径
System.getProperty("java.library.path****")
,
/system/lib/; /vendor/lib/; /product/lib/另外一个是App启动时的 Lib 库路径
1.8 Runtime#doLoad
在上面我们解决 Native Library 的路径问题,下面分析一下加载的过程
1 | private String doLoad(String name, ClassLoader loader) { |
1.9 Runtime.c#Runtime_nativeLoad
[> libcore/ojluni/src/main/native/Runtime.c]
1 | JNIEXPORT jstring JNICALL |
Runtime.c 中 Runtime_nativeLoad
方法会调用 JVM_NativeLoad
1.10 OpenjdkJvm.cc#JVM_NativeLoad
[>art/openjdkjvm/OpenjdkJvm.cc]
1 | JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env, |
1.11 java_vm_ext.cc#JVM_NativeLoad
[>art/runtime/java_vm_ext.cc]
1 | bool JavaVMExt::LoadNativeLibrary(JNIEnv* env, |
上面的内容比较多,需要一步步分析
- 第一步是判断 so 是否已经被加载过,如果已经加载过了,则直接返回加载成功
- 第二步是打开 so, 返回
handle
句柄,如果返回的句柄为空,这份表示加载失败 - 第三步是创建一个
SharedLibrary
结构体,放到libraries
中缓存 - 第四步是查找
JNI_OnLoad
符号,这里分两种情况- 如果在 JNI 中没有写
JNI_OnLoad
方法,找不到符号,返回加成功 - 另一种情况是,如果 JNI 中有
JNI_OnLoad
方法,则会重写当前的 ClassLoader, 并且判断 JNI 版本
- 如果在 JNI 中没有写
从上面的第四步,我们可以知道加载 so 中 JNI 的入口是 JNI_OnLoad
方法,所以在写 JNI 的时候,会在 JNI_OnLoad
方法中做一些初始化的工作。另外一个就是,如果写了 JNI_OnLoad
方法,就要指定 JNI 版本。
判断 JNI 的版本
[>art/runtime/java_vm_ext.cc]
1 | bool JavaVMExt::IsBadJniVersion(int version) { |
上面是判断 JNI 版本,可以看到只能是 JNI_VERSION_1_2,JNI_VERSION_1_4,JNI_VERSION_1_6
三个版本
下来是我们平时在写 JNI 的时候, JNI_OnLoad
方法中需要给定 jni 的版本, 同时做一些初始化的工作。
1 | int JNI_OnLoad(JavaVM *vm, void *reserved) { |
2. So 的加载原理
在前面的部分,我们梳理了 so 加载的整个流程,但这个过程还有一些以为,包括:
- ClassLoader 从哪里来
- Native 库是怎样来的
- so 是怎样到 Native 库里面的
下面将一个个来查找这些疑问的答案
2.1 ClassLoader 是怎样来的
2.1.1 System#loadLibrary
[> java/lang/System.java]
1 | public static void loadLibrary(String libname) { |
加载的 ClassLoader 从 VMStack 中获取, VMStack 再去从 Native 中获取
[>/libcore/libart/src/main/java/dalvik/system/VMStack.java]
1 | @FastNative |
2.1.2 ActivityThread#handleBindApplication
[>/frameworks/base/core/java/android/app/ActivityThread.java]
在 ActivityThread 是 Android App 启动的入口,关于 App 的启动可参考其他资料。
App 启动过程,会调到 ActivityThread#handleBindApplication 方法。
在这个方法中,会创建 LoadedApk
并且传入进去 Context 中的 ClassLoader.
Context 的实现是 ContextImpl,Context#getClassLoader() 方法,去看 ContextImpl#getClassLoader(), 详见 2.1.3
1 | private void handleBindApplication(AppBindData data) { |
2.1.3 ContextImpl#getClassLoader
[>/frameworks/base/core/java/android/app/ContextImpl.java]
1 |
|
这里的逻辑有点绕,将代码整理改成下面,会更加容易看懂
1 |
|
经过整理的代码逻辑就很清晰了,第一次进来的时候 mClassLoader 是空的,只要看后面的逻辑。
mPackageInfo是 LoadApk, mPackageInfo 不会为空,在 2.1.2 节知道它是 ContextImpl 创建的时候传进来的mP。所以,ackageInfo.getClassLoader() 是调用了 LoadApk#getClassLoader() 方法,关于这个方法详见 2.1.4
2.1.4 LoadApk#getClassLoader
[>/frameworks/base/core/java/android/app/LoadApk.java]
1 | public ClassLoader getClassLoader() { |
2.1.5 ApplicationLoaders#getClassLoader
[>/frameworks/base/core/java/android/app/ApplicationLoaders.java]
1 | ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled, |
2.1.6 ClassLoaderFactory#createClassLoader
[>/frameworks/base/core/java/com/android/internal/os/ClassLoaderFactory.java]
1 | public static ClassLoader createClassLoader(String dexPath, |
经过上面这么多步,终于看到了创建创建 ClassLoader 的地方.
根据参数的 classloaderName 的不同,会创建 PathClassLoader 或者 DelegateLastClassLoader。
classloaderName 参数是 app 启动的时候传下来的,见 2.1.4 节
总的来说,ClassLoader 是 app 启动的时候, ActivityThread 中经过一步步的调用,最后在 ApplicationLoaders 中用 ClassLoaderFactory 创建。
ClassLoader 的分类
关于不同的 ClassLoader 有不同的作用,可以去查相关的资料
到此,我们的第一个问题解决了,ClassLoader 是 app 启动的时候在 ActivityThread 中创建。
2.2 Native 库是怎样来的
通过对 DexPathList 的分析,可以知道 Native Library 来自来自两个地方
一个是 DexPathList 创建的时候,构造函数传进来的 librarySearchPath。
另外一个是 addNativePath(Collection
libPaths)
例如 :/data/app/com.test.baidu/base.apk!/lib/armeabi-v7a
2.2.1 DexPathList#findLibrary
在第一章的时候,加载 so 会调用到 DexPathList#findLibrary 方法,在这个方法里面会遍历 nativeLibraryPathElements。 nativeLibraryPathElements 是 NativeLibrary 路径的集合。
1 | public String findLibrary(String libraryName) { |
System.mapLibraryName 的实现是在 System.c 里面,返回 so 的文件名,例如
libraryName 是 test_baidu,
System.mapLibraryName(‘test_baidu’) 返回的是 libtest_baidu.so
下面要看看 nativeLibraryPathElements
是怎么来的
NativeLibraryElement 类是 Native Library 的路径元素
1 | static class NativeLibraryElement { |
2.2.1 DexPathList#DexPathList 构造函数
1 | DexPathList(ClassLoader definingContext, String dexPath, |
在 DexPathList 的构造函数中,我们可以看到 Native library 存在两个方面
- 一个是传入进来的 librarySearchPath
- 另外一个是通过虚拟机属性
java.library.path
获取的系统 Native 库
2.2.2 DexPathList#addNativePath
外部添加的 libPaths 路径
例如
/data/app/com.test.baidu/base.apk!/lib/armeabi-v7a
DexPathList#addNativePath 是在 ApplicationLoaders#addNative 中调用,见 2.2.3
1 | public void addNativePath(Collection<String> libPaths) { |
2.2.3 ApplicationLoaders#addNative
[>/frameworks/base/core/java/android/app/ApplicationLoaders.java]
1 | void addNative(ClassLoader classLoader, Collection<String> libPaths) { |
2.2.4 LoadedApk#createOrUpdateClassLoaderLocked
[> /frameworks/base/core/java/android/app/LoadedApk.java]
1 | private void createOrUpdateClassLoaderLocked(List<String> addedPaths) { |
createOrUpdateClassLoaderLocked 方法里面创建 ClassLoader 并且设置 lib 路径
- 首先 defaultSearchPaths 默认路径,在 ①⑤中获取并放置进去,包含
/system/lib/
/vendor/lib/
/product/lib/
- 其次,在 ② 中 makePaths
- 在 ④⑥中将 libs 路径添加到 DexPathList 中
2.2.5 LoadedApk#makePaths
1 | public static void makePaths(ActivityThread activityThread, |
在 ① 中,会根据 cpu 架构的不同,而添加不同路径,例如,如果手机 cpu 的架构 是 armeabi-v7a, 那 apk + "!/lib/" + aInfo.primaryCpuAbi
就是
data/app/包名==/base.apk!/lib/armeabli-v7a
2.2.6 ActivityThread#getInstrumentationLibrary
[>/frameworks/base/core/java/android/app/ActivityThread.java]
1 | private String getInstrumentationLibrary(ApplicationInfo appInfo, InstrumentationInfo insInfo) { |
在 ActivityThread 中的 nativeLibraryDir 通过 getInstrumentationLibrary 方法获取,也是通过 SystemProperties.get("ro.dalvik.vm.isa." + secondaryIsa);
系统属性获取
总的来说 NativeLibraryPath 主要是来至于几个方面
一个是系统的
java.library.path
属性,是/system/lib
/vendor/lib
/product/lib一个是
apk + "!/lib/" + aInfo.primaryCpuAbi
/data/app/包名==/base.apk!/lib/armeabli-v7a
- 一个是
"ro.dalvik.vm.isa." + secondaryIsa
属性/data/app/包名==/lib/arm
如下图中的 nativeLiraryPathsElements中的路径
解决问题
通过上面的分析,已经回答了我们在前言部分的三个疑问,那接下来就要解决这个异常了。
我们知道
Caused by: java.lang.IllegalArgumentException: Unable to find native library using classloader: dalvik.system.PathClassLoader
在上面的分析知道, 通过 ClassLoader#findLibrary去 libs 路径去查找我们要加载的 so, 找不到这个 path 导致。
动态加载 so,我们在通常需要把要加载的 so 从后台下载下来,然后通过 System.load(String filename) 或者 System.loadLibrary(String libname) 方法去加载 so。
那解决这个问题就是把我们下载存放 so 的路径,添加到 ClassLoader 的 libs 路径里面,而这些 libs 路径是 app 启动的时候就应经生成了。可以利用反射,在运行时路径添加进去。
将存放 so 的路径放到 ClassLoader 中
利用反射将存放 so 的路径放到 ClassLoader 中,刚好 tinker 的 TinkerLoadLibrary 也有实现发方法,我们就不用自己实现了,可以拿过来直接使用
LoadLibrary 核心代码
1 | private static final class V25 { |
关于反射的代码已放到 github
我的项目下载存放 so 的路径是 /data/user/0/包名/app_libs
运行之后,我们看开点 路径已经添加进了 ClassLoader 的 nativeLibraryDirecories 中
至此,异常解决了。
同时也对 so 的加载原理有了更好的了解。