动态So加载
回到主题,我们再来谈一下,什么是动态so加载。在我们日常开发项目中,肯定有很多so库,用于我们调用很多native层的能力,比如opencv,ffmpeg,又或者是其他各种各有的so库。同时我们有时候为了兼容很多架构,比如64位/32位的arm/x86等等,会使我们的包体积急剧扩大。动态so技术,其实就是利用远端下发so,在运行时调用下载后的so,从而达到包体积优化的目的。
说到包体积优化,各大公司也是有各种各样的方案,笔者看来,动态so优化,绝对是包体积优化手段中ROI最高的手段,没有之一(狗头)。想想看,一个so大的也有好几M,嘿嘿,收益当然大啦!因此我很久前(也不是很久),就开源过一个项目,SillyBoy用于提供给大家动态so实现的参考,而且这个框架没有任何三方库的依赖,得益于我已经从各个开源库(比如Relink)中crud了很多关键的逻辑(再次狗头保命),哈哈哈,当然我也在使用的过程中给这些三方库提了一些fix问题的pr。因此,SillyBoy本身也是非常轻量化了,(为什么叫这个名字,我也不知道,我喜欢乱取名,现在回想好社死呀)但是一开始我只是作为一个参考项目来去写,很长一段时间都没有去关注了。然而,这个库还是有很多读者或者使用者,特地找到我,去询问了很多问题。因此,趁着有点时间,也为了对得起咱们的读者,我把这个库的代码重新整理了一遍,同时也新增了很多功能与优化点
so动态加载重点
动态so的实现,我已经在这篇文章已经讲过了Android动态加载so,读者们最好先读一下这个,不然就不知道我们接下来讲什么了!,这里就不再重复。实现so动态加载的关键,其实就是解决依赖so在高版本android的namespace问题,这里有两种思路,第一种就是创建自己的classloader,在classloader的时候绑定自己的so库搜索path,就能解决namespace的问题。第二种就是像咱们项目一样,定义好依赖so的加载顺序,namespace会限制加载so时依赖了其他so的加载逻辑。
咱们举个例子,我们要加载native3的so,其中native3依赖了native2,native2依赖了native1,此时如果我们采用了移除so,通过反射添加path搜索路径,然后直接加载native3
System.loadLibrary("native3")
这个时候就会出现以下错误
Process: com.example.sillyboy, PID: 4864
java.lang.UnsatisfiedLinkError: dlopen failed: library "libnative2.so" not found: needed by /data/user/0/com.example.sillyboy/files/dynamic_so/libnative3.so in namespace classloader-namespace
at java.lang.Runtime.loadLibrary0(Runtime.java:1087)
at java.lang.Runtime.loadLibrary0(Runtime.java:1008)
at java.lang.System.loadLibrary(System.java:1664)
at com.example.nativecpp.MainActivity.lambda$onCreate$0(MainActivity.java:34)
at com.example.nativecpp.-$$Lambda$MainActivity$M6tjrjEeZQJBqaKNm24cF3tyMZA.onClick(Unknown Source:0)
at android.view.View.performClick(View.java:7518)
这是因为加载native3的依赖native2的时候(会先加载依赖so),收到了namespcase的限制,而namespace是跟使用的classloader就绑定好了(namespace 相关的知识)
public static ClassLoader createClassLoader(String dexPath,
String librarySearchPath, String libraryPermittedPath, ClassLoader parent,
int targetSdkVersion, boolean isNamespaceShared, String classLoaderName,
List<ClassLoader> sharedLibraries, List<String> nativeSharedLibraries,
List<ClassLoader> sharedLibrariesAfter) {
final ClassLoader classLoader = createClassLoader(dexPath, librarySearchPath, parent,
classLoaderName, sharedLibraries, sharedLibrariesAfter);
String sonameList = "";
if (nativeSharedLibraries != null) {
sonameList = String.join(":", nativeSharedLibraries);
}
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "createClassloaderNamespace");
这里就讲上述的属性传入,创建了一个属于该classloader的namespace
String errorMessage = createClassloaderNamespace(classLoader,
targetSdkVersion,
librarySearchPath,
libraryPermittedPath,
isNamespaceShared,
dexPath,
sonameList);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
if (errorMessage != null) {
throw new UnsatisfiedLinkError("Unable to create namespace for the classloader " +
classLoader + ": " + errorMessage);
}
return classLoader;
}
这里读者可以思考一下,为什么加载单个so的时候不会(答案在JavaVMExt::LoadNativeLibrary加载过程中)因此我们在所有依赖项中,通过解析so,本质也是一个elf文件,先加载最底层的依赖即可,代码在loadSoDynamically中,SillyBoy,这里不补充了。
新增优化点
本次新增的优化点就是,有读者想知道怎么去无侵入实现替换掉项目中的System.loadLibrary,又或者是有限制范围的替换(比如只替换某个类中的System.loadLibrary为咱们动态so的加载逻辑)
咱们也刚刚说到,直接调用loadLibrary去加载有依赖项的so时,会有问题
System.loadLibrary("native3")
因此我们要么把System.loadLibrary都手动换成动态so库的loadSoDynamically方法,要么就是利用字节码的方式去无侵入替换。我们接下来实现一下怎么去用字节码修改实现。
字节码修改System.loadLibrary
老规矩,我们看一下System.loadLibrary编译后的字节码
LDC "native3"
INVOKESTATIC java/lang/System.loadLibrary (Ljava/lang/String;)V
非常简单,核心就是两条指令
因此,我们直接把INVOKESTATIC的指令内容替换掉,就能够实现把System.loadLibrary 切换为咱们自定义的一个静态方法,静态方法里面再走我们动态so的加载即可,这里我们直接拿简单的treeapi实现
private static String PACKAGE_PATH = "com/pika/sillyboy";
private static String OWNER = "java/lang/System";
private static String METHOD_NAME = "loadLibrary";
private static String METHOD_DESC = "(Ljava/lang/String;)V";
private static String DYNAMIC_OWNER = "com/pika/sillyboy/DynamicSoLauncher";
public static void transClass(ClassNode classNode) {
if (classNode.name.startsWith(PACKAGE_PATH)){
return;
}
classNode.methods.forEach(methodNode -> methodNode.instructions.forEach(abstractInsnNode -> {
// 如果是InvokeStatic才继续进行
if (abstractInsnNode.getOpcode() == Opcodes.INVOKESTATIC) {
transformInvokeStatic((MethodInsnNode) abstractInsnNode);
}
}));
}
static void transformInvokeStatic(MethodInsnNode methodInsnNode) {
// (Ljava/lang/String;)V loadLibrary java/lang/System
if (OWNER.equals(methodInsnNode.owner) && METHOD_NAME.equals(methodInsnNode.name) && METHOD_DESC.equals(methodInsnNode.desc)) {
methodInsnNode.owner = DYNAMIC_OWNER;
}
}
替换后的自定义方法实现 DynamicSoLauncher中
DynamicSoLauncher
@JvmStatic
fun loadLibrary(soName: String) {
Log.e("hello", soName)
val wrapSoName = "lib${soName}.so"
loadSoDynamically(wrapSoName)
}
总结
以上代码包括实验代码,都能在这里找到SillyBoy,感谢一直给出star的读者们!