背景
在Android各项优化里面,其中有一块避不开的就是启动优化,大部分的优化往往跟业务相关,比如延迟加载、特定资源预加载等,今天不讨论业务相关,仅从jvm加载类优化聊起,因为它逻辑独立并且实现起来也相对简单。
类加载
简单描述一下Android的类加载,指的是从dex包加载对应的class到方法区中,后续就可使用这个类对象。也是利用这个类加载机制,衍生出了插件化、热修复机制。
ClassLoader
这里就不简述jvm的双亲委派机制了;
-
BootClassLoader 加载Framework的class;
-
PathClassLoader 默认的ClassLoader,负责加载已经安装到系统(/data/app)的apk,它的构造函数只允许传入dexPath、libraryPath(native lib路径)。
-
DexClassLoader 可以不限制地加载不同路径的dex包,DexClassLoaders是插件化、热修复的基础。构造函数可以传入dexPath、libraryPath、optimizedDirectory(dex2oat缓存路径),dex2oat操作会生成 ELF 文件。我们会利用optimizedDirectory参数来对插件的dex进行优化(AOT)。
类加载时序
参考: 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())
}
}
复制代码
优化策略、时机
- 预加载 Class.forName
通过上面的统计,我们知道哪些类加载比较耗时,可选择在合适的时机,将对应的类(特别是kotlin的很多类)适当在子线程加载。
Class.forName(className, true, context.classLoader)
复制代码
其中initialize参数设置为true,表示会初始化静态代码块,和赋值静态变量等操作
- 禁用 VerifyClass
在ClassLoader#loadClass流程中,都要经过defineClass(从dex包io加载class)、verifyClass(验证指令)、resolveClass(链接class,分配字段内存)和init(初始化静态变量和代码块)
如果你用Systrace或者perfetto工具去查看加载耗时,其中有VerifyClass耗费的时间是相当多的。
插件化
现在基本各大厂的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、优化启动速度。
上面的先留坑吧,等后面慢慢整理。