Android启动优化-类预加载

背景

在Android各项优化里面,其中有一块避不开的就是启动优化,大部分的优化往往跟业务相关,比如延迟加载、特定资源预加载等,今天不讨论业务相关,仅从jvm加载类优化聊起,因为它逻辑独立并且实现起来也相对简单。

类加载

简单描述一下Android的类加载,指的是从dex包加载对应的class到方法区中,后续就可使用这个类对象。也是利用这个类加载机制,衍生出了插件化、热修复机制。

ClassLoader

这里就不简述jvm的双亲委派机制了;

  1. BootClassLoader 加载Framework的class;

  2. PathClassLoader 默认的ClassLoader,负责加载已经安装到系统(/data/app)的apk,它的构造函数只允许传入dexPath、libraryPath(native lib路径)。

  3. DexClassLoader 可以不限制地加载不同路径的dex包,DexClassLoaders是插件化、热修复的基础。构造函数可以传入dexPath、libraryPath、optimizedDirectory(dex2oat缓存路径),dex2oat操作会生成 ELF 文件。我们会利用optimizedDirectory参数来对插件的dex进行优化(AOT)。

类加载时序

image.png

参考: zhuanlan.zhihu.com/p/33509426

如何知道哪些类比较耗时

在线上的阶段发现不少ANR都出现在Class#findClass上面,若是在合适的时机能够提前在非UI线程下预加载耗时的class,也能减少ANR的概率。

我们可以利用Hook ClassLoader的方式在线下/线上统计哪些class比较耗时。

/**
 * 统计loadClass耗时
 */
class LogClassLoader(dexPath: String, parent: ClassLoader) : PathClassLoader(dexPath, parent) {

    override fun loadClass(name: String): Class<*> {
        if (name.startsWith("android.") || name.startsWith("java.lang")) {
            return super.loadClass(name)
        }
        val start = System.currentTimeMillis()
        try {
            return super.loadClass(name)
        } finally {
            val cost= System.currentTimeMillis() - start
            val thread = Thread.currentThread().name
            Log.i("loadClass", "load $name thread:$thread cost: $cost")
        }
    }
}
复制代码

在初始化入口,hook classLoader,最后可统计出哪些类相对比较耗时。

fun hook(application: Application) {
    val pathClassLoader = application.classLoader
    try {
        val logClassLoader = LogClassLoader("", pathClassLoader.parent)
        val pathListField = BaseDexClassLoader::class.java.getDeclaredField("pathList")
        pathListField.isAccessible = true
        val pathList = pathListField.get(pathClassLoader)
        pathListField.set(logClassLoader, pathList)

        val parentField = ClassLoader::class.java.getDeclaredField("parent")
        parentField.isAccessible = true
        parentField.set(pathClassLoader, logClassLoader)
    } catch (throwable: Throwable) {
        Log.e("hook", throwable.stackTraceToString())
    }
}
复制代码

优化策略、时机

  1. 预加载 Class.forName

通过上面的统计,我们知道哪些类加载比较耗时,可选择在合适的时机,将对应的类(特别是kotlin的很多类)适当在子线程加载。

Class.forName(className, true, context.classLoader)
复制代码

其中initialize参数设置为true,表示会初始化静态代码块,和赋值静态变量等操作

  1. 禁用 VerifyClass

在ClassLoader#loadClass流程中,都要经过defineClass(从dex包io加载class)、verifyClass(验证指令)、resolveClass(链接class,分配字段内存)和init(初始化静态变量和代码块)

如果你用Systrace或者perfetto工具去查看加载耗时,其中有VerifyClass耗费的时间是相当多的。

可以参考:juejin.cn/post/695122…

插件化

现在基本各大厂的App都用到了插件化,例如微信、抖音等等。插件化有几个重要的点:加载插件的类class、resource资源、插件的so。

这里面的坑点也很多,例如:

  • 不同的classloader会加载不同namespace的so,插件与插件的so相互依赖就有问题;

  • 插件的ClassLoader跟主包的ClassLoader如何做到可以加载主包和插件包的class,也保证同样的class不会被不同的classLoader重复加载;

  • 插件化的dex包是不会dex2oat,一般我们开额外的进程来做dex2oat操作(AOT),编译优化后就可以设置为插件ClassLoader的optimizedDirectory,但是8.0之后BaseDexClassLoader的 optimizedDirectory 参数已经没用了,意味着插件AOT失效了;

  • Android 5.0之前是在安装的时候全量编译AOT,会变得巨慢;Android 7.0 之后是用了JIT和AOT,这有可能导致启动的时候就开始AOT,其中有一个优化手段就是抑制或者延迟AOT, 以避免ANR、优化启动速度。

上面的先留坑吧,等后面慢慢整理。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享