應(yīng)用啟動(dòng)類型
冷啟動(dòng)
冷啟動(dòng)指的是:用戶在 Launcher 程序里點(diǎn)擊應(yīng)用圖標(biāo)時(shí),會(huì)通知ActivityManagerService 啟動(dòng)應(yīng)用的入口 Activity缤至,ActivityManagerService 發(fā)現(xiàn)這個(gè)應(yīng)用還未啟動(dòng)劝贸,則會(huì)通知 Zygote 進(jìn)程孵化出應(yīng)用進(jìn)程姨谷,然后在這個(gè) dalvik 應(yīng)用進(jìn)程里執(zhí)行 ActivityThread 的 main 方法。應(yīng)用進(jìn)程接下來(lái)通知 ActivityManagerService 應(yīng)用進(jìn)程已啟動(dòng)映九,ActivityManagerService 保存應(yīng)用進(jìn)程的一個(gè)代理對(duì)象菠秒,這樣 ActivityManagerService 可以通過(guò)這個(gè)代理對(duì)象控制應(yīng)用進(jìn)程,然后 ActivityManagerService 通知應(yīng)用進(jìn)程創(chuàng)建入口 Activity 的實(shí)例氯迂,并執(zhí)行它的生命周期方法。
- Launcher startActivity
- AMS startActivity
- Zygote fork 進(jìn)程
- ActivityThread main()
4.1. ActivityThread attach
4.2. handleBindApplication
4.3. attachBaseContext
4.4. installContentProviders
4.5. Application onCreate - ActivityThread 進(jìn)入 loop 循環(huán)
- Activity 生命周期回調(diào)言缤,onCreate嚼蚀、onStart、onResume...
整個(gè)啟動(dòng)流程我們能干預(yù)的主要是 4.3管挟、4.5 和6轿曙,應(yīng)用啟動(dòng)優(yōu)化主要從這三個(gè)地方入手。
熱啟動(dòng)
與冷啟動(dòng)相比,熱啟動(dòng)應(yīng)用程序要簡(jiǎn)單得多导帝,開(kāi)銷更低守谓。在熱啟動(dòng),系統(tǒng)會(huì)把你活動(dòng)放到前臺(tái)您单,如果所有應(yīng)用程序的活動(dòng)仍駐留在內(nèi)存中斋荞,那么應(yīng)用程序可以避免重復(fù)對(duì)象初始化,UI 的布局和渲染虐秦。
熱啟動(dòng)顯示與冷啟動(dòng)場(chǎng)景相同的屏幕行為:系統(tǒng)進(jìn)程顯示空白屏幕平酿,直到應(yīng)用程序完成呈現(xiàn)活動(dòng)。
溫啟動(dòng)
- 用戶退出您的應(yīng)用悦陋,但隨后重新啟動(dòng)蜈彼。該過(guò)程可能已繼續(xù)運(yùn)行,但應(yīng)用程序必須通過(guò)調(diào)用 onCreate()從頭開(kāi)始重新創(chuàng)建活動(dòng)俺驶。
- 系統(tǒng)從內(nèi)存中驅(qū)逐您的應(yīng)用程序幸逆,然后用戶重新啟動(dòng)它。進(jìn)程和 Activity 需要重新啟動(dòng)暮现,但任務(wù)可以從保存的實(shí)例狀態(tài)包傳遞到 onCreate()中还绘。
啟動(dòng)優(yōu)化
- 閃屏頁(yè)優(yōu)化
- MultipDex 優(yōu)化
- 第三方庫(kù)懶加載
- WebView 優(yōu)化
- 線程優(yōu)化
- 系統(tǒng)調(diào)用優(yōu)化
2.1 閃屏頁(yè)優(yōu)化
原因:
啟動(dòng)應(yīng)用程序時(shí),空白的啟動(dòng)窗口會(huì)保留在屏幕上送矩,直到系統(tǒng)首次完成繪制應(yīng)用程序?yàn)橹共仙4藭r(shí),系統(tǒng)進(jìn)程會(huì)換出您應(yīng)用程序的啟動(dòng)窗口栋荸,從而允許用戶開(kāi)始與應(yīng)用程序進(jìn)行交互菇怀。
解決方式:
消除啟動(dòng)時(shí)的白屏/黑屏,市面上大部分 App 都采用了這種方法晌块,非常簡(jiǎn)單爱沟,是一個(gè)障眼法,不會(huì)縮短實(shí)際冷啟動(dòng)時(shí)間匆背,簡(jiǎn)單貼下實(shí)現(xiàn)方式吧呼伸。
<application
android:name=".MainApplication"
...
android:theme="@style/AppThemeWelcome>
styles.xml 增加一個(gè)主題叫 AppThemeWelcome
<style name="AppThemeWelcome" parent="Theme.AppCompat.NoActionBar">
...
<item name="android:windowBackground">@drawable/logo</item> <!-- 默認(rèn)背景-->
</style>
閃屏頁(yè)設(shè)置這個(gè)主題,或者全局給 Application 設(shè)置
<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>
這樣的話啟動(dòng) Activity 之后背景會(huì)一直在钝尸,所以在 Activity 的 onCreate 方法中切換成正常主題
protected void onCreate(@Nullable Bundle savedInstanceState) {
setTheme(R.style.AppTheme); //切換正常主題
super.onCreate(savedInstanceState);
這樣打開(kāi)桌面圖標(biāo)會(huì)馬上顯示 logo括享,不會(huì)出現(xiàn)黑/白屏,直到 Activity 啟動(dòng)完成珍促,替換主題铃辖,logo 消失,但是總的啟動(dòng)時(shí)間并沒(méi)有改變猪叙。
2.2 MultiDex 優(yōu)化
2.2.1 Apk 編譯流程
- 打包資源文件娇斩,生成 R.java 文件(使用工具 AAPT)
- 處理 AIDL 文件仁卷,生成 java 代碼 (沒(méi)有 AIDL 則忽略)
- 編譯 java 文件,生成對(duì)應(yīng) .class 文件 (java compiler)
- .class 文件轉(zhuǎn)換成 dex 文件 (dex)
- 打包成沒(méi)有簽名的 Apk (使用工具 aokbilder)
- 使用簽名工具給 Apk 簽名 (使用工具 Jarsinger)
- 對(duì)簽名后的 .apk 文件進(jìn)行對(duì)齊處理犬第,不進(jìn)行對(duì)齊處理不能發(fā)布到 Google Marker (使用工具 zipalign)
在第4步锦积,將 class 文件轉(zhuǎn)換成 dex 文件,默認(rèn)只會(huì)生成一個(gè) dex 文件歉嗓,單個(gè) dex 文件中的方法數(shù)不能超過(guò) 65536丰介,不然編譯會(huì)報(bào)錯(cuò):
Unable to execute dex: method ID not in [0, 0xffff]: 65536
App 集成一堆庫(kù)之后,方法數(shù)一般都是超過(guò) 65536 的遥椿,解決辦法就是:一個(gè) dex 裝不下基矮,用多個(gè) dex 來(lái)裝,gradle 增加一行配置即可冠场。
multiDexEnabled true
這樣解決了編譯問(wèn)題家浇,在5.0以上手機(jī)運(yùn)行正常,但是5.0以下手機(jī)運(yùn)行直接crash碴裙,報(bào)錯(cuò) Class NotFound xxx钢悲。
Android 5.0以下,ClassLoader加載類的時(shí)候只會(huì)從class.dex(主dex)里加載舔株,ClassLoader不認(rèn)識(shí)其它的class2.dex莺琳、class3.dex、...载慈,當(dāng)訪問(wèn)到不在主dex中的類的時(shí)候惭等,就會(huì)報(bào)錯(cuò):Class NotFound xxx,因此谷歌給出兼容方案办铡,MultiDex
辞做。
2.2.2 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");
}
}
從入口的判斷來(lái)看寡具,如果虛擬機(jī)本身就支持加載多個(gè)dex文件秤茅,那就啥都不用做;如果是不支持加載多個(gè)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. 這個(gè)load方法,第一次沒(méi)有緩存厦坛,會(huì)非常耗時(shí)
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();
//保存標(biāo)志位到sp,下次進(jìn)來(lái)就走if了亩歹,不走else
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
}
} else {
//沒(méi)有緩存匙监,解壓apk讀取
files = this.performExtractions();
//保存dex信息到sp,下次進(jìn)來(lái)就走if了小作,不走else
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
}
Log.i("MultiDex", "load found " + files.size() + " secondary dex files");
return files;
}
}
查找dex文件亭姥,有兩個(gè)邏輯,有緩存就調(diào)用 loadExistingExtractions
方法顾稀,沒(méi)有緩存或者緩存讀取失敗就調(diào)用 performExtractions
方法达罗,然后再緩存起來(lái)。使用到緩存静秆,那么 performExtractions
方法想必應(yīng)該是很耗時(shí)的粮揉,分析一下代碼:
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轉(zhuǎn)為zip格式
try {
int secondaryNumber = 2;
//apk已經(jīng)是改為zip格式了,解壓遍歷zip文件抚笔,里面是dex文件扶认,
//名字有規(guī)律,如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";
//創(chuàng)建這個(gè)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;
//這個(gè)方法是將classes1.dex文件寫(xiě)到壓縮文件classes1.zip里去殊橙,最多重試三次
extract(apk, dexFile, extractedFile, extractedFilePrefix);
...
}
//返回dex的壓縮文件列表
return files;
}
這里的邏輯就是解壓 apk辐宾,遍歷出里面的 dex 文件,例如class1.dex膨蛮,class2.dex叠纹,然后又壓縮成 class1.zip,class2.zip...敞葛,然后返回 zip 文件列表誉察。
思考為什么這里要壓縮呢? 后面涉及到 ClassLoader 加載類原理的時(shí)候會(huì)分析 ClassLoader 支持的文件格式惹谐。
第一次加載才會(huì)執(zhí)行解壓和壓縮過(guò)程持偏,第二次進(jìn)來(lái)讀取 sp 中保存的 dex 信息,直接返回 file list豺鼻,所以第一次啟動(dòng)的時(shí)候比較耗時(shí)综液。
dex 文件列表找到了,回到上面 MultiDex#doInstallation
方法的注釋2儒飒,找到的 dex 文件列表谬莹,然后調(diào)用 installSecondaryDexes
方法進(jìn)行安裝,怎么安裝呢桩了?方法點(diǎn)進(jìn)去看 SDK 19 以上的實(shí)現(xiàn)附帽。
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 擴(kuò)展數(shù)組
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 字段對(duì)應(yīng)的類的
makeDexElements
方法 - 通過(guò)
MultiDex.expandFieldArray
這個(gè)方法擴(kuò)展dexElements
數(shù)組,怎么擴(kuò)展井誉?看下代碼:
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)); //取出原來(lái)的dexElements 數(shù)組
Object[] combined = (Object[])((Object[])Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length)); //新的數(shù)組
System.arraycopy(original, 0, combined, 0, original.length); //原來(lái)數(shù)組內(nèi)容拷貝到新的數(shù)組
System.arraycopy(extraElements, 0, combined, original.length, extraElements.length); //dex2蕉扮、dex3...拷貝到新的數(shù)組
jlrField.set(instance, combined); //將dexElements 重新賦值為新的數(shù)組
}
就是創(chuàng)建一個(gè)新的數(shù)組,把原來(lái)數(shù)組內(nèi)容(主dex)和要增加的內(nèi)容(dex2颗圣、dex3...)拷貝進(jìn)去喳钟,反射替換原來(lái)的 dexElements
為新的數(shù)組屁使,如下圖
看起來(lái)有點(diǎn)眼熟,Tinker熱修復(fù)的原理也是通過(guò)反射將修復(fù)后的dex添加到這個(gè)dex數(shù)組去奔则,不同的是熱修復(fù)是添加到數(shù)組最前面蛮寂,而MultiDex是添加到數(shù)組后面。這樣講可能還不是很好理解易茬?來(lái)看看ClassLoader怎么加載一個(gè)類的就明白了~
2.2.3 ClassLoader 加載類原理
不管是 PathClassLoader
還是 DexClassLoader
酬蹋,都繼承自 BaseDexClassLoader
,加載類的代碼在BaseDexClassLoader
中抽莱。
2.2.4 源碼
/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
- 構(gòu)造方法通過(guò)傳入dex路徑范抓,創(chuàng)建了
DexPathList
。 - ClassLoader 的 findClass 方法最終是調(diào)用 DexPathList 的 findClass 方法
接著看 DexPathList
源碼 /dalvik/src/main/java/dalvik/system/DexPathList.java
DexPathList
里面定義了一個(gè) dexElements
數(shù)組食铐,findClass
方法中用到匕垫,看下
findClass方法邏輯很簡(jiǎn)單,就是遍歷dexElements 數(shù)組璃岳,拿到里面的DexFile對(duì)象年缎,通過(guò)DexFile的loadClassBinaryName方法加載一個(gè)類。
那么問(wèn)題來(lái)了铃慷,5.0以下這個(gè) dexElements
里面只有主dex(可以認(rèn)為是一個(gè)bug)单芜,沒(méi)有 dex2、dex3...犁柜,MultiDex 是怎么把 dex2 添加進(jìn)去呢?
答案就是反射 DexPathList
的 dexElements
字段洲鸠,然后把我們dex2 添加進(jìn)去,當(dāng)然馋缅,dexElements
里面放的是 Element
對(duì)象扒腕,我們只有 dex2 的路徑,必須轉(zhuǎn)換成 Element
格式才行萤悴,所以反射 DexPathList
里面的 makeDexElements
方法瘾腰,將 dex 文件轉(zhuǎn)換成 Element
對(duì)象即可。
dex2覆履、dex3...通過(guò) makeDexElements
方法轉(zhuǎn)換成要新增的 Element 數(shù)組蹋盆,最后一步就是反射DexPathList
的dexElements
字段,將原來(lái)的 Element 數(shù)組和新增的 Element 數(shù)組合并硝全,然后反射賦值給dexElements
變量栖雾,最后 DexPathList
的 dexElements
變量就包含我們新加的 dex 在里面了。
2.2.5 原理小結(jié)
ClassLoader 加載類原理:
ClassLoader.loadClass -> DexPathList.loadClass -> 遍歷dexElements數(shù)組 ->DexFile.loadClassBinaryName
通俗點(diǎn)說(shuō)就是:ClassLoader加載類的時(shí)候是通過(guò)遍歷dex數(shù)組伟众,從dex文件里面去加載一個(gè)類析藕,加載成功就返回,加載失敗則拋出Class Not Found 異常凳厢。
MultiDex原理:
在明白ClassLoader加載類原理之后账胧,我們可以通過(guò)反射dexElements數(shù)組竞慢,將新增的dex添加到數(shù)組后面,這樣就保證ClassLoader加載類的時(shí)候可以從新增的dex中加載到目標(biāo)類治泥,經(jīng)過(guò)分析后最終MultipDex原理圖如下:
2.2.6 MultiDex 優(yōu)化(兩種方案)
知道了MultiDex 原理之后梗顺,可以理解 install 過(guò)程為什么耗時(shí),因?yàn)樯婕暗浇鈮?apk 取出 dex车摄、壓縮 dex、將 dex 文件通過(guò)反射轉(zhuǎn)換成 DexFile 對(duì)象仑鸥、反射替換數(shù)組吮播。
那么MultiDex到底應(yīng)該怎么優(yōu)化呢,放子線程可行嗎眼俊?
方案1:子線程install(不推薦)
在閃屏頁(yè)開(kāi)一個(gè)子線程去執(zhí)行 MultiDex.install
意狠,然后加載完才跳轉(zhuǎn)到主頁(yè)。需要注意的是閃屏頁(yè)的 Activity疮胖,包括閃屏頁(yè)中引用到的其它類必須在主 dex 中环戈,不然在 MultiDex.install
之前加載這些不在主 dex 中的類會(huì)報(bào)錯(cuò) Class Not Found。這個(gè)可以通過(guò) gradle 配置澎灸,如下:
defaultConfig {
//分包院塞,指定某個(gè)類在main dex
multiDexEnabled true
multiDexKeepProguard file('multiDexKeep.pro') // 打包到main dex的這些類的混淆規(guī)制拖吼,沒(méi)特殊需求就給個(gè)空文件
multiDexKeepFile file('maindexlist.txt') // 指定哪些類要放到main dex
}
maindexlist.txt 文件指定哪些類要打包到主 dex 中骇扇,內(nèi)容格式如下
com/lanshifu/launchtest/SplashActivity.class
但是火焰,編譯運(yùn)行在4.4的機(jī)器上甲棍,啟動(dòng)閃屏頁(yè)这难,加載完準(zhǔn)備進(jìn)入主頁(yè)直接崩掉了裹匙。
報(bào)錯(cuò) NoClassDefFoundError
锚国,一般都是該類沒(méi)有在主 dex 中托享,要在 maindexlist.txt 將配置指定在主 dex其兴。
第三方庫(kù)中的 ContentProvider
必須指定在主 dex 中顶瞒,否則也會(huì)找不到,為什么元旬?文章開(kāi)頭說(shuō)過(guò)應(yīng)用的啟動(dòng)流程榴徐,ContentProvider 初始化時(shí)機(jī)如下圖:
ContentProvider初始化太早了,如果不在主dex中法绵,還沒(méi)啟動(dòng)閃屏頁(yè)就已經(jīng)crash了箕速。
所以這種方案的缺點(diǎn)很明顯:
- MultiDex加載邏輯放在閃屏頁(yè)的話,閃屏頁(yè)中引用到的類都要配置在主dex朋譬。
- ContentProvider必須在主dex盐茎,一些第三方庫(kù)自帶ContentProvider,維護(hù)比較麻煩徙赢,要一個(gè)一個(gè)配置字柠。
MultiDex優(yōu)化方案2:今日頭條方案
今日頭條沒(méi)有加固探越,反編譯后很容易通過(guò)關(guān)鍵字搜索找到 MultidexApplication
這個(gè)類,
看注釋1的 d.a(this); 這個(gè)方法窑业,代碼雖然混淆了钦幔,但是方法內(nèi)部的代碼還是可以看出是干嘛的,繼續(xù)跟這個(gè)方法常柄,為了不影響閱讀鲤氢,我對(duì)混淆做了一些處理,改成正常的方法名西潘。
每個(gè)方法開(kāi)頭都有 PatchProxy.isSupport
這個(gè) if 判斷卷玉,這個(gè)是美團(tuán) Robust 熱修復(fù)生成的代碼,今日頭條沒(méi)有自己的熱修復(fù)框架喷市,沒(méi)有用Tinker相种。Robust直接跳過(guò),看else代碼塊即可品姓。
繼續(xù)看 loadMultiDex
方法
邏輯如下:
- 創(chuàng)建臨時(shí)文件寝并,作為判斷 MultiDex 是否加載完的條件
- 啟動(dòng) LoadDexActivity 去加載 MultiDex(LoadDexActivity 在單獨(dú)進(jìn)程),加載完會(huì)刪除臨時(shí)文件
- 開(kāi)啟 while 循環(huán)腹备,直到臨時(shí)文件不存在才跳出循環(huán)衬潦,進(jìn)入 Application 的 onCreate。
創(chuàng)建臨時(shí)文件代碼
while循環(huán)代碼
LoadDexActivity 只有一個(gè)加載框馏谨,加載完再跳轉(zhuǎn)到閃屏頁(yè)
dex 加載完應(yīng)該要 finish 掉當(dāng)前 Activity
按照上面代碼分析别渔,今日頭條在 5.0 以下手機(jī)首次啟動(dòng)應(yīng)該是這樣:
- 打開(kāi)桌面圖標(biāo)
- 顯示默認(rèn)背景
- 跳轉(zhuǎn)到加載 dex 的界面,展示一個(gè) loading 的加載框幾秒鐘
- 跳轉(zhuǎn)到閃屏頁(yè)
再次梳理一下這種方式:
在主進(jìn)程 Application 的 attachBaseContext 方法中判斷如果需要使用 MultiDex惧互,則創(chuàng)建一個(gè)臨時(shí)文件哎媚,然后開(kāi)一個(gè)進(jìn)程(LoadDexActivity),顯示 Loading喊儡,異步執(zhí)行 MultiDex.install 邏輯拨与,執(zhí)行完就刪除臨時(shí)文件并 finish 自己。
主進(jìn)程 Application 的 attachBaseContext 進(jìn)入while 代碼塊艾猜,定時(shí)輪循臨時(shí)文件是否被刪除买喧,如果被刪除,說(shuō)明 MultiDex 已經(jīng)執(zhí)行完匆赃,則跳出循環(huán)淤毛,繼續(xù)正常的應(yīng)用啟動(dòng)流程。
注意 LoadDexActivity 必須要配置在 main dex 中算柳。
MultiDex優(yōu)化總結(jié)
方案1:直接在閃屏頁(yè)開(kāi)個(gè)子線程去執(zhí)行 MultiDex 邏輯低淡,MultiDex 不影響冷啟動(dòng)速度,但是難維護(hù)。
方案2:今日頭條的 MultiDex 優(yōu)化方案:
在 Application 的 attachBaseContext 方法里蔗蹋,啟動(dòng)另一個(gè)進(jìn)程的 LoadDexActivity 去異步執(zhí)行 MultiDex 邏輯何荚,顯示 Loading。
然后主進(jìn)程 Application 進(jìn)入 while 循環(huán)猪杭,不斷檢測(cè) MultiDex 操作是否完成
MultiDex 執(zhí)行完之后主進(jìn)程Application 繼續(xù)走餐塘,ContentProvider 初始化和 Application onCreate 方法,也就是執(zhí)行主進(jìn)程正常的邏輯皂吮。
預(yù)創(chuàng)建 Activity
這段代碼是今日頭條里面的戒傻,Activity 對(duì)象預(yù)先 new 出來(lái),
對(duì)象第一次創(chuàng)建的時(shí)候蜂筹,java 虛擬機(jī)首先檢查類對(duì)應(yīng)的 Class 對(duì)象是否已經(jīng)加載稠鼻。如果沒(méi)有加載,jvm 會(huì)根據(jù)類名查找 .class 文件狂票,將其 Class 對(duì)象載入。同一個(gè)類第二次 new 的時(shí)候就不需要加載類對(duì)象熙暴,而是直接實(shí)例化闺属,創(chuàng)建時(shí)間就縮短了。
第三方庫(kù)懶加載
很多第三方開(kāi)源庫(kù)都說(shuō)在 Application 中進(jìn)行初始化周霉,十幾個(gè)開(kāi)源庫(kù)都放在 Application 中掂器,肯定對(duì)冷啟動(dòng)會(huì)有影響,所以可以考慮按需初始化俱箱,例如 Glide国瓮,可以放在自己封裝的圖片加載類中,調(diào)用到再初始化狞谱,其它庫(kù)也是同理乃摹,讓 Application 變得更輕。
WebView 啟動(dòng)優(yōu)化
- WebView第一次創(chuàng)建比較耗時(shí)跟衅,可以預(yù)先創(chuàng)建WebView孵睬,提前將其內(nèi)核初始化。
- 使用WebView緩存池伶跷,用到WebView的地方都從緩存池取掰读,緩存池中沒(méi)有緩存再創(chuàng)建,注意內(nèi)存泄漏問(wèn)題叭莫。
- 本地預(yù)置html和css蹈集,WebView創(chuàng)建的時(shí)候先預(yù)加載本地html,之后通過(guò)js腳本填充內(nèi)容部分雇初。
在實(shí)際開(kāi)發(fā)中拢肆,遇到過(guò)這樣一個(gè)情況,初始化的時(shí)候一定要初始化 webview 用于展示第三方的廣告,由于 Splash 廣告需要第一時(shí)間來(lái)展示善榛,所以優(yōu)先級(jí)很高辩蛋,可以試一下下面這個(gè)方法:
private void startWebViewFactoryEngines(){
try {
Class<?> factoryClass = Class.forName("android.webkit.WebViewFactory");
Method method = factoryClass.getDeclaredMethod("getProvider",(Class<?>)null);
method.setAccessible(true);
Object webViewChromiumFactoryProvider = method.invoke(null,(Object)null);
Method startMethod = webViewChromiumFactoryProvider.getClass().getDeclaredMethod("startYourEngines",boolean.class);
startMethod.setAccessible(true);
startMethod.invoke(webViewChromiumFactoryProvider,false);
} catch (Exception e) {
e.printStackTrace();
Log.d("DEBUGPROVIDER","error in start the WebViewFactory Engines:" + e.getMessage());
}
}
可以參考(手機(jī)打開(kāi)):mp.weixin.qq.com/s/KwvWURD5W…
2.6 數(shù)據(jù)預(yù)加載
這種方式一般是在主頁(yè)空閑的時(shí)候,將其它頁(yè)面的數(shù)據(jù)加載好移盆,保存到內(nèi)存或數(shù)據(jù)庫(kù)悼院,等到打開(kāi)該頁(yè)面的時(shí)候,判斷已經(jīng)預(yù)加載過(guò)咒循,直接從內(nèi)存或數(shù)據(jù)庫(kù)讀取數(shù)據(jù)并顯示据途。
2.7 線程優(yōu)化
線程是程序運(yùn)行的基本單位,線程的頻繁創(chuàng)建是耗性能的叙甸,所以大家應(yīng)該都會(huì)用線程池颖医。單個(gè) cpu 情況下,即使是開(kāi)多個(gè)線程裆蒸,同時(shí)也只有一個(gè)線程可以工作熔萧,所以線程池的大小要根據(jù) cpu 個(gè)數(shù)來(lái)確定。
啟動(dòng)優(yōu)化方式就先介紹到這里僚祷,常見(jiàn)的就是這些佛致,其它的可以作為補(bǔ)充。
相關(guān)鏈接:
藍(lán)師傅 --- 面試官:今日頭條啟動(dòng)很快辙谜,你覺(jué)得可能是做了哪些優(yōu)化俺榆?
Developers
申明:開(kāi)始和結(jié)束的圖片來(lái)自網(wǎng)絡(luò),侵刪