布景
为了进步App的冷发动耗时,除了在惯例的事务侧进行耗时代码优化之外,为了进一步缩短发动耗时,需求在纯技能测做一些优化探究,本期咱们从类预加载、Retrofit 、ARouter方面进行了进一步的优化。从测验数据上来看,这些优化手法的收益有限,或许在中端机上加起来也不超越50ms的收益,但为了冷发动场景的极致优化,给用户带来更好的体验,任何有收益的优化手法都是值得测验的。
类预加载
一个类的完整加载流程至少包含 加载、链接、初始化,而类的加载在一个进程中只会触发一次,因而关于冷发动场景,咱们能够异步加载原本在发动阶段会在主线程触发类加载进程的类,这样当原流程在主线程访问到该类时就不会触发类加载流程。
Hook ClassLoader 完成
在Android体系中,类的加载都是经过PathClassLoader 完成的,依据类加载的父类委托机制,咱们能够经过Hook PathClassLoader 修正其默许的parent 来完成。
首要咱们创立一个MonitorClassLoader 承继自PathClassLoader,并在其内部记载类加载耗时
class MonitorClassLoader(
dexPath: String,
parent: ClassLoader, private val onlyMainThread: Boolean = false,
) : PathClassLoader(dexPath, parent) {
val TAG = "MonitorClassLoader"
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
if (cost > 1000_000){
Log.e(TAG, "加载 ${clazz} 耗时 ${(end - begin) / 1000} 微秒 ,线程ID ${Thread.currentThread().id}")
} else {
Log.d(TAG, "加载 ${clazz} 耗时 ${(end - begin) / 1000} 微秒 ,线程ID ${Thread.currentThread().id}")
}
return clazz;
}
}
之后,咱们能够在Application attach阶段 反射替换 application实例的classLoader 对应的parent指向。
中心代码如下:
companion object {
@JvmStatic
fun hook(application: Application, onlyMainThread: Boolean = false) {
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 = ClassLoader::class.java.getDeclaredField("parent")
parentField.isAccessible = true
parentField.set(pathClassLoader, monitorClassLoader)
} catch (throwable: Throwable) {
Log.e("hook", throwable.stackTraceToString())
}
}
}
首要逻辑为
- 反射获取原始 pathClassLoader 的 pathList
- 创立MonitorClassLoader,并反射设置 正确的 pathList
- 反射替换 原始pathClassLoader的 parent指向 MonitorClassLoader实例
这样,咱们就获取发动阶段的加载类了
依据JVMTI 完成
除了经过 Hook ClassLoader的计划完成,咱们也能够经过JVMTI 来完成类加载监控。关于JVMTI 可参阅之前的文章 /post/694278…。
经过注册ClassPrepare Callback, 能够在每个类Prepare阶段触发回调。
当然这种计划,比较 Hook ClassLoader 还是要繁琐许多,不过依据JVMTI 还能够做许多其他更强壮的事。
类预加载完成
现在运用通常都是多模块的,因而咱们能够规划一个笼统接口,不同的事务模块能够承继该笼统接口,界说不同事务模块需求进行预加载的类。
/**
* 资源预加载接口
*/
public interface PreloadDemander {
/**
* 装备所有需求预加载的类
* @return
*/
Class[] getPreloadClasses();
}
之后在发动阶段搜集所有的 Demander实例,并触发预加载
/**
* 类预加载执行器
*/
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能够手动搜集 发动阶段主线程的类列表,那么 咱们是否能够在端上 每次发动时 自动搜集加载的类,假如发现这个类不在现有 的名单中 则加入到名单,在下次发动时进行预加载。 当然 详细的战略还需求做详细规划,比方 控制预加载名单的列表巨细, 被加入预加载名单的类最低耗时阈值, 淘汰战略等等。
Retrofit ServiceMethod 预解析注入
布景
Retrofit 是现在最常用的网络库结构,其依据注解装备的网络恳求方法及Adapter的规划模式大大简化了网络恳求的调用方法。 不过其并没有选用相似APT的方法在编译时生成恳求代码,而是选用运行时解析的方法。
当咱们调用Retrofit.create(final Class service) 函数时,会生成一个该笼统接口的动态署理实例。
接口的所有函数调用都会被转发到该动态署理方针的invoke函数,终究调用loadServiceMethod(method).invoke 调用。
在loadServiceMethod函数中,需求解析原函数上的各种元信息,包含函数注解、参数注解、参数类型、返回值类型等信息,并终究生成ServiceMethod 实例,对原接口函数的调用其实终究触发的是 这个生成的ServiceMethod invoke函数的调用。
从源码完成上能够看出,对ServiceMethod的实例做了缓存处理,每个Method 对应一个ServiceMethod。
耗时测验
这儿我模拟了一个简单的 Service Method, 并调用archiveStat 观察初次调用及其后续调用的耗时,注意这儿的调用还未触发网络恳求,其返回的是一个Call方针。
从测验成果上看,初次调用需求触发需求耗费1.7ms,而后续的调用 只需求耗费50微妙左右。
优化计划
由于初次调用接口函数需求触发ServiceMethod实例的生成,这个进程比较耗时,因而优化思路也比较简单,搜集发动阶段会调用的 函数,提早生成ServiceMethod实例并写入到缓存中。
serviceMethodCache 的类型自身是ConcurrentHashMap,所以它是并发安全的。
可是源码中 进行ServiceMethod缓存判断的时分 还是以 serviceMethodCache为Lock Object 进行了加锁,这导致 多线程触发一起初次触发不同Method的调用时,存在锁等候问题
这儿首要需求理解为什么这儿需求加锁,其目的也是由于parseAnnotations 是一个好事操作,这儿是为了完成相似 putIfAbsent的完全原子性操作。 但实际上这儿加锁能够以 对应的Method类型为锁方针,由于自身不同Method 对应的ServiceMethod实例便是不同的。 咱们能够修正其源码的完成来防止这种场景的锁竞赛问题。
当然针对咱们的优化场景,其实不修正源码也是能够完成的,由于 ServiceMethod.parseAnnotations 是无锁的,究竟它是一个纯函数。 因而咱们能够在异步线程调用parseAnnotations 生成ServiceMethod 实例,之后经过反射 写入 Retrofit实例的 serviceMethodCache 中。这样存在的问题是 不同线程或许一起触发了一个Method的解析注入,但 由于serviceMethodCache 自身便是线程安全的,所以 它只是多做了一次解析,对终究成果并无影响。
ServiceMethod.parseAnnotations是包级私有的,咱们能够在当时工程创立一个一样的包,这样就能够直接调用该函数了。 中心完成代码如下
package retrofit2
import android.os.Build
import timber.log.Timber
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.lang.reflect.Modifier
object RetrofitPreloadUtil {
private var loadServiceMethod: Method? = null
var initSuccess: Boolean = false
// private var serviceMethodCacheField:Map<Method,ServiceMethod<Any>>?=null
private var serviceMethodCacheField: Field? = null
init {
try {
serviceMethodCacheField = Retrofit::class.java.getDeclaredField("serviceMethodCache")
serviceMethodCacheField?.isAccessible = true
if (serviceMethodCacheField == null) {
for (declaredField in Retrofit::class.java.declaredFields) {
if (Map::class.java.isAssignableFrom(declaredField.type)) {
declaredField.isAccessible =true
serviceMethodCacheField = declaredField
break
}
}
}
loadServiceMethod = Retrofit::class.java.getDeclaredMethod("loadServiceMethod", Method::class.java)
loadServiceMethod?.isAccessible = true
} catch (e: Exception) {
initSuccess = false
}
}
/**
* 预加载 方针service 的 相关函数,并注入到对应retrofit实例中
*/
fun preloadClassMethods(retrofit: Retrofit, service: Class<*>, methodNames: Array<String>) {
val field = serviceMethodCacheField ?: return
val map = field.get(retrofit) as MutableMap<Method,ServiceMethod<Any>>
for (declaredMethod in service.declaredMethods) {
if (!isDefaultMethod(declaredMethod) && !Modifier.isStatic(declaredMethod.modifiers)
&& methodNames.contains(declaredMethod.name)) {
try {
val parsedMethod = ServiceMethod.parseAnnotations<Any>(retrofit, declaredMethod) as ServiceMethod<Any>
map[declaredMethod] =parsedMethod
} catch (e: Exception) {
Timber.e(e, "load method $declaredMethod for class $service failed")
}
}
}
}
private fun isDefaultMethod(method: Method): Boolean {
return Build.VERSION.SDK_INT >= 24 && method.isDefault;
}
}
预加载名单搜集
有了优化计划后,还需求搜集原本在发动阶段会在主线程进行Retrofit ServiceMethod调用的列表, 这儿采纳的是字节码插桩的方法,运用的LancetX 结构进行修正。
现在名单的装备是预先搜集好,在装备中心进行装备,运行时依据装备中写的装备 进行预加载。 这儿还能够供给其他的装备计划,比方 供给一个注解用于标注该Retrofit函数需求进行预解析,
之后,在编译期间搜集所有需求预加载的Service及函数,生成对应的名单,不过这个计划需求必定开发成本,而且需求去修正事务模块的代码,现在的阶段还处于验证收益阶段,所以暂未施行。
收益
App搜集了发动阶段20个左右的Method 进行预加载,预计提高10~20ms。
ARouter
布景
ARouter结构供给了路由注册跳转 及 SPI 能力。为了优化冷发动速度,关于某些服务实例能够在发动阶段进行预加载生成对应的实例方针。
ARouter的注册信息是在预编译阶段(依据APT) 生成的,在编译阶段又经过ASM 生成对应映射联系的注入代码。
而在运行时以获取Service实例为例,当调用navigation函数获取实例终究会调用到 completion函数。
当初次调用时,其对应的RouteMeta 实例没有生成,会继续调用 addRouteGroupDynamic函数进行注册。
addRouteGroupDynamic 会创立对应预编译阶段生成的服务注册类并调用loadInto函数进行注册。而某些事务模块怎么服务注册信息比较多,这儿的loadInto就会比较耗时。
全体来看,关于获取Service实例的流程, completion的整个流程 涉及到 loadInto信息注册、Service实例反射生成、及init函数的调用。 而completion函数是synchronized的,因而无法利用多线程进行注册来缩短发动耗时。
优化计划
这儿的优化其实和Retroift Service 的注册机制相似,不同的Service注册时,其对应的元信息类(IRouteGroup)其实是不同的,因而只需求对对应的IRouteGroup加锁即可。
在completion的后半部分流程中,针对Provider实例生产的流程也需求进行独自加锁,防止屡次调用init函数。
收益
依据线下搜集的数据 装备了20+预加载的Service Method, 预期收益 10~20ms (中端机) 。
其他
后续将继续结合自身事务现状以及其他一线大厂共享的样例,在 x2c、class verify、禁用JIT、 disableDex2AOT等方面继续测验优化。
假如经过本文对你有所收获,能够来个点赞、保藏、重视三连,后续将共享更多功能监控与优化相关的文章。
也能够重视个人大众号:编程物语
本文相关测验代码已共享至github: github.com/Knight-ZXW/…
APM功能监控与优化专栏
功能优化专栏历史文章:
文章 | 地址 |
---|---|
Android平台下的cpu利用率优化完成 | /post/724324… |
抖音音讯调度优化发动速度计划实践 | /post/721766… |
扒一扒抖音是怎么做线程优化的 | /post/721244… |
监控Android Looper Message调度的另一种姿态 | /post/713974… |
Android 高版别收集体系CPU运用率的方法 | /post/713503… |
Android 平台下的 Method Trace 完成及运用 | /post/710713… |
Android 怎么解决运用SharedPreferences 造成的卡顿、ANR问题 | /post/705476… |
依据JVMTI 完成功能监控 | /post/694278… |