https://juejin.im/post/5d95f4a4f265da5b8f10714b
X2C框架
爲了即保留xml的優點,又解決它帶來的性能問題,我們開發了X2C方案。即在編譯生成APK期間,將需要翻譯的layout翻譯生成對應的java文件,這樣對於開發人員來說寫佈局還是寫原來的xml,但對於程序來說,運行時加載的是對應的java文件。 我們採用APT(Annotation Processor Tool)+ JavaPoet技術來完成編譯期間【註解】->【解註解】->【翻譯xml】->【生成java】整個流程的操作。
https://github.com/iReaderAndroid/X2C
0
前言
網上關於啓動優化的文章多不勝數,內容千篇一律,大都是列舉一些耗時操作,採用異步加載、懶加載等。
而在面試過程中,關於啓動優化的問題,如果只是很表面地回答耗時操作應該放在子線程,顯然太過於普通,無法跟競爭者拉開差距。如何讓面試官知道你的“內功深厚”,那肯定是要往原理層面去回答。
本文重點還是關注原理,冷啓動優化這個問題能延伸到很多原理層面的知識點,本文比較有意思的地方是通過反編譯今日頭條App,研究大廠的啓動優化方案。
講啓動優化之前,先看下應用的啓動流程.
1
應用啓動流程
應用進程不存在的情況下,從點擊桌面應用圖標,到應用啓動(冷啓動),大概會經歷以下流程:
-
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中初始化框架,不給你優化的機會。
2
啓動優化
直奔主題,常見的啓動優化方式大概有這些:
-
閃屏頁優化
-
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));
}
}
1. 反射ClassLoader 的 pathList 字段
2. 找到pathList 字段對應的類的makeDexElements 方法
3. 通過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爲新的數組,如下圖
看起來有點眼熟,Tinker熱修復的原理也是通過反射將修復後的dex添加到這個dex數組去,不同的是熱修復是添加到數組最前面,而MultiDex是添加到數組後面。這樣講可能還不是很好理解?來看看ClassLoader怎麼加載一個類的就明白了~
2.2.4 ClassLoader 加載類原理
不管是 PathClassLoader還是DexClassLoader,都繼承自BaseDexClassLoader,加載類的代碼在 BaseDexClassLoader中
4.4 源碼
/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
1.構造方法通過傳入dex路徑,創建了DexPathList。
2. ClassLoader的findClass方法最終是調用DexPathList 的findClass方法
接着看DexPathList源碼/dalvik/src/main/java/dalvik/system/DexPathList.java
DexPathList裏面定義了一個dexElements 數組,findClass方法中用到,看下
findClass方法邏輯很簡單,就是遍歷dexElements 數組,拿到裏面的DexFile對象,通過DexFile的loadClassBinaryName方法加載一個類。
最終創建Class是通過native方法,就不追下去了,大家有興趣可以看下native層是怎麼創建Class對象的。
那麼問題來了,5.0以下這個dexElements 裏面只有主dex(可以認爲是一個bug),沒有dex2、dex3...,MultiDex是怎麼把dex2添加進去呢?
答案就是反射DexPathList的dexElements字段,然後把我們的dex2添加進去,當然,dexElements裏面放的是Element對象,我們只有dex2的路徑,必須轉換成Element格式纔行,所以反射DexPathList裏面的makeDexElements 方法,將dex文件轉換成Element對象即可。
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開發高手課中看到張紹文也提到這一點
然後我在反編譯頭條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原理圖如下:
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的機器上,啓動閃屏頁,加載完準備進入主頁直接崩掉了。
報錯NoClassDefFoundError,一般都是該類沒有在主dex中,要在maindexlist.txt 將配置指定在主dex。**第三方庫中的ContentProvider必須指定在主dex中,否則也會找不到,爲什麼?**文章開頭說過應用的啓動流程,ContentProvider 初始化時機如下圖:
ContentProvider初始化太早了,如果不在主dex中,還沒啓動閃屏頁就已經crash了。
所以這種方案的缺點很明顯:
MultiDex加載邏輯放在閃屏頁的話,閃屏頁中引用到的類都要配置在主dex。
ContentProvider必須在主dex,一些第三方庫自帶ContentProvider,維護比較麻煩,要一個一個配置。
這時候就思考一下,有沒有其它更好的方案呢?大廠是怎麼做的?今日頭條肯定要對MultiDex進行優化吧,反編譯瞧瞧?
開始偷代碼...
MultiDex優化方案2:今日頭條方案
今日頭條沒有加固,反編譯後很容易通過關鍵字搜索找到MultidexApplication這個類,
看註釋1的d.a(this);這個方法,代碼雖然混淆了,但是方法內部的代碼還是可以看出是幹嘛的,繼續跟這個方法,爲了不影響閱讀,我對混淆做了一些處理,改成正常的方法名。
每個方法開頭都有PatchProxy.isSupport這個if判斷,這個是美團Robust熱修復生成的代碼,今日頭條沒有自己的熱修復框架,沒有用Tinker,而是用美團的,想了解關於Robust細節可以參考文末鏈接。Robust直接跳過,看else代碼塊即可。
繼續看loadMultiDex方法
邏輯如下:
1. 創建臨時文件,作爲判斷MultiDex是否加載完的條件
2. 啓動LoadDexActivity去加載MultiDex(LoadDexActivity在單獨進程),加載完會刪除臨時文件
3. 開啓while循環,直到臨時文件不存在才跳出循環,進入Application的onCreate
創建臨時文件代碼
while循環代碼
LoadDexActivity 只有一個加載框,加載完再跳轉到閃屏頁
dex加載完應該要finish掉當前Activity
按照上面代碼分析,今日頭條在5.0以下手機首次啓動應該是這樣:
-
打開桌面圖標
-
顯示默認背景
-
跳轉到加載dex的界面,展示一個loading的加載框幾秒鐘
-
跳轉到閃屏頁
實際上是不是這樣呢,用4.4機器試下?
看起來完全跟猜想的一致,擼幾行代碼驗證應該不難吧?
開始擼代碼,最終實現效果如下
效果跟今日頭條是一致的,不再重複分析代碼了,源碼上傳到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
這段代碼是今日頭條裏面的,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腳本填充內容部分。
2.6 數據預加載
這種方式一般是在主頁空閒的時候,將其它頁面的數據加載好,保存到內存或數據庫,等到打開該頁面的時候,判斷已經預加載過,直接從內存或數據庫讀取數據並顯示。
2.7 線程優化
線程是程序運行的基本單位,線程的頻繁創建是耗性能的,所以大家應該都會用線程池。單個cpu情況下,即使是開多個線程,同時也只有一個線程可以工作,所以線程池的大小要根據cpu個數來確定。
啓動優化方式就先介紹到這裏,常見的就是這些,其它的可以作爲補充。
3
啓動耗時分析方法
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
最後按下Enter停止捕獲trace信息,在目錄下生成報告test.log.html,直接可以用谷歌瀏覽器打開查看。
3.2 BlockCanary 也可以檢測
BlockCanary 可以監聽主線程耗時的方法,將閾值設置低一點,比如200毫秒,這樣的話如果一個方法執行時間超過200毫秒,獲取堆棧信息並通知開發者。
BlockCanary 原理在之前那篇卡頓優化的文章裏面講過一些,這裏就不再重複。
4
總結
文章有點長,看到這裏,是不是忘記開頭講什麼了?總結一下這篇文章主要涉及到哪些內容:
-
應用啓動流程
-
閃屏頁優化
-
MultiDex 原理分析
-
ClassLoader 加載一個類的流程分析
-
熱修復原理
-
MultiDex優化:介紹了兩種方式,一種是直接在閃屏頁開個子線程去加載dex,難維護,不推薦;一種是今日頭條的方案,在單獨一個進程加載dex,加載完主進程再繼續。
-
快速啓動Activity的方式:預創建Activity,預加載數據。
-
啓動時間監控的方式:Systrace+插樁、BlockCanary。
面試問到啓動優化問題,不要簡單一兩句話回答,可以說說自己在實際項目中做了哪些優化,比如Multidex優化,把整個流程,原理說清楚。當然,前提是自己要去實踐,理解爲什麼要這樣做。
就這樣,有問題請留言,更多文章,敬請期待。