本文重點還是關注原理,冷啟動優化這個問題能延伸到很多原理層面的知識點,本文比較有意思的地方是通過反編譯今日頭條App,研究大廠的啟動優化方案。
講啟動優化之前,先看下應用的啟動流程
一、應用啟動流程
應用程序不存在的情況下,從點選桌面應用圖示,到應用啟動(冷啟動),大概會經曆以下流程:
- Launcher startActivity
- AMS startActivity
- Zygote fork 程序
-
ActivityThread main()
4.1. ActivityThread attach
4.2. handleBindApplication
4.3 attachBaseContext
4.4. installContentProviders
4.5. Application onCreate
- ActivityThread 進入loop循環
- Activity生命周期回調,onCreate、onStart、onResume…
整個啟動流程我們能幹預的主要是 4.3、4.5 和6,應用啟動優化主要從這三個地方入手。理想狀況下,這三個地方如果不做任何耗時操作,那麼應用啟動速度就是最快的,但是現實很骨感,很多開源庫接入第一步一般都是在Application onCreate方法初始化,有的甚至直接内置ContentProvider,直接在ContentProvider中初始化架構,不給你優化的機會。
二、啟動優化
直奔主題,常見的啟動優化方式大概有這些:
- 閃屏頁優化
- MultipDex優化(本文重點)
- 第三方庫懶加載
- WebView優化
- 線程優化
- 系統調用優化
2.1 閃屏頁優化
消除啟動時的白屏/黑屏,市面上大部分App都采用了這種方法,非常簡單,是一個障眼法,不會縮短實際冷啟動時間,簡單貼下實作方式吧。
<application
android:name=".MainApplication"
...
android:theme="@style/AppThemeWelcome>
styles.xml 增加一個主題叫AppThemeWelcome
<style name="AppThemeWelcome" parent="Theme.AppCompat.NoActionBar">
...
<item name="android:windowBackground">@drawable/logo</item> <!-- 預設背景-->
</style>
閃屏頁設定這個主題,或者全局給Application設定
<activity android:name=".ui.activity.DemoSplashActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:theme="@style/AppThemeWelcome"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
這樣的話啟動Activity之後背景會一直在,是以在Activity的onCreate方法中切換成正常主題
protected void onCreate(@Nullable Bundle savedInstanceState) {
setTheme(R.style.AppTheme); //切換正常主題
super.onCreate(savedInstanceState);
這樣打開桌面圖示會馬上顯示logo,不會出現黑/白屏,直到Activity啟動完成,替換主題,logo消失,但是總的啟動時間并沒有改變。
2.2 MultiDex 優化(本文重點)
說
MultiDex
之前,先梳理下apk編譯流程
2.2.1 apk編譯流程
Android Studio 按下編譯按鈕後發生了什麼?
- 打包資源檔案,生成R.java檔案(使用工具AAPT)
- 處理AIDL檔案,生成java代碼(沒有AIDL則忽略)
- 編譯 java 檔案,生成對應.class檔案(java compiler)
- .class 檔案轉換成dex檔案(dex)
- 打包成沒有簽名的apk(使用工具apkbuilder)
- 使用簽名工具給apk簽名(使用工具Jarsigner)
- 對簽名後的.apk檔案進行對齊處理,不進行對齊處理不能釋出到Google Market(使用工具zipalign)
在第4步,将class檔案轉換成dex檔案,預設隻會生成一個dex檔案,單個dex檔案中的方法數不能超過65536,不然編譯會報錯:
Unable to execute dex: method ID not in [0, 0xffff]: 65536
App內建一堆庫之後,方法數一般都是超過65536的,解決辦法就是:一個dex裝不下,用多個dex來裝,gradle增加一行配置即可。
multiDexEnabled true
這樣解決了編譯問題,在5.0以上手機運作正常,但是5.0以下手機運作直接crash,報錯 Class NotFound xxx。
Android 5.0以下,ClassLoader加載類的時候隻會從class.dex(主dex)裡加載,ClassLoader不認識其它的class2.dex、class3.dex、…,當通路到不在主dex中的類的時候,就會報錯:Class NotFound xxx,是以谷歌給出相容方案,
MultiDex
。
2.2.2 MultiDex 原來這麼耗時
在Android 4.4的機器列印
MultiDex.install(context)
耗時如下:
MultiDex.install 耗時:1320
平均耗時1秒以上,目前大部分應用應該還是會相容5.0以下手機,那麼MultiDex優化是冷啟動優化的大頭。
為什麼
MultiDex
會這麼耗時?老規矩,分析一下MultiDex原理~
2.2.3 MultiDex 原理
下面看下
MultiDex
的install 方法做了什麼事
public static void install(Context context) {
Log.i("MultiDex", "Installing application");
if (IS_VM_MULTIDEX_CAPABLE) { //5.0 以上VM基本支援多dex,啥事都不用幹
Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
} else if (VERSION.SDK_INT < 4) { //
throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
} else {
...
doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "", true);
...
Log.i("MultiDex", "install done");
}
}
從入口的判斷來看,如果虛拟機本身就支援加載多個dex檔案,那就啥都不用做;如果是不支援加載多個dex(5.0以下是不支援的),則走到
doInstallation
方法。
private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix, boolean reinstallOnPatchRecoverableException) throws IOException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException, InstantiationException {
...
//擷取非主dex檔案
File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
IOException closeException = null;
try {
// 1. 這個load方法,第一次沒有緩存,會非常耗時
List files = extractor.load(mainContext, prefsKeyPrefix, false);
try {
//2. 安裝dex
installSecondaryDexes(loader, dexDir, files);
}
...
}
}
}
}
先看注釋1,
MultiDexExtractor#load
List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException {
if (!this.cacheLock.isValid()) {
throw new IllegalStateException("MultiDexExtractor was closed");
} else {
List files;
if (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) {
try {
//讀緩存的dex
files = this.loadExistingExtractions(context, prefsKeyPrefix);
} catch (IOException var6) {
Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var6);
//讀取緩存的dex失敗,可能是損壞了,那就重新去解壓apk讀取,跟else代碼塊一樣
files = this.performExtractions();
//儲存标志位到sp,下次進來就走if了,不走else
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
}
} else {
//沒有緩存,解壓apk讀取
files = this.performExtractions();
//儲存dex資訊到sp,下次進來就走if了,不走else
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
}
Log.i("MultiDex", "load found " + files.size() + " secondary dex files");
return files;
}
}
查找dex檔案,有兩個邏輯,有緩存就調用
loadExistingExtractions
方法,沒有緩存或者緩存讀取失敗就調用
performExtractions
方法,然後再緩存起來。使用到緩存,那麼
performExtractions
方法想必應該是很耗時的,分析一下代碼:
private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException {
//先确定命名格式
String extractedFilePrefix = this.sourceApk.getName() + ".classes";
this.clearDexDir();
List<MultiDexExtractor.ExtractedDex> files = new ArrayList();
ZipFile apk = new ZipFile(this.sourceApk); // apk轉為zip格式
try {
int secondaryNumber = 2;
//apk已經是改為zip格式了,解壓周遊zip檔案,裡面是dex檔案,
//名字有規律,如classes1.dex,class2.dex
for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
//檔案名:xxx.classes1.zip
String fileName = extractedFilePrefix + secondaryNumber + ".zip";
//建立這個classes1.zip檔案
MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);
//classes1.zip檔案添加到list
files.add(extractedFile);
Log.i("MultiDex", "Extraction is needed for file " + extractedFile);
int numAttempts = 0;
boolean isExtractionSuccessful = false;
while(numAttempts < 3 && !isExtractionSuccessful) {
++numAttempts;
//這個方法是将classes1.dex檔案寫到壓縮檔案classes1.zip裡去,最多重試三次
extract(apk, dexFile, extractedFile, extractedFilePrefix);
...
}
//傳回dex的壓縮檔案清單
return files;
}
這裡的邏輯就是解壓apk,周遊出裡面的dex檔案,例如class1.dex,class2.dex,然後又壓縮成class1.zip,class2.zip…,然後傳回zip檔案清單。
思考為什麼這裡要壓縮呢? 後面涉及到ClassLoader加載類原理的時候會分析ClassLoader支援的檔案格式。
第一次加載才會執行解壓和壓縮過程,第二次進來讀取sp中儲存的dex資訊,直接傳回file list,是以第一次啟動的時候比較耗時。
dex檔案清單找到了,回到上面
MultiDex#doInstallation
方法的注釋2,找到的dex檔案清單,然後調用
installSecondaryDexes
方法進行安裝,怎麼安裝呢?方法點進去看SDK 19 以上的實作
private static final class V19 {
private V19() {
}
static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
Field pathListField = MultiDex.findField(loader, "pathList");//1 反射ClassLoader 的 pathList 字段
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList();
// 2 擴充數組
MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
...
}
private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);
return (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions));
}
}
- 反射ClassLoader 的 pathList 字段
- 找到pathList 字段對應的類的
方法makeDexElements
- 通過
這個方法擴充MultiDex.expandFieldArray
數組,怎麼擴充?看下代碼:dexElements
private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field jlrField = findField(instance, fieldName);
Object[] original = (Object[])((Object[])jlrField.get(instance)); //取出原來的dexElements 數組
Object[] combined = (Object[])((Object[])Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length)); //新的數組
System.arraycopy(original, 0, combined, 0, original.length); //原來數組内容拷貝到新的數組
System.arraycopy(extraElements, 0, combined, original.length, extraElements.length); //dex2、dex3...拷貝到新的數組
jlrField.set(instance, combined); //将dexElements 重新指派為新的數組
}
就是建立一個新的數組,把原來數組内容(主dex)和要增加的内容(dex2、dex3…)拷貝進去,反射替換原來的
dexElements
為新的數組,如下圖

image
看起來有點眼熟, Tinker熱修複 的原理也是通過反射将修複後的dex添加到這個dex數組去,不同的是熱修複是添加到數組最前面,而MultiDex是添加到數組後面。這樣講可能還不是很好了解?來看看ClassLoader怎麼加載一個類的就明白了~
2.2.4 ClassLoader 加載類原理
不管是
PathClassLoader
還是
DexClassLoader
,都繼承自
BaseDexClassLoader
,加載類的代碼在
BaseDexClassLoader
中
4.4 源碼
/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
4.4 BaseDexClassLoader
- 構造方法通過傳入dex路徑,建立了
。DexPathList
- ClassLoader的findClass方法最終是調用DexPathList 的findClass方法
接着看
DexPathList
源碼
/dalvik/src/main/java/dalvik/system/DexPathList.java
DexPathList-dexElements
DexPathList
裡面定義了一個
dexElements
數組,
findClass
方法中用到,看下
DexPathList#findClass
findClass方法邏輯很簡單,就是 周遊dexElements 數組,拿到裡面的DexFile對象,通過DexFile的loadClassBinaryName方法加載一個類。
DexFile#loadClassBinaryName
最終建立Class是通過native方法,就不追下去了,大家有興趣可以看下native層是怎麼建立Class對象的。DexFile.cpp
那麼問題來了,5.0以下這個dexElements 裡面隻有主dex(可以認為是一個bug),沒有dex2、dex3…,MultiDex是怎麼把dex2添加進去呢?
答案就是反射
DexPathList
的
dexElements
字段,然後把我們的dex2添加進去,當然,dexElements裡面放的是Element對象,我們隻有dex2的路徑,必須轉換成Element格式才行,是以 反射DexPathList裡面的makeDexElements 方法 ,将dex檔案轉換成Element對象即可。
image
dex2、dex3…通過
makeDexElements
方法轉換成要新增的Element數組, 最後一步就是反射DexPathList的dexElements字段,将原來的Element數組和新增的Element數組合并,然後反射指派給dexElements變量,最後DexPathList的dexElements變量就包含我們新加的dex在裡面了。
makeDexElements
方法會判斷file類型,上面講dex提取的時候解壓apk得到dex,然後又将dex壓縮成zip,壓縮成zip,就會走到第二個判斷裡去。仔細想想,其實dex不壓縮成zip,走第一個判斷也沒啥問題吧,那谷歌的MultiDex為什麼要将dex壓縮成zip呢?在Android開發高手課中看到張紹文也提到這一點
image
然後我在反編譯頭條App的時候,發現頭條參考谷歌的MultiDex,自己寫了一套,猜想可能是優化這個多餘的壓縮過程,頭條的方案下面會介紹。
2.2.5 原理小結
ClassLoader 加載類原理:
ClassLoader.loadClass -> DexPathList.loadClass -> 周遊dexElements數組 ->DexFile.loadClassBinaryName
通俗點說就是:ClassLoader加載類的時候是通過周遊dex數組,從dex檔案裡面去加載一個類,加載成功就傳回,加載失敗則抛出Class Not Found 異常。
MultiDex原理:
在明白ClassLoader加載類原理之後,我們可以通過反射dexElements數組,将新增的dex添加到數組後面,這樣就保證ClassLoader加載類的時候可以從新增的dex中加載到目标類,經過分析後最終MultipDex原理圖如下:MultiDex原理![]()
今日頭條啟動優化
2.2.6 MultiDex 優化(兩種方案)
知道了MultiDex原理之後,可以了解install過程為什麼耗時,因為涉及到解壓apk取出dex、壓縮dex、将dex檔案通過反射轉換成DexFile對象、反射替換數組。
那麼MultiDex到底應該怎麼優化呢,放子線程可行嗎?
方案1:子線程install(不推薦)
這個方法大家很容易就能想到,在閃屏頁開一個子線程去執行
MultiDex.install
,然後加載完才跳轉到首頁。需要注意的是閃屏頁的Activity,包括閃屏頁中引用到的其它類必須在主dex中,不然在
MultiDex.install
之前加載這些不在主dex中的類會報錯Class Not Found。這個可以通過gradle配置,如下:
defaultConfig {
//分包,指定某個類在main dex
multiDexEnabled true
multiDexKeepProguard file('multiDexKeep.pro') // 打包到main dex的這些類的混淆規制,沒特殊需求就給個空檔案
multiDexKeepFile file('maindexlist.txt') // 指定哪些類要放到main dex
}
maindexlist.txt 檔案指定哪些類要打包到主dex中,内容格式如下
com/lanshifu/launchtest/SplashActivity.class
在已有項目中用這種方式,一頓操作猛如虎之後,編譯運作在4.4的機器上,啟動閃屏頁,加載完準備進入首頁直接崩掉了。
image
報錯
NoClassDefFoundError
,一般都是該類沒有在主dex中,要在maindexlist.txt 将配置指定在主dex。
第三方庫中的ContentProvider必須指定在主dex中,否則也會找不到,為什麼? 文章開頭說過應用的啟動流程, ContentProvider 初始化時機 如下圖:
image
ContentProvider初始化太早了,如果不在主dex中,還沒啟動閃屏頁就已經crash了。
是以這種方案的缺點很明顯:
- MultiDex加載邏輯放在閃屏頁的話,閃屏頁中引用到的類都要配置在主dex。
- ContentProvider必須在主dex,一些第三方庫自帶ContentProvider,維護比較麻煩,要一個一個配置。
這時候就思考一下,有沒有其它更好的方案呢?大廠是怎麼做的?今日頭條肯定要對MultiDex進行優化吧,反編譯瞧瞧?
image
點了一根煙之後,開始偷代碼…
MultiDex優化方案2:今日頭條方案
今日頭條沒有加強,反編譯後很容易通過關鍵字搜尋找到
MultidexApplication
這個類,
image
看注釋1的
d.a(this);
這個方法,代碼雖然混淆了,但是方法内部的代碼還是可以看出是幹嘛的,繼續跟這個方法,為了不影響閱讀,我對混淆做了一些處理,改成正常的方法名。
image
每個方法開頭都有
PatchProxy.isSupport
這個if判斷,這個是美團Robust熱修複生成的代碼,今日頭條沒有自己的熱修複架構,沒有用Tinker,而是用美團的,想了解關于Robust細節可以參考文末連結。Robust直接跳過,看else代碼塊即可。
繼續看
loadMultiDex
方法
image
邏輯如下:
1. 建立臨時檔案,作為判斷MultiDex是否加載完的條件
2. 啟動LoadDexActivity去加載MultiDex(LoadDexActivity在單獨程序),加載完會删除臨時檔案
3. 開啟while循環,直到臨時檔案不存在才跳出循環,進入Application的onCreate
建立臨時檔案代碼
image
while循環代碼
image
LoadDexActivity 隻有一個加載框,加載完再跳轉到閃屏頁
image
dex加載完應該要finish掉目前Activity
image
按照上面代碼分析,今日頭條在5.0以下手機首次啟動應該是這樣:
- 打開桌面圖示
- 顯示預設背景
- 跳轉到加載dex的界面,展示一個loading的加載框幾秒鐘
- 跳轉到閃屏頁
實際上是不是這樣呢,用4.4機器試下?
今日頭條啟動
看起來完全跟猜想的一緻,撸幾行代碼驗證應該不難吧?
image
點了一根煙之後,開始撸代碼,最終實作效果如下
仿頭條啟動.gif
效果跟今日頭條是一緻的,不再重複分析代碼了,源碼上傳到github,感興趣的同學可以參考參考,頭條的方案,值得嘗試~
https://github.com/lanshifu/MultiDexTest/
再次梳理一下這種方式:
- 在主程序Application 的 attachBaseContext 方法中判斷如果需要使用MultiDex,則建立一個臨時檔案,然後開一個程序(LoadDexActivity),顯示Loading,異步執行MultiDex.install 邏輯,執行完就删除臨時檔案并finish自己。
- 主程序Application 的 attachBaseContext 進入while代碼塊,定時輪循臨時檔案是否被删除,如果被删除,說明MultiDex已經執行完,則跳出循環,繼續正常的應用啟動流程。
- 注意LoadDexActivity 必須要配置在main dex中。
有些同學可能會問,啟動還是很久啊,冷啟動時間有變化嗎?
冷啟動時間是指點選桌面圖示到第一個Activity顯示這段時間。
MultiDex優化總結
方案1:直接在閃屏頁開個子線程去執行MultiDex邏輯,MultiDex不影響冷啟動速度,但是難維護。
方案2:今日頭條的MultiDex優化方案:
- 在Application 的attachBaseContext 方法裡,啟動另一個程序的LoadDexActivity去異步執行MultiDex邏輯,顯示Loading。
- 然後主程序Application進入while循環,不斷檢測MultiDex操作是否完成
- MultiDex執行完之後主程序Application繼續走,ContentProvider初始化和Application onCreate方法,也就是執行主程序正常的邏輯。
其實應該還有方案3,因為我發現頭條并沒有直接使用Google的MultiDex,而是參考谷歌的MultiDex,自己寫了一套,耗時應該會少一些,大家有興趣可以去研究一下。
2.3 預建立Activity
image
這段代碼是今日頭條裡面的,Activity對象預先new出來,
對象第一次建立的時候,java虛拟機首先檢查類對應的Class
對象是否已經加載。如果沒有加載,jvm會根據類名查找.class檔案,将其Class對象載入。同一個類第二次new的時候就不需要加載類對象,而是直接執行個體化,建立時間就縮短了。
頭條真是把啟動優化做到極緻。
2.4 第三方庫懶加載
很多第三方開源庫都說在Application中進行初始化,十幾個開源庫都放在Application中,肯定對冷啟動會有影響,是以可以考慮按需初始化,例如Glide,可以放在自己封裝的圖檔加載類中,調用到再初始化,其它庫也是同理,讓Application變得更輕。
2.5 WebView啟動優化。
WebView啟動優化文章也比較多,這裡隻說一下大概優化思路。
- WebView第一次建立比較耗時,可以預先建立WebView,提前将其核心初始化。
- 使用WebView緩存池,用到WebView的地方都從緩存池取,緩存池中沒有緩存再建立,注意記憶體洩漏問題。
- 本地預置html和css,WebView建立的時候先預加載本地html,之後通過js腳本填充内容部分。
這一部分可以參考:
https://mp.weixin.qq.com/s/KwvWURD5WKgLKCetwsH0EQ,
2.6 資料預加載
這種方式一般是在首頁空閑的時候,将其它頁面的資料加載好,儲存到記憶體或資料庫,等到打開該頁面的時候,判斷已經預加載過,直接從記憶體或資料庫讀取資料并顯示。
2.7 線程優化
線程是程式運作的基本機關,線程的頻繁建立是耗性能的,是以大家應該都會用線程池。單個cpu情況下,即使是開多個線程,同時也隻有一個線程可以工作,是以線程池的大小要根據cpu個數來确定。
啟動優化方式就先介紹到這裡,常見的就是這些,其它的可以作為補充。
三、啟動耗時分析方法
TraceView
性能損耗太大,得到的結果不真實。
Systrace
可以友善追蹤關鍵系統調用的耗時情況,如 Choreographer,但是不支援應用程式代碼的耗時分析。
3.1 Systrace + 函數插樁
結合
Systrace
和
函數插樁
,就是将如下代碼插入到每個方法的入口和出口
class Trace{
public static void i(String tag){
android.os.Trace.beginSection(tag);
}
public static void o(){
android.os.Trace.endSection();
}
}
插樁後的代碼如下
void test(){
Trace.i("test");
System.out.println("doSomething");
Trace.o();
}
插樁工具參考:
https://github.com/AndroidAdvanceWithGeektime/Chapter07
mac下systrace路徑在
/Users/{xxx}/Library/Android/sdk/platform-tools/systrace/
編譯運作app,執行指令
python2 /Users/lanshifu/Library/Android/sdk/platform-tools/systrace/systrace.py gfx view wm am pm ss dalvik app sched -b 90960 -a com.sample.systrace -o test.log.html
image
最後按下Enter停止捕獲trace資訊,在目錄下生成報告test.log.html,直接可以用谷歌浏覽器打開檢視。
3.2 BlockCanary 也可以檢測
BlockCanary 可以監聽主線程耗時的方法,将門檻值設定低一點,比如200毫秒,這樣的話如果一個方法執行時間超過200毫秒,擷取堆棧資訊并通知開發者。
BlockCanary 原理在之前那篇卡頓優化的文章裡面講過一些,這裡就不再重複。
總結
文章有點長,看到這裡,是不是忘記開頭講什麼了?總結一下這篇文章主要涉及到哪些内容:
- 應用啟動流程
- 閃屏頁優化
- MultiDex 原理分析
- ClassLoader 加載一個類的流程分析
- 熱修複原理
-
MultiDex優化:
介紹了兩種方式,一種是直接在閃屏頁開個子線程去加載dex,難維護,不推薦;一種是今日頭條的方案,在單獨一個程序加載dex,加載完主程序再繼續。
- 快速啟動Activity的方式:預建立Activity,預加載資料。
- 啟動時間監控的方式:Systrace+插樁、BlockCanary。
作者:藍師傅_Android
連結:https://www.jianshu.com/p/d0fe74f4e9c4
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。