Android冷啟動優(yōu)化的3個小案例分享
背景
為了提高App的冷啟動耗時,除了在常規(guī)的業(yè)務(wù)側(cè)進(jìn)行耗時代碼優(yōu)化之外,為了進(jìn)一步縮短啟動耗時,需要在純技術(shù)測做一些優(yōu)化探索,本期我們從類預(yù)加載、Retrofit 、ARouter方面進(jìn)行了進(jìn)一步的優(yōu)化。從測試數(shù)據(jù)上來看,這些優(yōu)化手段的收益有限,可能在中端機上加起來也不超過50ms的收益,但為了冷啟動場景的極致優(yōu)化,給用戶帶來更好的體驗,任何有收益的優(yōu)化手段都是值得嘗試的。
類預(yù)加載
一個類的完整加載流程至少包括 加載、鏈接、初始化,而類的加載在一個進(jìn)程中只會觸發(fā)一次,因此對于冷啟動場景,我們可以異步加載原本在啟動階段會在主線程觸發(fā)類加載過程的類,這樣當(dāng)原流程在主線程訪問到該類時就不會觸發(fā)類加載流程。
Hook ClassLoader 實現(xiàn)
在Android系統(tǒng)中,類的加載都是通過PathClassLoader 實現(xiàn)的,基于類加載的父類委托機制,我們可以通過Hook PathClassLoader 修改其默認(rèn)的parent 來實現(xiàn)。
首先我們創(chuàng)建一個MonitorClassLoader 繼承自PathClassLoader,并在其內(nèi)部記錄類加載耗時
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 對應(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實例
這樣,我們就獲取啟動階段的加載類了
基于JVMTI 實現(xiàn)
除了通過 Hook ClassLoader的方案實現(xiàn),我們也可以通過JVMTI 來實現(xiàn)類加載監(jiān)控。
通過注冊ClassPrepare Callback, 可以在每個類Prepare階段觸發(fā)回調(diào)。
當(dāng)然這種方案,相比 Hook ClassLoader 還是要繁瑣很多,不過基于JVMTI 還可以做很多其他更強大的事。
類預(yù)加載實現(xiàn)
目前應(yīng)用通常都是多模塊的,因此我們可以設(shè)計一個抽象接口,不同的業(yè)務(wù)模塊可以繼承該抽象接口,定義不同業(yè)務(wù)模塊需要進(jìn)行預(yù)加載的類。
/** * 資源預(yù)加載接口 */ public interface PreloadDemander { /** * 配置所有需要預(yù)加載的類 * @return */ Class[] getPreloadClasses(); }
之后在啟動階段收集所有的 Demander實例,并觸發(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) } } } }
收益
第一個版本配置了大概90個類,在終端機型測試數(shù)據(jù)顯示 這些類的加載需要消耗30ms左右的cpu時間,不同類加載的消耗時間差異主要來自于類的復(fù)雜度 比如繼承體系、字段屬性數(shù)量等, 以及類初始化階段的耗時,比如靜態(tài)成員變量的立即初始化、靜態(tài)代碼塊的執(zhí)行等。
方案優(yōu)化思考
我們目前的方案 配置的具體類列表來源于手動配置,這種方案的弊端在于,類的列表需要開發(fā)維護(hù),在版本快速迭代變更的情況下 維護(hù)成本較大, 并且對于一些大型App,存在著非常多的AB實驗條件,這也可能導(dǎo)致不同的用戶在類加載上是會有區(qū)別的。
在前面的小節(jié)中,我們介紹了使用自定義的 ClassLoader可以手動收集 啟動階段主線程的類列表,那么 我們是否可以在端上 每次啟動時 自動收集加載的類,如果發(fā)現(xiàn)這個類不在現(xiàn)有 的名單中 則加入到名單,在下次啟動時進(jìn)行預(yù)加載。 當(dāng)然 具體的策略還需要做詳細(xì)設(shè)計,比如 控制預(yù)加載名單的列表大小, 被加入預(yù)加載名單的類最低耗時閾值, 淘汰策略等等。
Retrofit ServiceMethod 預(yù)解析注入
背景
Retrofit 是目前最常用的網(wǎng)絡(luò)庫框架,其基于注解配置的網(wǎng)絡(luò)請求方式及Adapter的設(shè)計模式大大簡化了網(wǎng)絡(luò)請求的調(diào)用方式。 不過其并沒有采用類似APT的方式在編譯時生成請求代碼,而是采用運行時解析的方式。
當(dāng)我們調(diào)用Retrofit.create(final Class service) 函數(shù)時,會生成一個該抽象接口的動態(tài)代理實例。
接口的所有函數(shù)調(diào)用都會被轉(zhuǎn)發(fā)到該動態(tài)代理對象的invoke函數(shù),最終調(diào)用loadServiceMethod(method).invoke 調(diào)用。
在loadServiceMethod函數(shù)中,需要解析原函數(shù)上的各種元信息,包括函數(shù)注解、參數(shù)注解、參數(shù)類型、返回值類型等信息,并最終生成ServiceMethod 實例,對原接口函數(shù)的調(diào)用其實最終觸發(fā)的是 這個生成的ServiceMethod invoke函數(shù)的調(diào)用。
從源碼實現(xiàn)上可以看出,對ServiceMethod的實例做了緩存處理,每個Method 對應(yīng)一個ServiceMethod。
耗時測試
這里我模擬了一個簡單的 Service Method, 并調(diào)用archiveStat 觀察首次調(diào)用及其后續(xù)調(diào)用的耗時,注意這里的調(diào)用還未觸發(fā)網(wǎng)絡(luò)請求,其返回的是一個Call對象。
從測試結(jié)果上看,首次調(diào)用需要觸發(fā)需要消耗1.7ms,而后續(xù)的調(diào)用 只需要消耗50微妙左右。
優(yōu)化方案
由于首次調(diào)用接口函數(shù)需要觸發(fā)ServiceMethod實例的生成,這個過程比較耗時,因此優(yōu)化思路也比較簡單,收集啟動階段會調(diào)用的 函數(shù),提前生成ServiceMethod實例并寫入到緩存中。
serviceMethodCache 的類型本身是ConcurrentHashMap,所以它是并發(fā)安全的。
但是源碼中 進(jìn)行ServiceMethod緩存判斷的時候 還是以 serviceMethodCache為Lock Object 進(jìn)行了加鎖,這導(dǎo)致 多線程觸發(fā)同時首次觸發(fā)不同Method的調(diào)用時,存在鎖等待問題
這里首先需要理解為什么這里需要加鎖,其目的也是因為parseAnnotations 是一個好事操作,這里是為了實現(xiàn)類似 putIfAbsent的完全原子性操作。 但實際上這里加鎖可以以 對應(yīng)的Method類型為鎖對象,因為本身不同Method 對應(yīng)的ServiceMethod實例就是不同的。 我們可以修改其源碼的實現(xiàn)來避免這種場景的鎖競爭問題。
當(dāng)然針對我們的優(yōu)化場景,其實不修改源碼也是可以實現(xiàn)的,因為 ServiceMethod.parseAnnotations 是無鎖的,畢竟它是一個純函數(shù)。 因此我們可以在異步線程調(diào)用parseAnnotations 生成ServiceMethod 實例,之后通過反射 寫入 Retrofit實例的 serviceMethodCache 中。這樣存在的問題是 不同線程可能同時觸發(fā)了一個Method的解析注入,但 由于serviceMethodCache 本身就是線程安全的,所以 它只是多做了一次解析,對最終結(jié)果并無影響。
ServiceMethod.parseAnnotations是包級私有的,我們可以在當(dāng)前工程創(chuàng)建一個一樣的包,這樣就可以直接調(diào)用該函數(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ù),并注入到對應(yīng)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; } }
預(yù)加載名單收集
有了優(yōu)化方案后,還需要收集原本在啟動階段會在主線程進(jìn)行Retrofit ServiceMethod調(diào)用的列表, 這里采取的是字節(jié)碼插樁的方式,使用的LancetX 框架進(jìn)行修改。
目前名單的配置是預(yù)先收集好,在配置中心進(jìn)行配置,運行時根據(jù)配置中寫的配置 進(jìn)行預(yù)加載。 這里還可以提供其他的配置方案,比如 提供一個注解用于標(biāo)注該Retrofit函數(shù)需要進(jìn)行預(yù)解析,
之后,在編譯期間收集所有需要預(yù)加載的Service及函數(shù),生成對應(yīng)的名單,不過這個方案需要一定開發(fā)成本,并且需要去修改業(yè)務(wù)模塊的代碼,目前的階段還處于驗證收益階段,所以暫未實施。
收益
App收集了啟動階段20個左右的Method 進(jìn)行預(yù)加載,預(yù)計提升10~20ms。
ARouter
背景
ARouter框架提供了路由注冊跳轉(zhuǎn) 及 SPI 能力。為了優(yōu)化冷啟動速度,對于某些服務(wù)實例可以在啟動階段進(jìn)行預(yù)加載生成對應(yīng)的實例對象。
ARouter的注冊信息是在預(yù)編譯階段(基于APT) 生成的,在編譯階段又通過ASM 生成對應(yīng)映射關(guān)系的注入代碼。
而在運行時以獲取Service實例為例,當(dāng)調(diào)用navigation函數(shù)獲取實例最終會調(diào)用到 completion函數(shù)。
當(dāng)首次調(diào)用時,其對應(yīng)的RouteMeta 實例尚未生成,會繼續(xù)調(diào)用 addRouteGroupDynamic函數(shù)進(jìn)行注冊。
addRouteGroupDynamic 會創(chuàng)建對應(yīng)預(yù)編譯階段生成的服務(wù)注冊類并調(diào)用loadInto函數(shù)進(jìn)行注冊。而某些業(yè)務(wù)模塊如何服務(wù)注冊信息比較多,這里的loadInto就會比較耗時。
整體來看,對于獲取Service實例的流程, completion的整個流程 涉及到 loadInto信息注冊、Service實例反射生成、及init函數(shù)的調(diào)用。 而completion函數(shù)是synchronized的,因此無法利用多線程進(jìn)行注冊來縮短啟動耗時。
優(yōu)化方案
這里的優(yōu)化其實和Retroift Service 的注冊機制類似,不同的Service注冊時,其對應(yīng)的元信息類(IRouteGroup)其實是不同的,因此只需要對對應(yīng)的IRouteGroup加鎖即可。 另外由于這部分代碼現(xiàn)在可能多線程同時在進(jìn)行,部分邏輯需要進(jìn)行二次判斷,
在completion的后半部分流程中,針對Provider實例生產(chǎn)的流程也需要進(jìn)行單獨加鎖,避免多次調(diào)用init函數(shù)。
收益
根據(jù)線下收集的數(shù)據(jù) 配置了20+預(yù)加載的Service Method, 預(yù)期收益 10~20ms (中端機) 。
其他
后續(xù)將繼續(xù)結(jié)合自身業(yè)務(wù)現(xiàn)狀以及其他一線大廠分享的樣例,在 x2c、class verify、禁用JIT、 disableDex2AOT等方面繼續(xù)嘗試優(yōu)化。
以上就是Android冷啟動優(yōu)化的3個小案例分享的詳細(xì)內(nèi)容,更多關(guān)于Android冷啟動優(yōu)化的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android 中讀取SD卡文件時拋出NullPointerException錯誤解決辦法
這篇文章主要介紹了Android 中讀取SD卡文件時拋出NullPointerException錯誤解決辦法的相關(guān)資料,需要的朋友可以參考下2017-05-05Android5.x中的陰影效果elevation和translationZ的實現(xiàn)方法
這篇文章主要介紹了 android5.x中的陰影效果elevation和translationZ的相關(guān)資料,需要的朋友可以參考下2016-12-12以一個著色游戲展開講解Android中區(qū)域圖像填色的方法
這篇文章主要介紹了Android中實現(xiàn)區(qū)域圖像顏色填充的方法,文中以一個著色游戲為例講解了邊界的填充等各種填色操作,需要的朋友可以參考下2016-02-02Android制作登錄頁面并且記住賬號密碼功能的實現(xiàn)代碼
這篇文章主要介紹了Android制作登錄頁面并且記住賬號密碼功能的實現(xiàn)代碼,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-04-04Android實現(xiàn)網(wǎng)易新聞客戶端側(cè)滑菜單(2)
這篇文章主要為大家詳細(xì)介紹了Android實現(xiàn)網(wǎng)易新聞客戶端側(cè)滑菜單第二篇,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-11-11Flutter開發(fā)setState能否在build中直接調(diào)用詳解
這篇文章主要為大家介紹了Flutter開發(fā)setState能否在build中直接調(diào)用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10Android實現(xiàn)按鈕點擊事件的三種方法總結(jié)
Button是程序用于和用戶進(jìn)行交互的一個重要控件。既然有Button,那肯定有onClick方法,下面就教大家三種實現(xiàn)點擊事件的方法,感興趣的可以了解一下2022-04-04android中使用SharedPreferences進(jìn)行數(shù)據(jù)存儲的操作方法
本篇文章介紹了,在android中使用SharedPreferences進(jìn)行數(shù)據(jù)存儲的操作方法。需要的朋友參考下2013-04-04