前段時間作冷啓動優化,恰好也好久沒寫博文了,以爲仍是頗有必要記錄下。前端
public class MainActivity extends Activity {
private static final Handler sHandler = new Handler(Looper.getMainLooper());
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sHandler.postDelay(new Runnable() {
@Override
public void run() {
// 頁面啓動所需耗時初始化
doSomething();
}
}, 200);
}
}複製代碼
大部分開發者在遇到頁面冷啓動耗時初始化時,會首先考慮經過Handler.postDelay()方法延遲執行。但延遲多久合適?100ms?500ms?仍是1s?java
延遲過晚,可能會有體驗問題;延遲過早,對冷啓動沒效果。延遲的時間(好比200ms)在三星手機上測試時沒問題,換了在華爲手機試了就有問題了,而後就圍繞着機型的適配不斷調整延遲的時間,試圖尋找最合適的值,結果發現根本就是不可能的。android
先來看一張圖shell
上圖是Google提供的冷啓動流程圖,能夠看到冷啓動的起始點時Application.onCreate()方法,結束點在ActivityRecord.reportLanuchTimeLocked()方法。數組
咱們能夠經過如下兩種方式查看冷啓動的耗時bash
在 Android Studio Logcat 過濾關鍵字 「Displayed」,能夠查看到以下日誌:微信
2019-07-03 01:49:46.748 1678-1718/? I/ActivityManager: Displayed com.tencent.qqmusic/.activity.AppStarterActivity: +12s449ms
後面的12s449ms就是冷啓動耗時架構
經過終端執行「adb shell am start -W -S <包名/完整類名> 」app
「ThisTime:1370」即爲本次冷啓動耗時(單位ms)異步
上面知道,冷啓動計時起始點是Application.onCreate(),結束點是ActivityRecord.reportLanuchTimeLocked(),但這不是咱們能夠寫業務寫邏輯的地方啊,大部分應用業務都以Activity爲載體,那麼結束回調在哪?
從冷啓動流程圖看,結束時間是在UI渲染完計算的,因此很明顯,Activity生命週期中的onCreate()、onResume()、onStart()都不能做爲冷啓動的結束回調。
常規操做中用Handler.postDelay()問題在於Delay的時間不固定,但咱們知道消息處理機制中,MessageQueue有個ArrayList<IdleHandler>
public final class MessageQueue {
Message mMessages;
priavte final ArrayList<IdleHandler> mIdelHandlers = new ArrayList<IdelHandler>();
Message next() {
...
int pendingIdelHandlerCount = -1; // -1 only during first iteration
for(;;) {
...
// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}
// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null;
// release the reference to the handler
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
}
...
}
}
}複製代碼
能夠在列表中添加Idle任務,Idle任務列表只有MessageQueue隊列爲空時纔會執行,也就是所在線程任務已經執行完時,線程處於空閒狀態時纔會執行Idle列表中的任務。
冷啓動過程當中,在Activity.onCreate()中將耗時初始化任務放置到Idle中
public class MainActivity extends Activity {
private static final Handler sHandler = new Handler(Looper.getMainLooper());
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
// 頁面啓動所需耗時初始化
doSomething();
return false;
}});
}
}複製代碼
正常狀況下,初始化任務是在UI線程全部任務執行完纔開始執行,且該方案也不用考慮機型問題。但有個問題,若是UI線程的任務一直不執行完呢?會有這狀況?舉個🌰,Activity首頁頂部有個滾動的Banner,banner的滾動是經過不斷增長延遲Runnable實現。那麼,初始化任務就可能一直無法執行。
另外,若是初始化的任務會關係到UI的刷新,這時,在Activity顯示後再去執行,在體驗上也可能會有所折損。
回顧冷啓動流程圖,冷啓動結束時,恰好是UI渲染完,若是咱們能確保在UI渲染完再去執行任務,這樣,既能提高冷啓動數據,又能解決UI上的問題。
所以,解鈴還須繫鈴人,要想找到最合適的結束回調,仍是得看源碼。
首先,咱們找到了第一種方案
public class BaseActivity extends Activity {
private static final Handler sHandler = new Handler(Looper.getMainLooper());
private boolean onCreateFlag;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
onCreateFlag = true;
setContentView(R.layout.activity_main);
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (onCreateFlag && hasFocus) {
onCreateFlag = false;
sHandler.post(new Runnable() {
@Override
public void run() {
onFullyDrawn();
}
})
}
}
@CallSuper
protected void onFullyDrawn() {
// TODO your logic
}
}複製代碼
關於onWindowFocusChanged()的系統調用流程感興趣的能夠看看個人上一篇文章 《Activity.onWindowFocusChanged()調用流程》
至於爲何要在onWindowFocusChanged()再經過Handler.post()延後一個任務,一開始我是經過打點,發現沒post()時,onWindowFocusChanged()打點在Log「Displayed」以前,增長post()便在Log「Displayed」以後,梳理了下調用流程,大概是渲染調用requestLayout()也是增長任務監聽,只有SurfaceFlinger渲染信號回來時纔會觸發渲染,所以延後一個任務,恰好在其以後
第二種方案,咱們經過View.post(Runnable runnable)方法實現
public class BaseActivity extends Activity {
private static final Handler sHandler = new Handler(Looper.getMainLooper());
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
// 方案只有在onResume()或以前調用有效
protected void postAfterFullDrawn(final Runnable runnable) {
if (runnable == null) {
return;
}
getWindow().getDecorView().post(new Runnable() {
@Override
public void run() {
sHandler.post(runnable);
}
});
}
}複製代碼
須要注意的是,該方案只有在onResume()或以前調用有效。爲何?
先看View.post()源碼實現
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
// 這裏要注意啦!attachInfo 不爲空,實際是經過Handler.post()延遲一個任務
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
private HandlerActionQueue mRunQueue;
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}複製代碼
經過View.post()調用了HandlerActionQueue.post()
public class HandlerActionQueue {
private HandlerAction[] mActions;
private int mCount;
public void post(Runnable action) {
postDelayed(action, 0);
}
/** * 該方法僅僅是將傳入的任務Runnable存放到數組中 **/
public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}
}複製代碼
到此,咱們調用View.post(Runnable runnable)僅僅是把任務Runnable以HandlerAction姿式存放在HandlerActionQueue的HandlerAction[]數組中。那這個數組何時會被訪問調用?
既然是冷啓動,那仍是得看冷啓動系統的回調,直接看ActivityThread.handleResumeActivity()
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
ActivityClientRecord r = mActivities.get(token);
...
r = performResumeActivity(token, clearHide, reason); ...
if (r != null) {
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
// 上面一大串操做基本能夠不看,由於到這咱們基本都知道下一步是渲染,也就是ViewRootImpl上場了
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
}
}
}複製代碼
到渲染了,直接進ViewRootImpl.performTraversals()
public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
boolean mFirst;
public ViewRootImpl(Context context, Display display) {
...
mFirst = true; // true for the first time the view is added
...
}
private void performTraversals() {
final View host = mView;
...
if (mFirst) {
...
host.dispatchAttachedToWindow(mAttachInfo, 0);
...
}
...
performMeasure();
performLayout();
preformDraw();
...
mFirst = false;
}
}複製代碼
再進到View.dispatchAttachedToWindow()去瞧瞧
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
// 倒車請注意!倒車請注意!這裏mAttachInfo != null啦!
mAttachInfo = info;
...
// Transfer all pending runnables.
// 系統也提示了,到這裏執行pending的任務runnbales
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
...
}
// 開始訪問前面存放的任務,看看executeActions()怎麼工做
public class HandlerActionQueue {
private HandlerAction[] mActions;
/** * 我褲子都脫了,你給我看這些?實際也是調用Handler.post()執行任務 **/
public void executeActions(Handler handler) {
synchronized (this) {
final HandlerAction[] actions = mActions;
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
mActions = null;
mCount = 0;
}
}
}複製代碼
也就是說,View內部維護了一個HandlerActionQueue,咱們能夠在DecorView attachToWindow前,經過View.post()將任務Runnables存放到HandlerActionQueue中。當DecorView attachToWindow時會先遍歷先前存放在HandlerActionQueue的任務數組,經過handler挨個執行。
1.在View.dispatchAttachedToWindow()時mAttachInfo就被賦值了,所以,以後經過View.post()實際就是直接調用Handler.post()執行任務。再往前看,performResumeActivity()在渲染以前先執行,也就說明了爲何只有在onResume()或以前調用有效
2.在View.post()的Runnable run()方法回調中在延遲一個任務,從performTraverals() 調用順序看恰好是在渲染完後下一個任務執行
先來看兩張效果圖
第一張點擊完桌面Icon後並無立刻拉起應用,而是停頓了下,給人感受是手機卡頓了;
第二張點擊完桌面Icon後當即出現白屏,而後隔了一段時間後纔出現背景圖,體驗上很明顯以爲是應用卡了。
那是什麼致使它們的差別?答案就是把閃屏Activity主題設置成全屏無標題欄透明樣式
<activity android:name="com.huison.test.MainActivity" ... android:theme="@style/TranslucentTheme" />
<style name="TranslucentTheme" parent="android:Theme.Translucent.NoTitleBar.Fullscreen" />複製代碼
這樣能夠解決冷啓動白屏或黑屏問題,體驗上會更好。
關於冷啓動優化,總結爲12個字「 減法爲主,異步爲輔,延遲爲補 」
儘可能作減法,能不作的儘可能不作!
Application.onCreate()必定要輕!必定要輕!必定要輕!項目中多多少少會涉及到第三方SDK的接入,但不要所有在Application.onCreate()中初始化,儘可能懶加載。
Debug包能夠加日誌打印和部分統計,但Release能不加的就不加
耗時任務儘可能異步!見過好多RD都不怎麼喜歡作回調,獲取某個狀態值時,即便調用的函數很耗時,也是直接調用,異步回調從新刷新轉態值也能知足業務需求。
固然也不是全部的場景都採用異步回調,由於異步就涉及線程切換,在某些場景下可能會出現閃動,UI體驗極差,因此說要儘可能!
其實前面找結束點都是爲延遲鋪路的,但延遲方案並非最佳的,當咱們把冷啓動的任務都延遲到結束時執行,冷啓動是解決了,但有可能出現結束時任務過多、負載過大而引起其餘問,好比ANR、交互卡頓。之前作服務端時,前端(當時幾百萬DAU)有一個哥們直接寫死早上9點請求某個接口,致使接口直接報警了,若是他把9點改成10點,結果確定同樣,後面改爲了區段性隨機請求,這樣就把峯值磨平了。一樣,冷啓動過程若是把任務都延遲到結束點,那結束點也有可能負載過大出問題。
削峯填谷,離散化任務,合理地利用計算機資源纔是解決根本問題!
1.冷啓動儘可能減小SharedPreferences使用,尤爲是和文件操做一塊兒,底層ContextImpl同步鎖常常直接卡死。網上有人說用微信的MMKV替換SP,我試了下,效果不是很明顯,可能和項目有關係吧,不過MMKV初始化也須要時間。
2.關注冷啓動的常駐內存和GC狀況,若是GC過於頻繁也會有所影響,支付寶作過這方面的分析
支付寶客戶端架構解析:Android 客戶端啓動速度優化之「垃圾回收」
到此,冷啓動優化總結也算告一段落,有人會問作了那麼多,效果到底如何?好像是哦,最怕就是"一頓操做猛如虎,上線review二百五"!GP-Vitals有冷啓動指標,項目優化前冷啓動時間過長(>5s)百分比爲3.63%,一頓操做後百分比降低到0.95%,哇!Surprise!