文章

冷启动优化-class预加载

冷启动优化-class预加载

class 的加载

一个类的完整加载流程至少包括 加载链接初始化,而类的加载在一个进程中只会触发一次,因此对于冷启动场景,我们可以异步加载原本在启动阶段会在主线程触发类加载过程的类,这样当原流程在主线程访问到该类时就不会触发类加载流程。

ClassLoader 基础:[[ClassLoader基础]]

获取启动阶段需要预加载的 class

Hook ClassLoader

在 Android 系统中,类的加载都是通过 PathClassLoader 实现的,基于类加载的父类委托机制,我们可以通过 Hook PathClassLoader 修改其默认的 parent 来实现。

  • 创建一个 MonitorClassLoader 继承自 PathClassLoader,并在其内部记录类加载及其耗时的情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class MonitorClassLoader(
    dexPath: String?,
    parent: ClassLoader?,
    private val onlyMainThread: Boolean = false,
) :
    PathClassLoader(dexPath, parent) {
    private val TAG = "hacket.MonitorClassLoader"

    companion object {
        var classLoadCost = 0L
    }
    @SuppressLint("LongLogTag")
    override fun loadClass(name: String?, resolve: Boolean): Class<*> {
        val begin = SystemClock.elapsedRealtimeNanos()
        if (onlyMainThread && Looper.getMainLooper().thread != Thread.currentThread()) {
            return super.loadClass(name, resolve)
        }
        val clazz = super.loadClass(name, resolve)
        val end = SystemClock.elapsedRealtimeNanos()
        val cost = end - begin
        classLoadCost += cost
        if (cost > 1000_000L) {
            Log.e(
                TAG,
                "加载 $clazz 耗时 ${(end - begin) / 1000L} 微秒 ,线程ID ${Thread.currentThread().id}(${Thread.currentThread().name}),总耗时=${classLoadCost / 1000L} 微秒"
            )
        } else {
            Log.d(
                TAG,
                "加载 $clazz 耗时 ${(end - begin) / 1000L} 微秒 ,线程ID ${Thread.currentThread().id}(${Thread.currentThread().name}),总耗时=${classLoadCost / 1000L} 微秒(${classLoadCost / 1000L/1000F} 豪秒)"
            )
        }
        return clazz;
    }
}
  • Application attachBaseContext 阶段反射替换 application 实例的 classLoader 对应的 parent 指向
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
object HookClassLoader {
    @JvmStatic
    fun hook(application: Application, onlyMainThread: Boolean = true) {
        val pathClassLoader = application.classLoader
        try {
            val monitorClassLoader = MonitorClassLoader("", pathClassLoader.parent, onlyMainThread)
            val pathListField = BaseDexClassLoader::class.java.getDeclaredField("pathList")
            pathListField.isAccessible = true
            val pathList = pathListField.get(pathClassLoader)
            pathListField.set(monitorClassLoader, pathList)

//            val parentField = BaseDexClassLoader::class.java.getDeclaredField("parent")
            val parentField =
                ClassLoader::class.java.getDeclaredField("parent") // BaseDexClassLoader的父类是ClassLoader
            parentField.isAccessible = true
            parentField.set(pathClassLoader, monitorClassLoader)
        } catch (throwable: Throwable) {
            Log.e("hook", throwable.stackTraceToString())
        }
    }
}

改之前:PathClassLoader.parent → BootClassLoader

Hook 之后:

  • PathClassLoader.parent → MonitorClassLoader.parent → BootClassLoader
  • MonitorClassLoader.pathList = PathClassLoader.pathList

这样就能获取到启动阶段的加载类和耗时情况

基于 JVMTI 实现

什么是 JVMTI?

JVMTI 全称 JVM Tool Interface,它是 Java 虚拟机定义的一个开发和监控 JVM 使用的程序接口 (programing interface),通过该接口可以探查 JVM 内部的一些运行状态,甚至控制 JVM 应用程序的执行。需要注意的是,并非所有的 JVM 实现都支持 JVMTI

JVMTI 是双通道接口 (two-way interface)。JVMTI 的客户端,或称为代理 (agent),agent 可以通过注册监听感兴趣的事件,另外,JVMTI 提供了很多操作函数可以直接用来控制应用程序。

JVMTI 代理与目标 JVM 运行在同一个进程中,通过 JVMTI 进行通信,最大化控制能力,最小化通信成本

Ref

class 预加载实现

简单实现

将每个类的全路径,用 Class.forName 加载进来

1
2
3
4
5
6
7
8
9
10
object PreVerifyUtils {
    fun verify() {
        try {
            Class.forName(XXX::class.java.name)
            Class.forName(XXX1::class.java.name)
            Class.forName(XXX2::class.java.name)
            // ...
        }
    }
}

封装

目前应用通常都是多模块的,因此我们可以设计一个抽象接口,不同的业务模块可以继承该抽象接口,定义不同业务模块需要进行预加载的类。

1
2
3
4
5
6
7
8
9
10
/**
 * 资源预加载接口
 */
public interface PreloadDemander {
    /**
     * 配置所有需要预加载的类
     * @return
     */
    Class[] getPreloadClasses();
}

之后在启动阶段收集所有的类加载实例,并触发预加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 类预加载执行器
 */
object ClassPreloadExecutor {
    private val demanders = mutableListOf<PreloadDemander>()
    fun addDemander(classPreloadDemander: PreloadDemander) {
        demanders.add(classPreloadDemander)
    }
    /**
     * this method shouldn't run on main thread
     */
    @WorkerThread fun doPreload() {
	for (demander in localDemanders) {
		val classes = demander.preloadClasses
		classes.forEach {
			val classLoader = ClassPreloadExecutor::class.java.classLoader
			Class.forName(it.name, true, classLoader)
			}
		}
    }
}

收益

  • 配置了大概 90 个类,在终端机型测试数据显示 这些类的加载需要消耗30ms左右的 cpu 时间,不同类加载的消耗时间差异主要来自于类的复杂度 比如继承体系、字段属性数量等, 以及类初始化阶段的耗时,比如静态成员变量的立即初始化、静态代码块的执行等。

优化

目前的方案 配置的具体类列表来源于手动配置,这种方案的弊端在于,类的列表需要开发维护,在版本快速迭代变更的情况下 维护成本较大, 并且对于一些大型 App,存在着非常多的 AB 实验条件,这也可能导致不同的用户在类加载上是会有区别的。

使用自定义的 ClassLoader 可以手动收集启动阶段主线程的类列表,那么我们是否可以在端上每次启动时自动收集加载的类,如果发现这个类不在现有的名单中则加入到名单,在下次启动时进行预加载。当然具体的策略还需要做详细设计,比如 控制预加载名单的列表大小被加入预加载名单的类最低耗时阈值淘汰策略 等等。

Ref

本文由作者按照 CC BY 4.0 进行授权