在Android日常开发中,经常需要用到一些第三方so库,往往so库的内存都挺大的(相对于APK的体积来说),随着so库使用越多,在整个APK体积中的占比就越多,为了解决这一问题就诞生出了so库的动态加载。
文章中代码使用的Demo
1. So库的加载原理 首先我们需要了解,系统加载So库的工作流程,当我们调用System.loadLibrary(“XXX”)后,Android的Framework层都干了什么?Android的So加载机制,大致可分为四个步骤:
安装APK的时候,PMS根据当前设备abi信息,从APK包拷贝相应的So文件
启动APP的时候,Android Framework创建应用的classloader实例,并将当前应用的相关所有so库文件所在的目录注入到当前的ClassLoader中。
调用System.loadLibrary("XXX")framework从上下文ClassLoader实例目录数组中查找并加载名为libXXX.so的文件
调用so库相关的JNI文件。
步骤1和2不熟悉的朋友,可以自行阅读相关源码和资料。这里我们直接分析第三步。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class System { public static void loadLibrary (String libname) { Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname); } } public class Runtime { private synchronized void loadLibrary0 (ClassLoader loader, Class<?> callerClass, 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 && !(loader instanceof BootClassLoader)) { String filename = loader.findLibrary(libraryName); if (filename == null && (loader.getClass() == PathClassLoader.class || loader.getClass() == DelegateLastClassLoader.class)) { filename = System.mapLibraryName(libraryName); } if (filename == null ) { throw new UnsatisfiedLinkError(loader + " couldn't find \"" + System.mapLibraryName(libraryName) + "\"" ); } String error = nativeLoad(filename, loader); if (error != null ) { throw new UnsatisfiedLinkError(error); } return ; } } }
BaseDexClassLoader
1 2 3 4 5 6 7 8 9 10 11 public class BaseDexClassLoader { @UnsupportedAppUsage private final DexPathList pathList; @Override public String findLibrary (String name) { return pathList.findLibrary(name); } }
DexPathList
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class DexPathList { private final File[] nativeLibraryDirectories; 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.loadLibrary方法时,系统最终会调用到DexPathList类的findLibrary方法,该方法会在nativeLibraryPathElements数组中查找对应的路径,我们将自己的so加入到nativeLibraryPathElements最前面,由此达到动态加入so的目标。 可以通过如下,(注意不同的Android版本实现的方法略不同,需要做好适配,可查看此处的代码 )。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 private object V25 { @Throws(Throwable::class) fun install (classLoader: ClassLoader , folder: File ) { val pathListField = ReflectUtil.findField(classLoader, "pathList" ) val dexPathList = pathListField[classLoader] val nativeLibraryDirectories = ReflectUtil.findField(dexPathList, "nativeLibraryDirectories" ) var origLibDirs = nativeLibraryDirectories[dexPathList] as MutableList<File> if (origLibDirs == null ) { origLibDirs = ArrayList(2 ) } val libDirIt = origLibDirs.iterator() while (libDirIt.hasNext()) { val libDir = libDirIt.next() if (folder == libDir) { libDirIt.remove() break } } origLibDirs.add(0 , folder) val systemNativeLibraryDirectories = ReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories" ) var origSystemLibDirs = systemNativeLibraryDirectories[dexPathList] as List<File> if (origSystemLibDirs == null ) { origSystemLibDirs = ArrayList(2 ) } val newLibDirs: MutableList<File> = ArrayList(origLibDirs.size + origSystemLibDirs.size + 1 ) newLibDirs.addAll(origLibDirs) newLibDirs.addAll(origSystemLibDirs) val makeElements = ReflectUtil.findMethod( dexPathList, "makePathElements" , MutableList::class .java ) val elements = makeElements.invoke(dexPathList, newLibDirs) as Array<Any> val nativeLibraryPathElements = ReflectUtil.findField(dexPathList, "nativeLibraryPathElements" ) nativeLibraryPathElements[dexPathList] = elements } }
2. 需要注意的问题 2.1 依赖库dlopen失败问题 如果我们使用了2个so文件:LibA.so和LibB.so,libA依赖libB。当我们调用系统的System.loadLibrary("A")的时候,Android Framework最终会使用dlopen加载LibA.so 文件,并通过其依赖关系的信息自动使用dlopen加载libB.so 在Android N以前,只要将LibA.so和LibB.so所在的文件目录路径注入到当前的classLoader的nativeLibraryPathElements里,则加载so文件的时候,这两个文件都能正常被找到。从 N 开始,libA.so 能正常加载,而 libB.so 会出现加载失败错误。具体原因是Android Native用来链接so库的Linker.cpp dlopen函数有改动,以前Linker会从ClassLoader实例的nativeLibraryElements里的所有路径查找相应的so文件。更新后,更新之后,Linker 里检索的路径在创建 ClassLoader 实例后就被系统通过 Namespace 机制绑定了,当我们注入新的路径之后,虽然 ClassLoader 里的路径增加了,但是 Linker 里 Namespace 已经绑定的路径集合并没有同步更新,所以出现了 libA.so 文件能找到,而 libB.so 找不到的情况。 面对这个问题有以下几种解决方案:
自定义System.loadLibrary,加载So文件前,先解析So的头文件中的依赖信息,再递归加载依赖的So文件,比如说SoLoader 开源库就如此。我自己实现的Demo 也是这么做的
自定义Linker,完全自己控制So文件的检索逻辑,这就是开源库Relinker
替换classLoader,但不建议这么做
2.1 如何动态化so库相关流程
为了在开发阶段调试简单,第三方so库会存放在lib文件夹下,还是会用Cmake编译So库,最终所有的打包进APK中,代码中加载So库的方式还是用系统的System.loadLibrary("xxx")。最终发布正式版本的时候难道需要人工把so库文件从移除掉,代码中System.loadLibrary("xxx")手动修改吗? 为了解决这些问题可以从AGP构建流程中着手 ,
2.2 通过variant类型移除so库 我看网上很多方案通过在merge${Variant.name}NativeLibs(合并所有依赖的native库)、strip${Variant.name}DebugSymbols(从Native库中移除debug的标志)这两个Task之间插入一个自定义task,用来删除merge${Variant.name}NativeLibs这个任务中需要动态下发So库文件。但这样就会破坏Gradle的缓存机制。其实官方已经给我提供了移除SO库的配置,我们可以在构建Release包的情况下使用如下方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 androidComponents.onVariants { variant -> println("onVariant---${variant.name} " ) val abi = arrayListOf("lib/arm64-v8a/" , "lib/armeabi-v7a/" , "lib/x86/" , "lib/x86_64/" ) val excludeSo = arrayListOf("libnativeLib.so" , "libnative1.so" ) if (variant.name.contains("release" , true )) { abi.forEach { abiPath -> excludeSo.forEach { so -> println("$abiPath $so " ) variant.packaging.jniLibs.excludes.add("$abiPath $so " ) } } } }
ASM替换System.loadLibrary 通过instrumentation API(AGP 7.0之前使用 Transform API )字节码“插桩”的方式把项目中的System.loadLibrary("xxx")替换成你自己封装的So库加载方式:DynamicSoHelp .loadSoLibrary("xxx")。具体如何使用instrumentation API字节码替换,由于篇幅问题就不介绍了。
3.其他 还有很多其它细节需要考虑,比如so库的上传下载,压缩和解压以及安全校验,整个流程中减少人为的操作,尽量通过自动化的流程来完成。在我思考这个过程中,在网上看到货拉拉关于So库动态加载 系统化文章,很建议大家阅读。虽然方案没有开源,但介绍的的很详细,可以借鉴一下。