插件化so库加载原理及实现
系统加载 so 库的工作流程
当我们调用当调用 System#loadLibrary("xxx" ) 后,Android Framework 都干了些了啥?
static {
System.loadLibrary("ymm_log");
}
在看下System类的实现:
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
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;
}
String filename = System.mapLibraryName(libraryName);
List<String> candidates = new ArrayList<String>();
String lastError = null;
for (String directory : getLibPaths()) {
String candidate = directory + filename;
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
String error = doLoad(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);
}
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);
}
}
获取so文件名的方式,就是从classLoader中获取,最终加载时通过本地方法nativeLoad实现
String filename = loader.findLibrary(libraryName)
其实现在BaseDexClassLoader
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
方案分析:
1. JNI 代码内置方案
代码隔离方案比较适合新增的 Native 模块,一开始就奔着动态化、延迟加载的方向去。
2. 插件化方案
单独把 so 文件单独打包进插件包,JNI 代码保留在宿主代码内部
由于 nativeLibraryDirectories 的具体实现是一个 ArrayList 实例,其元素读写操作自身是不保证线程安全的
急需解决的问题
1. 安全性问题
所有可执行代码在拷贝安装到安全路径(比如 Android 的 data/data 内部路径)之前,都有被劫持或者破坏的风险。
最好的做法是每次加载 so 库之前都对其做一次安全性校验。
最简单的方式是记录 so 文件的 MD5 或者 CRC 等 Hash 信息(粒度可以是每个单独的 so 文件,或者一批 so 文件的压缩包)
如果本地下载目录中的 so 文件总数目,少于预定义在集合里 so 文件数目,说明不完整
2. 版本控制问题
通过版本控制流程,我们可以在服务端禁用这个版本的 so 插件,从而使客户端进入“so 插件不可用”的逻辑,而不至于执行有问题的代码。
3. abi 兼容性判断
检查 so 插件包里的 so 库 abi 信息是否与宿主目前运行时的 abi 一致。
直接指定你 so 下载的路径,通过反射获取 android.os.SystemProperties 私有方法 get ro.product.cpu.abi 可以动态获取 CPU 架构
/**
* 获取设备的cpu架构类型
*/
public static String getCpuArchType() {
if (!TextUtils.isEmpty(cpuArchType)) {
return cpuArchType;
}
try {
Class<?> clazz = Class.forName("android.os.SystemProperties");
Method get = clazz.getDeclaredMethod("get", new Class[]{String.class});
cpuArchType = (String) get.invoke(clazz, new Object[]{"ro.product.cpu.abi"});
} catch (Exception e) {
}
try {
if (TextUtils.isEmpty(cpuArchType)) {
cpuArchType = Build.CPU_ABI;//获取不到,重新获取,可能不准确?
}
} catch (Exception e) {
}
if (TextUtils.isEmpty(cpuArchType)) {
cpuArchType = "armeabi-v7a";
}
cpuArchType = cpuArchType.toLowerCase();
return cpuArchType;
}
4. System#load 加载代码侵入问题
通过 System#loadLibrary("xxx" ) 加载 so 库, Android Framework 会遍历当前上下文的 ClassLoader 实例里的 nativeLibraryDirectories 数组,在数组里所有的文件路径下查找文件名为 libxxx.so 的文件,所以我们的解决思路就是在安装好 so 插件之后,将其所在的内部安全路径注入到这个 nativeLibraryDirectories 数组里,即可实现通过 System#loadLibrary 加载,代码如下:
第一步: 通过反射,注入 so 文件注入到 nativeLibraryDirectories 路径
private static final class V14 {
private static void install(ClassLoader classLoader, File folder) throws Throwable {
// 反射宿主 APK 的 ClassLoader 的 pathList成员变量
Field pathListField = MkReflectUtil.findField(classLoader, "pathList");
// 获取这个成员变量 在 宿主 APK 的 ClassLoader 对象的取值
Object dexPathList = pathListField.get(classLoader);
// 将被加载的 被加载的 so 实例存储到 dexPathList
MkReflectUtil.expandArray(dexPathList, "nativeLibraryDirectories", new File[]{folder});
}
}
private static final class V23 {
private static void install(ClassLoader classLoader, File folder) throws Throwable {
Field pathListField = MkReflectUtil.findField(classLoader, "pathList");
Object dexPathList = pathListField.get(classLoader);
Field nativeLibraryDirectories = MkReflectUtil.findField(dexPathList, "nativeLibraryDirectories");
List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
//去重
if (libDirs == null) {
libDirs = new ArrayList<>(2);
}
final Iterator<File> libDirIt = libDirs.iterator();
while (libDirIt.hasNext()) {
final File libDir = libDirIt.next();
if (folder.equals(libDir)) {
libDirIt.remove();
Log.d(TAG, "dq libDirIt.remove() " + folder.getAbsolutePath());
break;
}
}
libDirs.add(0, folder);
Field systemNativeLibraryDirectories =
MkReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
//判空
if (systemLibDirs == null) {
systemLibDirs = new ArrayList<>(2);
}
//Log.d(TAG, "dq systemLibDirs,size=" + systemLibDirs.size());
// 获得Element[] 数组
Method makePathElements = MkReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class, List.class);
ArrayList<IOException> suppressedExceptions = new ArrayList<>();
libDirs.addAll(systemLibDirs);
// 输出调用对象,插件APK所在目录,插件APK的全路径,和用于存储IO异常的List,获得Element[] 返回
Object[] elements = (Object[]) makePathElements.invoke(dexPathList, libDirs, null, suppressedExceptions);
Field nativeLibraryPathElements = MkReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
nativeLibraryPathElements.setAccessible(true);
nativeLibraryPathElements.set(dexPathList, elements);
}
}
private static final class V25 {
private static void install(ClassLoader classLoader, File folder) throws Throwable {
Field pathListField = MkReflectUtil.findField(classLoader, "pathList");
Object dexPathList = pathListField.get(classLoader);
Field nativeLibraryDirectories = MkReflectUtil.findField(dexPathList, "nativeLibraryDirectories");
List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
//去重
if (libDirs == null) {
libDirs = new ArrayList<>(2);
}
final Iterator<File> libDirIt = libDirs.iterator();
while (libDirIt.hasNext()) {
final File libDir = libDirIt.next();
if (folder.equals(libDir)) {
libDirIt.remove();
Log.d(TAG, "dq libDirIt.remove()" + folder.getAbsolutePath());
break;
}
}
libDirs.add(0, folder);
//system/lib
Field systemNativeLibraryDirectories = MkReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
//判空
if (systemLibDirs == null) {
systemLibDirs = new ArrayList<>(2);
}
//Log.d(TAG, "dq systemLibDirs,size=" + systemLibDirs.size());
Method makePathElements = MkReflectUtil.findMethod(dexPathList, "makePathElements", List.class);
libDirs.addAll(systemLibDirs);
Object[] elements = (Object[]) makePathElements.invoke(dexPathList, libDirs);
Field nativeLibraryPathElements = MkReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
nativeLibraryPathElements.setAccessible(true);
nativeLibraryPathElements.set(dexPathList, elements);
}
}
注入 so 路径的逻辑如下:
- APK 的 ClassLoader 的 pathList 的成员变量,
- pathList 实际上是 SoPathList, 类的实例 的内部 成员变量 List 实例
- 这个 List 存储的是 被加载的 so 文件实例
/**
* 1. 通过反射拿到dexElements的取值
* 2. 将 findField 方法获取到的 object[] 插入到数组的最前面。
* 3. 被插入的 object[] 数组就是外部修复包存储路径集合编译后形成的队列
* 即外部修复包的资源和 .class 队列
* @param instance 宿主 APK 的 ClassLoader实例的成员变量 pathList(DexPathList类似)
* @param fieldName 需要被反射和替换的 DexPathList 类对象的成员变量 "dexElements", 用于存储 .dex 加载对象dex
* @param extraElements 被加载的插件 apk 的 .dex实例列表
*/
public static void expandFieldArray(Object instance, String fieldName, Object[] extraElements)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
// 1 通过反射获取 classLoader 实例的成员变量 pathList(DexPathList类的实例)的成员变量dexElements
Field jlrField = findField(instance, fieldName);
// 2 获取当前dexElements 这个成员变量在classLoader 实例的成员变量 pathList(DexPathList类的实例)中的取值
Object[] original = (Object[]) jlrField.get(instance);
// 3 新建一个数组,这个数组用来容纳 宿主 apk .dex 文件加载出来的elements[] 和 插件apk .dex 文件加载出来的 elements[]
Object[] combined = (Object[]) Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length);
// 4 先把插件 apk 中获取的elements[] 以及 dexFileArr复制到数组里面,方便我们动态加载
System.arraycopy(extraElements, 0, combined, 0, extraElements.length);
// 5 再把apk所有的 dexElements 成员变量取值复制到数组里面
System.arraycopy(original, 0, combined, extraElements.length, original.length);
// 6 覆盖 dexElements 成员变量取值
jlrField.set(instance, combined);
}