likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 18

动态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动态加载重点

咱们举个例子,我们要加载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)
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的读者们!

转载自:https://juejin.cn/post/7227029203656867899
评论
请登录