插件化之动态加载so

记录动态加载需要解决的问题和涉及的技术原理


学要解决的问题

  1. 安全性问题
    MD5校验so文件完整性

  2. 版本控制问题
    1)宿主API兼容
    2)可规避有bug的so文件

  3. abi兼容问题
    根据设备架构识别不同的so文件

  4. 代码侵入问题
    自身和第三方sdk的load方法无法统一
    1)采用ASM全部修改成system.load(“ ”)
    2)so路径注入nativeLibraryDirectories数组(classloader内部字段),然后采用system.loadlibrary(“ ”)

涉及的技术原理

  1. so加载流程
    1)安装时,pms根据设备abi拷贝对应的so文件
    2)启动时,so文件路径注入nativeLibraryDirectories数组
    3)从classloader内字段nativeLibraryDirectories查找并加载对应so文件(最后涉及到dlopen)

  2. JNI独立模块
    插件apk内创建新的classloader,成本较大,但收益高

  3. JNI内置宿主
    插件使用宿主的classloader,成本较小,但有明显缺点:nativeLibraryDirectories数组并发问题

  4. dlopen问题
    so内部依赖问题(涉及Linker相关改动),解决方案:

1)分析so依赖,然后递归加载依赖(Soloader)

Linker 里检索的路径在创建 ClassLoader 实例后就被系统通过 Namespace 机制绑定了,当我们注入新的路径之后,虽然 ClassLoader 里的路径增加了,但是 Linker 里 Namespace 已经绑定的路径集合并没有同步更新,所以出现了 libxxx.so 文件(当前的so)能找到,而依赖的so 找不到的情况

2)自定义Linker检索逻辑(Relinker,成本较低)

3)直接替换classloader,规避linker改动 (成本较大)


注:
  1. SD卡等外部存储路径是一种可拆卸的(已安装)不可执行(noexec)的存储介质,不能直接用作作为替换文件的运行目录,使用前应该把文件复制到APP内部存储下再运行。所以使用 System.load 加载so时要注意把so拷贝至 /data/data/package-name/ 下

  2. Android应用所需要的So文件一般被放在/data/data/package_name/lib目录中
    (其实这里已经可以用system.load(path)来调用了)

  3. so库,本质就是一个elf文件,那么so库也符合elf文件的格式,ELF文件由4部分组成,分别是:

  • ELF头(ELF header)
  • 程序头表(Program header table)
  • 节(Section)
  • 节头表(Section header table)
  1. 实际上,一个文件中不一定包含全部内容,而且它们的位置也未必如同所示这样安排,只有ELF头的位置是固定的,其余各部分的位置、大小等信息由ELF头中的各项值来决定。

  2. 依赖其他so的信息存在dynamic字段中,可以用readelf(ndk中就有toolchains目录后) 查看,readelf -d nativecpptwo.so 这里的 -d 就是查看dynamic段的意思


参考链接

https://juejin.cn/post/7107958280097366030 (字节pika文章)
https://github.com/TestPlanB/dyso (字节pika github)
https://cloud.tencent.com/developer/article/1592672 (bugly官方干货)
https://cloud.tencent.com/developer/article/1643819 (作者已经删除相关代码,原因不详)
https://github.com/facebook/SoLoader (递归加载依赖的 so 文件)
https://github.com/google/android-classyshark (so 依赖分析工具)
https://community.jiguang.cn/article/127460 (so相关文章)