本文首發於微信公眾號「玉剛說」
原文連結:你的 APP 為何啟動那麼慢?
App啟動方式
冷啟動(Cold start)
冷啟動是指APP在手機啟動後第一次執行,或者APP程式被kill掉後在再次啟動。
可見冷啟動的必要條件是該APP程式不存在,這就意味著系統需要建立程式,APP需要初始化。在這三種啟動方式中,冷啟動耗時最長,對於冷啟動的優化也是最具挑戰的。因此本文重點談論的是對冷啟動相關的優化。
溫啟動(Warm start)
App程式存在,當時Activity可能因為記憶體不足被回收。這時候啟動App不需要重新建立程式,但是Activity的onCrate還是需要重新執行的。場景類似開啟淘寶逛了一圈然後切到微信去聊天去了,過了半小時再次回到淘寶。這時候淘寶的程式存在,但是Activity可能被回收,這時候只需要重新載入Activity即可。
熱啟動(Hot start)
App程式存在,並且Activity物件仍然存在記憶體中沒有被回收。可以重複避免物件初始化,佈局解析繪製。
場景就類似你開啟微信聊了一會天這時候出去看了下日曆 在開啟微信 微信這時候啟動就屬於熱啟動。
在最近任務給App加鎖和啟動方式有什麼關係
某些廠商為了使用者體驗提供了給APP上鎖的功能,目的就是讓使用者自己做主是上鎖的APP不被殺,啟動的時候不會處於冷啟動方式,但是加鎖也不是萬能的,Low memory killer在記憶體極度吃緊的情況下也會殺死加鎖APP,在此啟動時也將以冷啟動方式執行。
AI和啟動方式有什麼關係
AI在程式管理方面可謂是大有可為。MIUI10釋出了程式AI喚醒功能,使APP啟動速度遠超友商。這其中的道理簡單說就是學習使用者的使用習慣,提前將App程式建立好,當使用者開啟APP時不會出現冷啟動。比如你是微信重度使用者你發現用了MIUI10就再也見不到微信啟動頁面的那個地球了,這就是AI喚醒的功勞。
從點選APP圖示到主頁顯示出現需要經過的步驟
這裡我們來討論冷啟動的過程,程式啟動原則上有四種途徑,也就是通過其他程式對該APP的四大元件的呼叫來實現。
這裡我們重點討論使用者點選桌面後的APP啟動,通過startActivity方式的啟動。
呼叫startActivity,該方法經過層層呼叫,最終會呼叫ActivityStackSupervisor.java中的startSpecificActivityLocked,當activity所屬程式還沒啟動的情況下,則需要建立相應的程式.
void startSpecificActivityLocked(...) {
ProcessRecord app = mService.getProcessRecordLocked(r.processName,
r.info.applicationInfo.uid, true);
if (app != null && app.thread != null) {
...//程式已建立
return
}
//建立程式
mService.startProcessLocked(r.processName, r.info.applicationInfo, true, 0,
"activity", r.intent.getComponent(), false, false, true);
}
複製程式碼
最終程式由 Zygote Fork程式:
程式啟動後系統還有一個工作就是:程式啟動後立即顯示應用程式的空白啟動視窗。
一旦系統建立應用程式程式,應用程式程式就會負責下一階段。這些階段是:
1.建立應用程式物件
2.啟動主執行緒
3.建立主要Activity
4.繪製檢視(View)
5.佈局螢幕
6.執行初始化繪製
而一旦App程式完成了第一次繪製,系統程式就會用Main Activity替換已經展示的Background Window,此時使用者就可以使用App了。
這裡很明顯有兩個優化點:
1.Application OnCrate()優化
當APP啟動時,空白的啟動視窗將保留在螢幕上,直到系統首次完成繪製應用程式。此時,系統程式會交換應用程式的啟動視窗,允許使用者開始與應用程式進行互動。如果應用程式中過載了Application.onCreate(),系統會呼叫onCreate()方法。之後,應用程式會生成主執行緒(也稱為UI執行緒),並通過建立MainActivity來執行任務。
2.Activity onCreate()優化
onCreate()方法對載入時間的影響最大,因為它以最高的開銷執行工作:載入並繪製檢視,以及初始化Activity執行所需的物件。
啟動速度優化
如何對啟動時間進行量化?
1.目前為止見過最最牛逼的是使用機械手和高速相機測試,手機開機後使用機械手點選應用桌面圖示,高速相機記錄啟動過程,後續通過程式分析視訊,從機械手點選圖示到Activity顯示出來使用了多少時間。這種方式是最直觀和精確的,但是成本也很高。
2.通過shell 命令
adb shell am start -W [packageName]/[packageName.MainActivity]
複製程式碼
執行成功後將返回三個測量到的時間:
ThisTime:一般和TotalTime時間一樣,除非在應用啟動時開了一個透明的Activity預先處理一些事再顯示出主Activity,這樣將比TotalTime小。
TotalTime:應用的啟動時間,包括建立程式+Application初始化+Activity初始化到介面顯示。
WaitTime:一般比TotalTime大點,包括系統影響的耗時。
3.可以通過在程式碼中增加log來計算啟動時間
4.使用systrace
Application OnCrate()優化
1.第三方SDK初始化的處理
Application是程式的主入口,很多三方SDK示例程式中都要求自己在Application OnCreate時做初始化操作。這就是增加Application OnCreate時間的主要元凶,所以需要儘量避免在Application onCreate時同步做初始化操作。比較好的解決方案就是對三方SDK就行懶載入,不在Application OnCreate()時初始化,在真正用到的時候再去載入。
下面例項對比下ImageLoader在採用懶載入後啟動速度優化。
一般我們在使用imageLoader時都會在Application onCreate()時在主執行緒載入:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
ImageLoaderConfiguration.Builder config =
new ImageLoaderConfiguration.Builder(this);
ImageLoader.getInstance().init(config.build());
}
}
複製程式碼
此時使用adb shell am start -W [packageName]/[packageName.MainActivity]檢測應用啟動時間,每次執行命令時需要殺死程式。
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.luozhanwei.myapplication/.MainActivity }
Status: ok
Activity: com.luozhanwei.myapplication/.MainActivity
ThisTime: 423
TotalTime: 423
WaitTime: 441
複製程式碼
Total time在423ms之間,下面是封裝了一個懶載入ImageLoader的工具類示例:
public class ImageUtil {
private static boolean sInit;
private synchronized static void ensureInit() {
if (sInit) {
return;
}
ImageLoaderConfiguration.Builder config =
new ImageLoaderConfiguration.Builder(SecurityCoreApplication.getInstance());
....
// Initialize ImageLoader with configuration.
ImageLoader.getInstance().init(config.build());
sInit = true;
}
public static void display(String uri, ImageView imageView, boolean cacheOnDisk) {
imageView.setImageResource(R.drawable.icon_app_default);
ensureInit();
ImageLoader loader = ImageLoader.getInstance();
if (cacheOnDisk) {
loader.displayImage(uri, imageView);
} else {
loader.displayImage(uri, imageView, OPTIONS_NO_CACHE_DISK);
}
}
複製程式碼
使用這種方案後的啟動時間:
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.luozhanwei.myapplication/.MainActivity }
Status: ok
Activity: com.luozhanwei.myapplication/.MainActivity
ThisTime: 389
TotalTime: 389
WaitTime: 405
複製程式碼
看到TotalTime比之前減少了34ms(給出的資料為10次檢測平均值)。
所以Application OnCreate 避免在主執行緒做大量耗時操作,例如和IO相關的邏輯,這樣都會影響到應用啟動速度。如果必須要做需要放到子執行緒中。
Activity onCreate()優化
減少LaunchActivity的View層級,減少View測量繪製時間。
避免主執行緒做耗時操作
使用者體驗優化
消除啟動時的白屏/黑屏
為什麼啟動時會出現短暫黑屏或白屏的現象?當使用者點選你的app那一刻到系統呼叫Activity.onCreate()之間的這個時間段內,WindowManager會先載入app主題樣式中的windowBackground做為app的預覽元素,然後再真正去載入activity的layout佈局。
很顯然,如果你的application或activity啟動的過程太慢,導致系統的BackgroundWindow沒有及時被替換,就會出現啟動時白屏或黑屏的情況(取決於你的主題是Dark還是Light)。
解決方案
1.甩鍋給系統
使用透明主題:
<item name="android:windowIsTranslucent">true</item>
複製程式碼
Activity.onCreate()之前App不做顯示,這樣使用者誤以為是手機慢了,這種瞞天過海的方案大家還是不要用了。
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowIsTranslucent">true</item>
</style>
</resources>
複製程式碼
效果如下:
2.主題替換
我們在style中自定義一個樣式Lancher,在其中放一張背景圖片,或是廣告圖片之類的
<style name="AppTheme.Launcher">
<item name="android:windowBackground">@drawable/bg</item>
</style>
複製程式碼
把這個樣式設定給啟動的Activity
<activity
android:name=".activity.SplashActivity"
android:screenOrientation="portrait"
android:theme="@style/AppTheme.Launcher"
>
複製程式碼
然後在Activity的onCreate方法,把Activity設定回原來的主題
@Override
protected void onCreate(Bundle savedInstanceState) {
//替換為原來的主題,在onCreate之前呼叫
setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);
}
複製程式碼
這樣在啟動時就通過給使用者看一張圖片或是廣告來防止黑白屏的尷尬。