首頁 > 軟體

Android 勇闖高階效能優化之啟動優化篇

2021-10-23 22:00:54

🔥 背景

使用者不會在乎你的專案是不是過大,裡面是不是有很多初始化的邏輯。他只在乎你-慢了。

所以咱們這篇文章有兩個目的:

啟動速度提升(使用者眼中的大神就是你)

優化程式碼邏輯和規範(別讓自己成為繼任者中的XX)

今天咱們就來了解一下應用啟動內部機制和啟動速度優化。

🔥 啟動內部機制

應用有三種啟動狀態:

  • 冷啟動;
  • 溫啟動;
  • 熱啟動。

💥 冷啟動

冷啟動是指應用從頭開始:冷啟動發生在裝置啟動後第一次啟動應用程式 (Zygote>fork>app) ,或系統關閉應用程式後。

在冷啟動開始時,系統有三個任務。 這些任務是:

  • 載入和啟動應用程式。
  • 啟動後立即顯示應用程式的空白啟動頁面。
  • 建立應用程式程序。

一旦系統建立了應用程式程序,應用程式程序就負責接下來的階段:

  • 建立應用的實體。
  • 啟動主執行緒。
  • 建立主頁面。
  • 繪製頁面上的View。
  • 佈局頁面。
  • 執行首次的繪製。

如下圖:

  • Displayed Time:初始顯示時間
  • reportFullyDrawn():完全顯示的時間

注意:在建立 Application 和建立 Activity 期間可能會出現效能問題。

🌀 建立 Application

當應用程式啟動時,空白啟動頁面保留在螢幕上,直到系統首次完成應用程式的繪製。

如果你重寫了Application.onCreate(),系統將呼叫Application 上的onCreate()方法。之後,應用程式生成主執行緒,也稱為UI執行緒,並將建立主Activity的任務交給它。

🌀 建立Activity

應用程序建立你的Activity後,Activity會執行以下操作:

  • 初始化值。
  • 呼叫建構函式。
  • 呼叫 Activity 當前生命週期狀態的回撥方法,如 Activity.onCreate()。

注意:onCreate() 方法對載入時間的影響最大,因為它執行開銷最高的工作:載入UI的佈局和渲染,以及初始化Activity執行所需的物件。

💥 熱啟動

熱啟動時,系統將應用從後臺拉回前臺,應用程式的 Activity 在記憶體中沒有被銷燬,那麼應用程式可以避免重複物件初始化,UI的佈局和渲染。

如果 Activity 被銷燬則需要重新建立。

和冷啟動的區別: 不需要建立 Application。

💥 溫啟動

溫啟動介於冷啟動和熱啟動中間吧。例如:

使用者按返回鍵退出應用,然後重新啟動。程序可能還沒有被殺死,但應用必須通過呼叫onCreate()重新建立 Activity。

系統回收了應用的記憶體,然後使用者重新執行應用。應用程序和Activity都需要重新啟動。

咱們看看他們共同消耗多長時間。

🔥 查詢的啟動時間

💥 初始顯示時間(Time to initial display)

在 Android 4.4(API 級別 19)及更高版本中,logcat 包含一個輸出行,其中包含一個名為 Displayed 的值。 此值表示啟動流程和完成在螢幕上繪製相應活動之間經過的時間量。 經過的時間包含以下事件序列:

  • 啟動程序。
  • 初始化物件。
  • 建立並初始化Activity。
  • 載入佈局。
  • 第一次繪製你的應用程式。

注意這裡檢視紀錄檔需要如下操作:

報告的紀錄檔行類,如下圖:

//冷啟動
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +1s355ms
//溫啟動(程序被殺死)
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +1s46ms
//熱啟動
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +289ms
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +253ms

圖例講解:

第一個時間,冷啟動時間:+1s355ms;

然後我們在後臺殺死程序,再次啟動應用;

第二個時間,溫啟動時間:+1s46ms;

這裡咱們在後臺殺死程序所以:應用程序和Activity需要重新啟動。

第三個時間:熱啟動時間:+289ms 和 +253ms;

按返回鍵,僅退出activity。所以耗時比較短。

當然整體看這個應用開啟時間並不長,因為 Demo 的 Application 和 Activity 都沒有進行太多的操作。

💥 完全顯示時間(Time to full display)

你可以使用 reportFullyDrawn() 方法來測量應用程式啟動和所有資源和檢視層次結構的完整顯示之間經過的時間。在應用程式執行延遲載入的情況下,這可能很有價值。在延遲載入中,應用程式不會阻止視窗的初始繪製,而是非同步載入資源並更新檢視層次結構。

這裡我在Activity.onCreate()中加了個工作執行緒。並在裡面呼叫reportFullyDrawn() 方法。程式碼如下:

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.e(this.getClass().getName(), "onCreate");
    setContentView(R.layout.activity_main);
    ...
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(3000);
                reportFullyDrawn();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();
}

報告的紀錄檔行類,如下圖:

I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +3s970ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +3s836ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +3s107ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +3s149ms

圖例講解:

然後你會發現介面出來好一會才打這個紀錄檔。看到這裡我覺得好多人已經知道怎麼去優化啟動速度了。

🔥 效能遲緩分析

看到上面的實驗其實三種啟動情況,受我們影響的方面在於 application 和 activity 。

💥 Application 初始化

當你的程式碼覆蓋 Application 物件並在初始化該物件時執行繁重的工作或複雜的邏輯時,啟動效能可能會受到影響。 產生的原因包括:

  • 應用程式的初始onCreate()函數。如:執行了不需要立即執行的初始化。
  • 應用程式初始化的任何全域性單例物件。如:一些不必要的物件。
  • 可能發生的任何磁碟I/O、反序列化或緊密迴圈。

解決方案

無論問題在於不必要的初始化還是磁碟I/O,解決方案都是延遲初始化。換句話說,你應該只初始化立即需要的物件。不要建立全域性靜態物件,而是轉向單例模式,應用程式只在第一次需要時初始化物件。

此外,考慮使用依賴注入框架(如Hilt)

💥 Activity初始化

活動建立通常需要大量高開銷工作。 通常,有機會優化這項工作以實現效能改進。

產生的原因包括:

  • 載入大型或複雜的佈局。
  • 阻止在磁碟或網路 I/O 上繪製螢幕。
  • 載入和解碼Bitmap。
  • VectorDrawable 物件。
  • Activity 初始化任何全域性單例物件。
  • 所有資源初始化。

解決方案如下。

🌀 佈局優化

  • 通過減少冗餘或巢狀佈局來扁平化檢視層次結構。
  • 佈局複用(< include/>和 < merge/> )
  • 使用ViewStub,不載入在啟動期間不需要可見的 UI 部分。

🌀 程式碼優化

  • 不必要的初始化還是磁碟I/O,延遲初始化
  • 資源初始化分類,以便應用程式可以在不同的執行緒上延遲執行。
  • 動態載入資源和Bitmap

關於這兩塊的優化後續會有單獨的文章去寫。

🔥 阻塞實驗

💥 Application 阻塞 2秒, Activity 阻塞 2秒

🌀 SccApp.class

public class SccApp extends Application {
    @RequiresApi(api = Build.VERSION_CODES.P)
    @Override
    public void onCreate() {
        super.onCreate();
        String name = getProcessName();
        MLog.e("ProcessName:"+name);
        getProcessName("com.scc.demo");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

🌀 MainActivity.class

public  class MainActivity extends ActivityBase implements View.OnClickListener {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.e(this.getClass().getName(), "onCreate");
        setContentView(R.layout.activity_main);
        ...
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                    reportFullyDrawn();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

報告的紀錄檔,如下:

//冷啟動
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +5s458ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +8s121ms
//溫啟動(程序被殺死)
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +5s227ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +7s935ms
//熱啟動
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +2s304ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +5s189ms
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +2s322ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +5s169ms

💥 將Appliacation 和Activity阻塞的2秒都放在工作執行緒去操作

這個就是把程式碼放在如下程式碼中執行即可,就不全部貼出來了。

        new Thread(new Runnable() {
            @Override
            public void run() {
                ...
            }
        }).start();

執行結果如下:

//冷啟動
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +1s227ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +3s957ms
//溫啟動(程序被殺死)
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +1s83ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +3s828ms
//熱啟動
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +324ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +3s169ms
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +358ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +3s207ms

🔥 APP 啟動黑/白屏

Android 應用啟動時,尤其是大型應用, 經常出現幾秒鐘的黑畫面或白屏,黑畫面或白屏取決於主介面 Activity 的主題風格。

💥 優雅的解決黑白屛

Android 應用啟動時很多大型應用都會有一個廣告(圖片及視訊)頁或閃屏頁(2-3S)。這並不是開發者想要放上去的,而是為了避免上述啟動白屏導致使用者體很差。當然你可以珍惜這2-3秒做一個非同步載入或者請求。

寫到這裡。應用啟動模式、啟動時間、啟動速度優化算是完事了。當然後面如果有更好的優化方案還會繼續補充。

到此這篇關於Android 勇闖高階效能優化之啟動優化篇的文章就介紹到這了,更多相關Android 啟動優化內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com