0. 應用啟動優化概述
在 Android 開發中,應用啟動速度是一個非常重要的點,應用啟動優化也是一個非常重要的過程.對於應用啟動優化,其實核心思想就是在啟動過程中少做事情,具體實踐的時候無非就是下面幾種:
- 非同步載入
- 延時載入
- 懶載入
不用一一去解釋,做過啟動優化的估計都使用過,本篇文章將詳細講解一下一種延時載入的實現以及其原理.
其實這種載入的實現是非常簡單的,但是其中的原理可能比較複雜,還涉及到Looper/Handler/MessageQueue/VSYNC等.以及其中碰到的一些問題,還會有一些我自己額外的思考.
1. 優化後的DelayLoad的實現
一提到DelayLoad,大家可能第一時間想到的就是在 onCreate 裡面呼叫 Handler.postDelayed方法, 將需要 Delay 載入的東西放到這裡面去初始化, 這個也是一個比較方便的方法. Delay一段時間再去執行,這時候應用已經載入完成,介面已經顯示出來了, 不過這個方法有一個致命的問題: 延遲多久?
大家都知道,在 Android 的高階機型上,應用的啟動是非常快的 , 這時候只需要 Delay 很短的時間就可以了, 但是在低端機型上,應用的啟動就沒有那麼快了,而且現在應用為了相容舊的機型,往往需要 Delay 較長的時間,這樣帶來體驗上的差異是很明顯的.
這裡先說優化方案:
1. 首先 , 建立 Handler 和 Runnable 物件, 其中 Runnable 物件的 run方法裡面去更新 UI 執行緒.
1 2 3 4 5 6 7 8 |
private Handler myHandler = new Handler(); private Runnable mLoadingRunnable = new Runnable() { @Override public void run() { updateText(); //更新UI執行緒 } }; |
2. 在主 Activity 的 onCreate 中加入下面的程式碼
1 2 3 4 5 6 7 |
getWindow().getDecorView().post(new Runnable() { @Override public void run() { myHandler.post(mLoadingRunnable); } }); |
其實實現的話非常簡單,我們來對比一下三種方案的效果.
2. 三種寫法的差異對比
為了驗證我們優化的 DelayLoad的效果,我們寫了一個簡單的app , 這個 App 中包含三張不同大小的圖片,每張圖片下面都會有一個 TextView , 來標記圖片的顯示高度和寬度. MainActivity的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
public class MainActivity extends AppCompatActivity { private static final int DEALY_TIME = 300 ; private ImageView imageView1; private ImageView imageView2; private ImageView imageView3; private TextView textView1; private TextView textView2; private TextView textView3; private Handler myHandler = new Handler(); private Runnable mLoadingRunnable = new Runnable() { @Override public void run() { updateText(); } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); imageView1 = (ImageView) findViewById(R.id.image1); imageView2 = (ImageView) findViewById(R.id.image2); imageView3 = (ImageView) findViewById(R.id.image3); textView1 = (TextView) findViewById(R.id.text1); textView2 = (TextView) findViewById(R.id.text2); textView3 = (TextView) findViewById(R.id.text3); // 第一種寫法:直接Post myHandler.post(mLoadingRunnable); // 第二種寫法:直接PostDelay 300ms. // myHandler.postDelayed(mLoadingRunnable, DEALY_TIME); // 第三種寫法:優化的DelayLoad // getWindow().getDecorView().post(new Runnable() { // @Override // public void run() { // myHandler.post(mLoadingRunnable); // } // }); // Dump當前的MessageQueue資訊. getMainLooper().dump(new Printer() { @Override public void println(String x) { Log.i("Gracker",x); } },"onCreate"); } private void updateText() { TraceCompat.beginSection("updateText"); textView1.setText("image1 : w=" + imageView1.getWidth() + " h =" + imageView1.getHeight()); textView2.setText("image2 : w=" + imageView2.getWidth() + " h =" + imageView2.getHeight()); textView3.setText("image3 : w=" + imageView3.getWidth() + " h =" + imageView3.getHeight()); TraceCompat.endSection(); } |
我們需要關注兩個點:
- updateText 這個函式是什麼時候被執行的?
- App 啟動後,三個圖片的長寬是否可以被正確地顯示出來?
- 是否有 Delay Load 的效果?
2.1 第一種寫法
- updateText執行的時機?
下面是第一種寫法的Trace圖:
可以看到 updateText 是在 Activity 的 onCreate/onStart/onResume三個回撥執行完成後才去執行的. - 圖片的寬高是否正確顯示?
從圖片看一看到,寬高並沒有顯示. 這是為什麼呢? 這個問題就要從Activity 的 onCreate/onStart/onResume三個回撥說起了. 其實Activity 的 onCreate/onStart/onResume三個回撥中,並沒有執行Measure和Layout操作, 這個是在後面的performTraversals中才執行的. 所以在這之前寬高都是0.
- 是否有 Delay Load 的效果?
並沒有. 因為我們知道, 應用啟動的時候,要等兩次 performTraversals 都執行完成之後才會顯示第一幀, 而 updateText 這個方法在第一個 performTraversals 執行之前就執行了. 所以 updateText 方法的執行時間是算在應用啟動的時間裡面的.
2.2 第二種寫法
第二種寫法我們Delay了300ms .我們來看一下表現.
- updateText執行的時機?
可以看到,這種寫法的話,updateText是在兩個performTraversals 執行完成之後(這時候 APP 的第一幀才顯示出來)才去執行的, 執行完成之後又呼叫了一次 performTraversals 將 TextView 的內容進行更新.
- 圖片的寬高是否正確顯示?
從上圖可以看到,圖片的寬高是正確顯示了出來. 原因上面已經說了,measure/layout執行完成後,寬高的資料就可以獲取了.
- 是否有 Delay Load 的效果?
不一定,取決於 Delay的時長.
從前面的 Trace 圖上我們可以看到 , updateText 方法由於 Delay 了300ms, 所以在應用第一幀顯示出來170ms之後, 圖片的文字資訊才進行了更新. 這個是有 Delay Load 的效果的.
但是這裡只是一個簡單的TextView的更新, 如果是較大模組的載入 , 使用者視覺上會有很明顯的 “ 空白->內容填充” 這個過程, 或者會附加”閃一下”特效…這顯然是我們不想看到的.有人會說:可以把Delay的時間減小一點嘛,這樣就不會閃了. 話是這麼說,但是由於 Android 機器的多元性(其實就是有很多高階機器,也有很多低端機器) , 在這個機子上300ms的延遲算是快,在另外一個機子上300ms算是很慢.
我們將Delay時間調整為50ms, 其Trace圖如下:
可以看到,updateText 方法在第一個 performTraversals 之後就執行了,所以也沒有 Delay Load 的效果(雖然寬高是正確顯示了,因為在第一個 performTraversals 方法中就執行了layout和measure).
2.3 第三種寫法
經過前兩個方法 , 我們就會想, 如果能不使用Delay方法, updateText 方法能在 第二個performTraversals 方法執行完成後(即APP第一幀在螢幕上顯示),馬上就去執行,那麼即起到了 Delay Load的作用,又可以正確顯示圖片的寬高.
第三種寫法就是這個效果:
- updateText執行的時機?
可以看到這種寫法. updateText 在第二個 performTraversals 方法執行完成後馬上就執行了, 然後下一個 VSYNC 訊號來了之後, TextView就更新了.
- 圖片的寬高是否正確顯示?
當然是正確顯示的.如圖:
- 是否有 Delay Load 的效果?
從 Trace 圖上看, 是有 Delay Load的效果的, 而且可以在應用第一幀顯示後馬上進行資料 Load , 不用考慮 Delay時間的長短.
3. 一些思考
關於優化的 Delay Load 的實現,從程式碼層面來看其實是非常簡單的.其帶來的效果也是很讚的.
但是實現之後我們還需要思考一下,為何這麼做就可以實現這種功能呢?很顯然要回答這個問題,我們需要知道更底層的一些東西.這個還涉及到 Handler/Message/MessageQueue/Looper/VSYNC/ViewRootImpl等知識. 往大里說應該還涉及到AMS/WMS等.由於涉及到的東西比較多,我就不在這一篇裡面闡述了, 下一篇文章將會從從原理上講解一下為何優化的 Delay Load 會起作用.
4. 程式碼
本文章所所涉及到的程式碼我放到了Github上:
https://github.com/Gracker/DelayLoadSample