欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android冷啟動(dòng)優(yōu)化的3個(gè)小案例分享

 更新時(shí)間:2023年07月26日 11:18:08   作者:卓修武K  
為了提高App的冷啟動(dòng)耗時(shí),除了在常規(guī)的業(yè)務(wù)側(cè)進(jìn)行耗時(shí)代碼優(yōu)化之外,為了進(jìn)一步縮短啟動(dòng)耗時(shí),需要在純技術(shù)測(cè)做一些優(yōu)化探索,本期我們從類預(yù)加載、Retrofit 、ARouter方面進(jìn)行了進(jìn)一步的優(yōu)化,感興趣的同學(xué)跟著小編一起來看看吧

背景

為了提高App的冷啟動(dòng)耗時(shí),除了在常規(guī)的業(yè)務(wù)側(cè)進(jìn)行耗時(shí)代碼優(yōu)化之外,為了進(jìn)一步縮短啟動(dòng)耗時(shí),需要在純技術(shù)測(cè)做一些優(yōu)化探索,本期我們從類預(yù)加載、Retrofit 、ARouter方面進(jìn)行了進(jìn)一步的優(yōu)化。從測(cè)試數(shù)據(jù)上來看,這些優(yōu)化手段的收益有限,可能在中端機(jī)上加起來也不超過50ms的收益,但為了冷啟動(dòng)場(chǎng)景的極致優(yōu)化,給用戶帶來更好的體驗(yàn),任何有收益的優(yōu)化手段都是值得嘗試的。

類預(yù)加載

一個(gè)類的完整加載流程至少包括 加載、鏈接、初始化,而類的加載在一個(gè)進(jìn)程中只會(huì)觸發(fā)一次,因此對(duì)于冷啟動(dòng)場(chǎng)景,我們可以異步加載原本在啟動(dòng)階段會(huì)在主線程觸發(fā)類加載過程的類,這樣當(dāng)原流程在主線程訪問到該類時(shí)就不會(huì)觸發(fā)類加載流程。

Hook ClassLoader 實(shí)現(xiàn)

在Android系統(tǒng)中,類的加載都是通過PathClassLoader 實(shí)現(xiàn)的,基于類加載的父類委托機(jī)制,我們可以通過Hook PathClassLoader 修改其默認(rèn)的parent 來實(shí)現(xiàn)。

首先我們創(chuàng)建一個(gè)MonitorClassLoader 繼承自PathClassLoader,并在其內(nèi)部記錄類加載耗時(shí)

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} 耗時(shí) ${(end - begin) / 1000} 微秒 ,線程ID ${Thread.currentThread().id}")
    } else {
        Log.d(TAG, "加載 ${clazz} 耗時(shí) ${(end - begin) / 1000} 微秒 ,線程ID ${Thread.currentThread().id}")
    }
    return  clazz;
}
}

之后,我們可以在Application attach階段 反射替換 application實(shí)例的classLoader 對(duì)應(yīng)的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
  • 創(chuàng)建MonitorClassLoader,并反射設(shè)置 正確的 pathList
  • 反射替換 原始pathClassLoader的 parent指向 MonitorClassLoader實(shí)例

這樣,我們就獲取啟動(dòng)階段的加載類了

基于JVMTI 實(shí)現(xiàn)

除了通過 Hook ClassLoader的方案實(shí)現(xiàn),我們也可以通過JVMTI 來實(shí)現(xiàn)類加載監(jiān)控。

通過注冊(cè)ClassPrepare Callback, 可以在每個(gè)類Prepare階段觸發(fā)回調(diào)。

當(dāng)然這種方案,相比 Hook ClassLoader 還是要繁瑣很多,不過基于JVMTI 還可以做很多其他更強(qiáng)大的事。

類預(yù)加載實(shí)現(xiàn)

目前應(yīng)用通常都是多模塊的,因此我們可以設(shè)計(jì)一個(gè)抽象接口,不同的業(yè)務(wù)模塊可以繼承該抽象接口,定義不同業(yè)務(wù)模塊需要進(jìn)行預(yù)加載的類。

/**
 * 資源預(yù)加載接口
 */
public interface PreloadDemander {
    /**
     * 配置所有需要預(yù)加載的類
     * @return
     */
    Class[] getPreloadClasses();
}

之后在啟動(dòng)階段收集所有的 Demander實(shí)例,并觸發(fā)預(yù)加載

/**
 * 類預(yù)加載執(zhí)行器
 */
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)
    			}
			}
    }
}

收益

第一個(gè)版本配置了大概90個(gè)類,在終端機(jī)型測(cè)試數(shù)據(jù)顯示 這些類的加載需要消耗30ms左右的cpu時(shí)間,不同類加載的消耗時(shí)間差異主要來自于類的復(fù)雜度 比如繼承體系、字段屬性數(shù)量等, 以及類初始化階段的耗時(shí),比如靜態(tài)成員變量的立即初始化、靜態(tài)代碼塊的執(zhí)行等。

方案優(yōu)化思考

我們目前的方案 配置的具體類列表來源于手動(dòng)配置,這種方案的弊端在于,類的列表需要開發(fā)維護(hù),在版本快速迭代變更的情況下 維護(hù)成本較大, 并且對(duì)于一些大型App,存在著非常多的AB實(shí)驗(yàn)條件,這也可能導(dǎo)致不同的用戶在類加載上是會(huì)有區(qū)別的。

在前面的小節(jié)中,我們介紹了使用自定義的 ClassLoader可以手動(dòng)收集 啟動(dòng)階段主線程的類列表,那么 我們是否可以在端上 每次啟動(dòng)時(shí) 自動(dòng)收集加載的類,如果發(fā)現(xiàn)這個(gè)類不在現(xiàn)有 的名單中 則加入到名單,在下次啟動(dòng)時(shí)進(jìn)行預(yù)加載。 當(dāng)然 具體的策略還需要做詳細(xì)設(shè)計(jì),比如 控制預(yù)加載名單的列表大小, 被加入預(yù)加載名單的類最低耗時(shí)閾值, 淘汰策略等等。

Retrofit ServiceMethod 預(yù)解析注入

背景

Retrofit 是目前最常用的網(wǎng)絡(luò)庫框架,其基于注解配置的網(wǎng)絡(luò)請(qǐng)求方式及Adapter的設(shè)計(jì)模式大大簡(jiǎn)化了網(wǎng)絡(luò)請(qǐng)求的調(diào)用方式。 不過其并沒有采用類似APT的方式在編譯時(shí)生成請(qǐng)求代碼,而是采用運(yùn)行時(shí)解析的方式。

當(dāng)我們調(diào)用Retrofit.create(final Class service) 函數(shù)時(shí),會(huì)生成一個(gè)該抽象接口的動(dòng)態(tài)代理實(shí)例。

接口的所有函數(shù)調(diào)用都會(huì)被轉(zhuǎn)發(fā)到該動(dòng)態(tài)代理對(duì)象的invoke函數(shù),最終調(diào)用loadServiceMethod(method).invoke 調(diào)用。

在loadServiceMethod函數(shù)中,需要解析原函數(shù)上的各種元信息,包括函數(shù)注解、參數(shù)注解、參數(shù)類型、返回值類型等信息,并最終生成ServiceMethod 實(shí)例,對(duì)原接口函數(shù)的調(diào)用其實(shí)最終觸發(fā)的是 這個(gè)生成的ServiceMethod invoke函數(shù)的調(diào)用。

從源碼實(shí)現(xiàn)上可以看出,對(duì)ServiceMethod的實(shí)例做了緩存處理,每個(gè)Method 對(duì)應(yīng)一個(gè)ServiceMethod。

耗時(shí)測(cè)試

這里我模擬了一個(gè)簡(jiǎn)單的 Service Method, 并調(diào)用archiveStat 觀察首次調(diào)用及其后續(xù)調(diào)用的耗時(shí),注意這里的調(diào)用還未觸發(fā)網(wǎng)絡(luò)請(qǐng)求,其返回的是一個(gè)Call對(duì)象。

從測(cè)試結(jié)果上看,首次調(diào)用需要觸發(fā)需要消耗1.7ms,而后續(xù)的調(diào)用 只需要消耗50微妙左右。

優(yōu)化方案

由于首次調(diào)用接口函數(shù)需要觸發(fā)ServiceMethod實(shí)例的生成,這個(gè)過程比較耗時(shí),因此優(yōu)化思路也比較簡(jiǎn)單,收集啟動(dòng)階段會(huì)調(diào)用的 函數(shù),提前生成ServiceMethod實(shí)例并寫入到緩存中。

serviceMethodCache 的類型本身是ConcurrentHashMap,所以它是并發(fā)安全的。

但是源碼中 進(jìn)行ServiceMethod緩存判斷的時(shí)候 還是以 serviceMethodCache為L(zhǎng)ock Object 進(jìn)行了加鎖,這導(dǎo)致 多線程觸發(fā)同時(shí)首次觸發(fā)不同Method的調(diào)用時(shí),存在鎖等待問題

這里首先需要理解為什么這里需要加鎖,其目的也是因?yàn)閜arseAnnotations 是一個(gè)好事操作,這里是為了實(shí)現(xiàn)類似 putIfAbsent的完全原子性操作。 但實(shí)際上這里加鎖可以以 對(duì)應(yīng)的Method類型為鎖對(duì)象,因?yàn)楸旧聿煌琈ethod 對(duì)應(yīng)的ServiceMethod實(shí)例就是不同的。 我們可以修改其源碼的實(shí)現(xiàn)來避免這種場(chǎng)景的鎖競(jìng)爭(zhēng)問題。

當(dāng)然針對(duì)我們的優(yōu)化場(chǎng)景,其實(shí)不修改源碼也是可以實(shí)現(xiàn)的,因?yàn)?ServiceMethod.parseAnnotations 是無鎖的,畢竟它是一個(gè)純函數(shù)。 因此我們可以在異步線程調(diào)用parseAnnotations 生成ServiceMethod 實(shí)例,之后通過反射 寫入 Retrofit實(shí)例的 serviceMethodCache 中。這樣存在的問題是 不同線程可能同時(shí)觸發(fā)了一個(gè)Method的解析注入,但 由于serviceMethodCache 本身就是線程安全的,所以 它只是多做了一次解析,對(duì)最終結(jié)果并無影響。

ServiceMethod.parseAnnotations是包級(jí)私有的,我們可以在當(dāng)前工程創(chuàng)建一個(gè)一樣的包,這樣就可以直接調(diào)用該函數(shù)了。 核心實(shí)現(xiàn)代碼如下

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
        }
    }
    /**
     * 預(yù)加載 目標(biāo)service 的 相關(guān)函數(shù),并注入到對(duì)應(yīng)retrofit實(shí)例中
     */
    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;
    }
}

預(yù)加載名單收集

有了優(yōu)化方案后,還需要收集原本在啟動(dòng)階段會(huì)在主線程進(jìn)行Retrofit ServiceMethod調(diào)用的列表, 這里采取的是字節(jié)碼插樁的方式,使用的LancetX 框架進(jìn)行修改。

目前名單的配置是預(yù)先收集好,在配置中心進(jìn)行配置,運(yùn)行時(shí)根據(jù)配置中寫的配置 進(jìn)行預(yù)加載。 這里還可以提供其他的配置方案,比如 提供一個(gè)注解用于標(biāo)注該Retrofit函數(shù)需要進(jìn)行預(yù)解析,

之后,在編譯期間收集所有需要預(yù)加載的Service及函數(shù),生成對(duì)應(yīng)的名單,不過這個(gè)方案需要一定開發(fā)成本,并且需要去修改業(yè)務(wù)模塊的代碼,目前的階段還處于驗(yàn)證收益階段,所以暫未實(shí)施。

收益

App收集了啟動(dòng)階段20個(gè)左右的Method 進(jìn)行預(yù)加載,預(yù)計(jì)提升10~20ms。

ARouter

背景

ARouter框架提供了路由注冊(cè)跳轉(zhuǎn) 及 SPI 能力。為了優(yōu)化冷啟動(dòng)速度,對(duì)于某些服務(wù)實(shí)例可以在啟動(dòng)階段進(jìn)行預(yù)加載生成對(duì)應(yīng)的實(shí)例對(duì)象。

ARouter的注冊(cè)信息是在預(yù)編譯階段(基于APT) 生成的,在編譯階段又通過ASM 生成對(duì)應(yīng)映射關(guān)系的注入代碼。

而在運(yùn)行時(shí)以獲取Service實(shí)例為例,當(dāng)調(diào)用navigation函數(shù)獲取實(shí)例最終會(huì)調(diào)用到 completion函數(shù)。

當(dāng)首次調(diào)用時(shí),其對(duì)應(yīng)的RouteMeta 實(shí)例尚未生成,會(huì)繼續(xù)調(diào)用 addRouteGroupDynamic函數(shù)進(jìn)行注冊(cè)。

addRouteGroupDynamic 會(huì)創(chuàng)建對(duì)應(yīng)預(yù)編譯階段生成的服務(wù)注冊(cè)類并調(diào)用loadInto函數(shù)進(jìn)行注冊(cè)。而某些業(yè)務(wù)模塊如何服務(wù)注冊(cè)信息比較多,這里的loadInto就會(huì)比較耗時(shí)。

整體來看,對(duì)于獲取Service實(shí)例的流程, completion的整個(gè)流程 涉及到 loadInto信息注冊(cè)、Service實(shí)例反射生成、及init函數(shù)的調(diào)用。 而completion函數(shù)是synchronized的,因此無法利用多線程進(jìn)行注冊(cè)來縮短啟動(dòng)耗時(shí)。

優(yōu)化方案

這里的優(yōu)化其實(shí)和Retroift Service 的注冊(cè)機(jī)制類似,不同的Service注冊(cè)時(shí),其對(duì)應(yīng)的元信息類(IRouteGroup)其實(shí)是不同的,因此只需要對(duì)對(duì)應(yīng)的IRouteGroup加鎖即可。 另外由于這部分代碼現(xiàn)在可能多線程同時(shí)在進(jìn)行,部分邏輯需要進(jìn)行二次判斷,

在completion的后半部分流程中,針對(duì)Provider實(shí)例生產(chǎn)的流程也需要進(jìn)行單獨(dú)加鎖,避免多次調(diào)用init函數(shù)。

收益

根據(jù)線下收集的數(shù)據(jù) 配置了20+預(yù)加載的Service Method, 預(yù)期收益 10~20ms (中端機(jī)) 。

其他

后續(xù)將繼續(xù)結(jié)合自身業(yè)務(wù)現(xiàn)狀以及其他一線大廠分享的樣例,在 x2c、class verify、禁用JIT、 disableDex2AOT等方面繼續(xù)嘗試優(yōu)化。

以上就是Android冷啟動(dòng)優(yōu)化的3個(gè)小案例分享的詳細(xì)內(nèi)容,更多關(guān)于Android冷啟動(dòng)優(yōu)化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論