动态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

非常简单,中心便是两条指令

我填坑了 - 完善动态so加载库

因而,咱们直接把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的读者们!