Android App秒開的奧祕

ad6623發表於2019-09-20

什麼是秒開

Android App秒開,狹義的講是指你的App的Activity從啟動到顯示所花費的時間在1秒以內,廣義的講是指這個過程所花費的時間越少越好。這個時間越短,你的App給使用者的感覺就是響應越快,使用越流暢,使用者體驗更好。秒開是Android App的一個很重要的效能指標。需要我們持續的給予關注和優化。 #如何優化秒開 Google提供了很多效能優化的建議和官方的工具,網上也有非常多的關於Android App效能優化的文章和工具,可以幫助你解決大部分卡頓的問題。但是現實卻可能是即使你付出了很多精力去做優化,你的App還是在啟動新Activity的時候花費過多的時間。特別是隨著需求的不斷增長,你的App會變得複雜而龐大,要做優化首先要定位需要優化的點,而這會變得愈發困難。同時大型App在啟動新Activity的時間花費過多情況出現的可能性反而會越來越大。

在眾多的優化建議中,有一條比較基本的原則是儘量避免在主執行緒(或者說UI執行緒)中進行耗時操作。例如檔案讀寫操作、網路請求、大量計算、迴圈等等。直觀的理解是因為啟動新Activity需要在主執行緒執行很多程式碼,例如onCreate()等生命週期的回撥。如果此時有耗時操作的程式碼在主執行緒被執行,到新Activity展示出來所需要的時間就會延長。要優化秒開,首先要能監測主執行緒的執行狀態,那麼問題來了,主執行緒到底是怎樣在執行呢?你的程式碼又是什麼時候,如何在主執行緒被執行的呢?

深入主執行緒

要了解主執行緒的工作過程,首先要了解Android的訊息機制。

訊息機制

先看一下現實生活中的一個例子,雖然現在都是移動支付了,但相信大家都去銀行取過錢。當你到達銀行的時候,如果你是第一個,那恭喜你,你可以馬上到櫃員那裡辦理你的業務;如果你前面還有人,那就比較慘了,你需要排隊,得等到你前面的人都辦完業務才會輪到你;更可怕的是如果你前面有幾位需要辦理的業務花費的時間比較長,那你需要等更長的時間;後面來的人則會按順序排在你身後,和你一樣不耐煩的琢磨什麼時候才能輪到自己。

抽象一下,訊息機制其實和這個例子十分類似。每個人都看做是個訊息,什麼時候到的銀行是不確定的。櫃員可以看做一個訊息處理器,他幫你辦業務就相當於在處理你的訊息;而人們按照先後順序排起來的隊伍可以看做是個訊息佇列。所以這個過程可以抽象為有個訊息處理器,他有個訊息佇列,隨機來到的訊息按照一定順序排列在這個佇列裡,訊息處理器不停的從佇列頭部獲取訊息然後處理之,周而復始的迴圈重複這個過程。如下圖所示:

訊息機制
那麼Android是怎樣怎樣實現這個訊息機制的呢?

Android的訊息機制

訊息機制首先得有訊息,在Android中就是Message。怎樣能確定一個訊息呢?訊息要有來源或者目標,也就是target;訊息要表明自己要做什麼,也就是what或者callback;訊息要表明自己希望在什麼時候執行,也就是when。有了這幾個要素,基本上這個訊息就是個完備的訊息了,可以被加入到訊息佇列中了。Android中的訊息佇列是MessageQueue。訊息處理迴圈是Looper。Looper是個死迴圈,不停的從MessageQueue中獲取訊息然後處理之,具體的執行是在Handler裡面進行的。另外訊息加入訊息佇列也需要Handler來操作。Message,MessageQueue,Looper,Handler組合在一起,就構成了整個Android的訊息機制。

Android的主執行緒就執行著這樣一個訊息機制。

Android的主執行緒

主執行緒是在ActivityThread中建立的,可以看到在main函式中

public static void main(String[] args) {
        ...
        Looper.prepareMainLooper();
        ...
        Looper.loop();
    }
複製程式碼

主執行緒實現了一個訊息機制。所以Android的主執行緒就是個訊息處理的迴圈。它所做的工作就是在不停的從訊息佇列獲取訊息,處理訊息,周而復始。你的App所有的在UI上的操作,例如點選事件的處理、頁面動畫、顯示更新頁面、View繪製、啟動新Activity等操作都是在給主執行緒發訊息,主執行緒然後挨個處理這些訊息。

主執行緒如何影響秒開

我們瞭解了主執行緒的工作機制後,就要看看主執行緒中的訊息處理是如何影響Activity秒開的。 當我們要啟動一個新的Activity的時候,從呼叫startActivity開始到新Activity顯示出來,Android系統會傳送一系列的訊息給主執行緒。這一系列的訊息處理所花費的總時間會影響頁面的秒開,如果執行時間過長,使用者就會有響應非常慢的感覺。此外,除了Android系統會給主執行緒發訊息,App自身也會給主執行緒發訊息,如果在啟動新Activity的過程中,這些App自己的訊息正好插入這一系列的Android系統訊息中,那也會導致總的處理時間延長,造成不能秒開。

秒開示意
上圖代表了啟動新Activity的主執行緒的三種情況,每個矩形代表主執行緒處理一個訊息所花的時間,越寬代表處理的時間越長。綠色填充的代表這是一個Android系統發過來的訊息;藍色填充的代表這是一個App自己發過來的訊息。最下方的向右箭頭代表時間,起點是startActivity被呼叫的時刻。

  • 第一種狀況代表正常的情形,主執行緒中只有和startActivity相關的系統訊息被處理,而且處理每個訊息所花費的時間都在合理範圍內。所以這個頁面可以滿足秒開。
  • 第二種情況代表一個異常的情形,雖然主執行緒處理的訊息都是系統訊息,但是某一個或某幾個訊息的處理時間超出了合理值,導致頁面不能秒開。
  • 第三種情況代表另一種異常的情形,在系統訊息中混入了App自己的訊息,主執行緒不僅要處理系統訊息,還要處理App自己的訊息,結果就是總的啟動時間要額外加上App訊息的處理時間,導致頁面不能秒開。 實際情況中還有可能會出現既有系統訊息處理時間過長同時也混有App自己的訊息的情形.

秒開優化

瞭解了影響秒開的因素之後,我們只要有辦法能監測主執行緒中每個訊息處理時間,我們就能定位到造成頁面卡慢的原因,然後再做優化。 幸好Android工程師為我們在Looper中預留了打log的位置。

public static void loop() {
        final Looper me = myLooper();
        ...
        final MessageQueue queue = me.mQueue;
        ...
        for (;;) {
            Message msg = queue.next(); // might block
           ...
            Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            msg.target.dispatchMessage(msg);

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
           ...
            msg.recycleUnchecked();
        }
    }

public void setMessageLogging(@Nullable Printer printer) {
        mLogging = printer;
    }
複製程式碼

可見在訊息被處理的開始和處理結束之後都會列印log。 你只需要在程式碼中呼叫Looper.setMessageLogging()設定一下就好。

Looper.getMainLooper().setMessageLogging(new Printer() {
                @Override
                public void println(String s) {   
                    Log.v("debug", s);
                }
            });
複製程式碼

編譯執行你的程式,你會在logcat輸出看到類似這樣的log:

Message logging
每行 “>>>>> Dispatching to”開頭的log代表一個訊息即將開始被處理;緊接著下一行“<<<<< Finished to”開頭的log代表這一訊息處理完畢。通過這些log你可以知道所有被主執行緒處理的訊息,並可以根據開始結束的時間差知道每個訊息消耗的時間。有了這些資訊你可以找到導致你的app卡慢的訊息,然後進一步去debug問題。

在你啟動一個新的Activity的時候你可以觀測這樣的log輸出,看看裡面有沒有處理時間比較長的訊息,或者看看裡面有沒有App自己的訊息被處理,如果有的話,這些都是需要優化的點。

然而直接看log的缺點是這樣的log會比較多,而且並不容易定位啟動Activity的開始和結束時間點,另外每個訊息處理的時間也要自己計算,並不是十分直觀。

StallBuster

為了方便的進行秒開優化,我做了個工具叫StallBuster來協助定位Activity秒開失敗的原因。

整合StallBuster非常簡單,只需要兩步就可以了

  1. 新增對StallBuster的依賴
dependencies {
    compile 'com.github.zhangjianli:stallbuster:1.1'
}
複製程式碼
  1. 在你的App的Application中新增以下程式碼
public class YourApplication extends Application {
    @Override
    public void onCreate() {
        StallBuster.getInstance().init(this);
        super.onCreate();
    }
}
複製程式碼

這樣就可以了,編譯執行你的App。在你的App中開啟新的Activity,StallBuster會發出一個Notification。告訴你剛啟動這個Activity花了多少毫秒

notification
點選這個Notification就會開啟StallBuster的歷史記錄頁面。
records
這個頁面按照時間順序列出了你的App啟動每個Activity的歷史記錄。每條記錄最左邊是啟動所花費的時間。綠色代表所費時間符合秒開要求;紅色代表時間太長。需要關注。右邊是這條記錄對應的Activity名稱。點選某條記錄就會進入詳情頁。

詳情頁
在詳情頁裡你可以看到啟動這個Activity的過程中主執行緒處理過的訊息。上方的核取方塊可以過濾執行時間比較短的訊息,方便定位問題。

對於每條記錄,首先顯示的是這條訊息開始被處理的時間戳。然後是cost欄位,表示處理這條訊息花了多長時間。正常情況下是字型是黑色的;如果處理時間過長,則顯示為紅色。表明這裡可能是我們需要優化的地方。

接下來是target欄位,對應的是這個訊息是被哪個Handler處理的。Android系統的Handler會顯示為黑色;App自己的Handler會顯示為紅色,表明這個訊息不應該在啟動Activity的時候出現,這裡也可能是需要優化的地方。

例如上圖中第一條記錄,.MainActivity$StallHandler處理這個訊息花費了142ms。這會使啟動SubActivity的時間至少延長了142ms。而這個Handler是App自己的Handler。我們需要除錯程式碼使得在啟動這個Activity的時候確保不會有來自這個Handler的訊息,142ms的時間就會節省下來。

最後一個欄位是message或者callback。對應的是Message中的what或callback。有了這些資訊我們就能很方便的定位主執行緒中影響秒開的訊息,進而優化我們的App。

StallBuster就給大家介紹到這裡,希望StallBuster能幫到你。如果大家有任何建議或者問題請給我留言。

總結

App秒開是是一項非常重要的效能指標。秒開的優化是個複雜的工作,有很多因素會影響App秒開。其中比較重要的一個因素是啟動Activity的時候主執行緒的訊息處理情況。在啟動Activity過程中需要避免訊息處理時間過長,也要避免在此期間有App自己的訊息需要處理。優化的關鍵點是要定位到主執行緒中的耗時操作,我們可以通過列印分析主執行緒的訊息處理log來定位,但這種方式並不是很直觀方便。這時可以使用StallBuster幫助你快速定位秒開問題點,讓秒開優化變的更加簡單。

相關文章