一觸即發 App啓動優化最佳實踐

一觸即發 App啓動優化最佳實踐

文中的不少圖都是Google性能優化指南第六季中的一些截圖html

Google給出的優化指南來鎮樓
https://developer.android.com...android

閃屏定義

Android官方的性能優化典範,從第六季開始,發起了一系列針對App啓動的優化實踐,地址以下:
https://www.youtube.com/watch...shell

可想而知,App的啓動性能是很是重要的。同時,Google針對App閃屏,也給出了很是詳細的設計定義,以下所示。性能優化

https://material.google.com/p...微信

其實最先的時候,閃屏是用來在App未徹底啓動的時候,讓用戶不至於困惑App是否啓動而加入的一個設計。而如今的不少App,基本上都把閃屏當作一個廣告、宣傳的頁面了,貌似已經失去了本來的意義,但閃屏,無論怎麼說,在一個App啓動的時候,都是很是重要的,設計的事情,交給UE吧,開發要作的,就是讓App的啓動體驗,作到最好。網絡

App啓動流程

App啓動的整個過程,能夠分解成下面幾個過程:多線程

  1. 用戶在Launcher上點擊App Iconapp

  2. 系統爲App建立進程,顯示啓動窗口框架

  3. App在進程中建立本身的組件異步

這個過程能夠用下面這幅圖來描述:

而咱們可以優化的,也就是下面Application的建立部分,系統的進程分配以及一些窗口切換的動畫效果等,都是跟ROM相關的,咱們沒法處理。因此,咱們須要把重點放到Application的建立過程。

上面是官方的說明,下面咱們用更加通俗的語言來解釋一遍。

當用戶點擊桌面icon的時候,系統準備好了,給App分配進程空間,就好像去酒店開房,可是你又不能直接進入房間,你得坐電梯去房間,那麼你坐電梯的這個時間,實際上就是系統的準備時間,那麼系統的這個準備時間通常來講不會太長,但假如的開的是一個總統套房呢,系統就得花很多時間來打理,因此係統給全部用戶都準備了一個過渡界面,這個界面,就是啓動時的黑屏白屏,也就是你坐電梯裏面看的小廣告,看完小廣告,你就到房間了,而後你想幹嗎均可以了,這個想幹嗎的速度,就徹底取決於你開門的速度了,你門開得快,天然那啥快,因此這裏是開發者能夠優化的地方,有些開發者掏個鑰匙要好幾秒,有的只要幾百毫秒,徹底影響了後面那啥的效率。

那麼通常來講,故事到這裏就結束了,可是,系統,也就是這個酒店,並非一個野雞酒店,他也想盡可能作得讓顧客滿意,這樣纔會有回頭客啊,因此,酒店作了一個優化,可讓每一個顧客本身定義在坐電梯的時候想看什麼!也就是說,系統在加載App的時候,首先是加載了資源文件,這裏就包括了要啓動的Activity的Theme,而這個Theme呢,是能夠自定義的,也就是顧客在坐電梯時想看的東西,而不是千篇一概的白屏或者黑屏,他能夠定製不少東西,例如ActionBar、背景、StatBar等等。

啓動時間的測量

關於Activity啓動時間的定義

對於Activity來講,啓動時,首先執行的是onCreate()、onStart()、onResume()這些生命週期函數,但即便這些生命週期方法回調結束了,應用也不算已經徹底啓動,還須要等View樹所有構建完畢,通常認爲,setContentView中的View所有顯示結束了,算做是應用徹底啓動了。

Display Time

從API19以後,Android在系統Log中增長了Display的Log信息,經過過濾ActivityManager以及Display這兩個關鍵字,能夠找到系統中的這個Log:

$ adb logcat | grep 「ActivityManager」
ActivityManager: Displayed com.example.launcher/.LauncherActivity: +999ms

抓到的Log如圖所示:

那麼這個時間,其實是Activity啓動,到Layout所有顯示的過程,可是要注意,這裏並不包括數據的加載,由於不少App在加載時會使用懶加載模式,即數據拉取後,再刷新默認的UI。

reportFullyDrawn

前面說了,系統日誌中的Display Time只是佈局的顯示時間,並不包括一些數據的懶加載等消耗的時間,因此,系統給咱們定義了一個相似的『自定義上報時間』——reportFullyDrawn。

一樣是借用Google的一張圖來講明:

reportFullyDrawn是由咱們本身調用的,通常在數據所有加載完畢後,手動調用,這樣就會在Log中增長一條日誌:

$ adb logcat | grep 「ActivityManager」
ActivityManager: Displayed com.example.launcher/. LauncherActivity: +999ms
ActivityManager: Fully drawn com.example.launcher/. LauncherActivity: +1s999ms

通常來講,使用的場景以下:

public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Void> {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    public void onLoadFinished(Loader<Void> loader, Void data) {
        // 加載數據
        // ……
        // 上報reportFullyDrawn
        reportFullyDrawn();
    }

    @Override
    public Loader<Void> onCreateLoader(int id, Bundle args) {
        return null;
    }

    @Override
    public void onLoaderReset(Loader<Void> loader) {

    }
}

可是要注意,這個方式須要API19+,因此,這裏須要對SDK版本進行判斷。

計算啓動時間——ADB

經過ADB命令能夠統計應用的啓動時間,指令以下所示:

➜  ~  adb shell am start -W com.xys.preferencetest/.MainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.xys.preferencetest/.MainActivity }
Status: ok
Activity: com.xys.preferencetest/.MainActivity
ThisTime: 1047
TotalTime: 1047
WaitTime: 1059
Complete

該指令一共給出了三個時間:

  • ThisTime:最後一個啓動的Activity的啓動耗時

  • TotalTime:本身的全部Activity的啓動耗時

  • WaitTime: ActivityManagerService啓動App的Activity時的總時間(包括當前Activity的onPause()和本身Activity的啓動)

這三個時間不是很好理解,咱們能夠把整個過程分解

1.上一個Activity的onPause()——2.系統調用AMS耗時——3.第一個Activity(也許是閃屏頁)啓動耗時——4.第一個Activity的onPause()耗時——5.第二個Activity啓動耗時

那麼,ThisTime表示5(最後一個Activity的啓動耗時)。TotalTime表示3.4.5總共的耗時(若是啓動時只有一個Activity,那麼ThisTime與TotalTime應該是同樣的)。WaitTime則表示全部的操做耗時,即1.2.3.4.5全部的耗時。

每次給出的時間可能並不同,並且應用從首次安裝啓動到後面每次正常啓動,時間都會不一樣,區別於系統是否要分配進程空間。

計算啓動時間——Screen Record

經過錄屏進行啓動的分析,是一個很好的辦法,在API21+,Android給咱們提供了一個更加方便、準確的方式:

➜  ~ adb shell screenrecord --bugreport /sdcard/test.mp4

Android在screenrecord中新增了一個參數——bugreport,那麼加了這個參數以後,錄製出來的視頻,在左上角就會增長一行數字的顯示,如圖所示。

在視頻開始前,會顯示設備信息和一些參數:

視頻開始後,左上角會有一行數字:

例如圖中的:15:31:22.261 f=171(0)

其中,前面的4個數字,就是時間戳,即15點31分22秒261,f=後面的數字是當前的幀數,注意,不是幀率,而是表明當前是第幾幀,括號中的數字,表明的是『Dropped frames
count』,即掉幀數。

有了這個東西,再結合視頻就能夠很是清楚的看見這些信息了。

啓動時間的調試

模擬啓動延時

在測試的時候,咱們能夠經過下面的方式來進行啓動的延遲模擬:

SystemClock.sleep(2000)

或者直接經過:

try {
    Thread.sleep(2000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

或者經過:

new Handler().postDelayed(new Runnable() {
    @Override
    public void run() {
        // Delay
    }

}, 2000);

這些方案均可以進行啓動延遲的模擬。

強制冷啓動

在『開發者選項』中的Background Process Limit中設置爲No Background Processes

優化點

Static Block

不少代碼中的Static Block,都是作一些初始化工做,特別是ContentProvider中在Static Block中初始化一些UriMatcher,這些東西能夠作成懶加載模式。

Application

Application是程序的主入口,特別是不少第三方SDK都會須要在Application的onCreate裏面作不少初始化操做,不得不說,各類第三方SDK,都特別喜歡這個『兵家必爭之地』,再加上本身的一些庫的初始化,會讓整個Application不堪重負。

優化的方法,無非是經過如下幾個方面:

  • 延遲初始化

  • 後臺任務

  • 界面預加載

阻塞

阻塞有不少種狀況,例如磁盤IO阻塞(讀寫文件、SharedPerfences)、網絡阻塞(如今應該不會了)以及高CPU佔用的代碼(加解密、渲染、解析等等)。

View層級

見《Android羣英傳》

耗時方法

經過使用TraceView && Systrace && Method Tracing工具來進行排查,見《Android羣英傳:神兵利器》

App啓動優化的通常過程

  1. 經過TraceView、Systrace來分析耗時的方法與組件。

  2. 梳理啓動加載的每個庫、組件。

  3. 將梳理出來的庫,按功能和需求進行劃分,設計該庫的啓動時機。

  4. 與交互溝通,設計啓動畫面,按前文方法進行優化。

解決方案

Theme

當系統加載一個Activity的時候,onCreate()是一個耗時過程,那麼在這個過程當中,系統爲了讓用戶能有一個比較好的體驗,實際上會先繪製一些初始界面,相似於PlaceHolder。

系統首先會讀取當前Activity的Theme,而後根據Theme中的配置來繪製,當Activity加載完畢後,纔會替換爲真正的界面。因此,Google官方提供的解決方案,就是經過android:windowBackground屬性,來進行加載前的配置,同時,這裏不只能夠配置顏色,還能配置圖片,例如,咱們可使用一個layer-list來做爲android:windowBackground要顯示的圖:

start_window.xml

<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
            android:opacity="opaque">
    <item android:drawable="@android:color/darker_gray"/>
    <item>
        <bitmap
            android:gravity="center"
            android:src="@mipmap/ic_launcher"/>
    </item>
</layer-list>

能夠看見,這裏經過layer-list來實現圖片的疊加,讓開發者能夠自由組合。

配置中的android:opacity="opaque"參數是爲了防止在啓動的時候出現背景的閃爍。

接下來能夠設置一個新的Style,這個Style就是Activity預加載的Style。

<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>
    </style>

    <style name="StartStyle" parent="AppTheme">
        <item name="android:windowBackground">@drawable/start_window</item>
    </style>
</resources>

OK,下面在Mainifest中給Activity指定須要預加載的Style:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.xys.startperformancedemo">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".MainActivity"
            android:theme="@style/StartStyle">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

這裏須要注意下,必定是Activity的Theme,而不是Application的Theme。

最後,咱們在Activity加載真正的界面以前,將Theme設置回正常的Theme就行了:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        setTheme(R.style.AppTheme);
        super.onCreate(savedInstanceState);
        SystemClock.sleep(2000);
        setContentView(R.layout.activity_main);
    }
}

在這個Activity中,我使用SystemClock.sleep(2000),模擬了一個Activity加載的耗時過程,在super.onCreate(savedInstanceState)調用前,將主題從新設置爲原來的主題。

經過這種方式設置的效果以下:

啓動的時候,會先展現一個畫面,這個畫面就是系統解析到的Style,等Activity加載徹底完畢後,纔會加載Activity的界面,而在Activity的界面中,咱們將主題從新設置爲正常的主題,從而達到一個友好的啓動體驗,這種方式其實並無真正的加速啓動過程,而是經過交互體驗來優化了展現的效果。

異步初始化

這個很簡單,就是讓App在onCreate裏面儘量的少作事情,而利用手機的多核特性,儘量的利用多線程,例如一些第三方框架的初始化,若是能放線程,就儘可能的放入線程中,最簡單的,你能夠直接new Thread(),固然,你也能夠經過公共的線程池來進行異步的初始化工做,這個是最可以壓縮啓動時間的方式

延遲初始化

延遲初始化並非減小了啓動時間,而是讓耗時操做讓位、讓資源給UI繪製,將耗時的操做延遲到UI加載完畢後,因此,這裏建議經過mDecoView.post方法,來進行延遲加載,代碼以下:

getWindow().getDecorView().post(new Runnable() {

  @Override public void run() {
    ……
  }
});

咱們的ContentView就是經過mDecoView.addView加入到根佈局的,因此,經過這種方式,可讓延遲加載的內容,在ContentView初始化完畢後,再進行執行,保證了UI繪製的流暢性。

IntentService

IntentService是繼承於Service並處理異步請求的一個類,在IntentService的內部,有一個工做線程來處理耗時操做,啓動IntentService的方式和啓動傳統Service同樣,同時,當任務執行完後,IntentService會自動中止,而不須要去手動控制。

public class InitIntentService extends IntentService {

    private static final String ACTION = "com.xys.startperformancedemo.action";

    public InitIntentService() {
        super("InitIntentService");
    }

    public static void start(Context context) {
        Intent intent = new Intent(context, InitIntentService.class);
        intent.setAction(ACTION);
        context.startService(intent);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        SystemClock.sleep(2000);
        Log.d(TAG, "onHandleIntent: ");
    }
}

咱們將耗時任務丟到IntentService中去處理,系統會自動開啓線程去處理,同時,在任務結束後,還能本身結束Service,多麼的人性化!OK,只須要在Application或者Activity的onCreate中去啓動這個IntentService便可:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    InitIntentService.start(this);
}

最後不要忘記在Mainifest註冊Service。

使用ActivityLifecycleCallbacks

Framework提供的這個方法能夠監控到全部Activity的生命週期,在這裏,咱們就能夠經過onActivityCreated這樣一個回調,來將一些UI相關的初始化操做放到這裏,同時,經過unregisterActivityLifecycleCallbacks來避免重複的初始化。同時,這裏onActivityCreated回調的參數Bundle,能夠用來區別是不是被系統所回收的Activity。

public class MainApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        // 初始化基本內容
        // ……
        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                unregisterActivityLifecycleCallbacks(this);
                // 初始化UI相關的內容
                // ……
            }

            @Override
            public void onActivityStarted(Activity activity) {
            }

            @Override
            public void onActivityResumed(Activity activity) {
            }

            @Override
            public void onActivityPaused(Activity activity) {
            }

            @Override
            public void onActivityStopped(Activity activity) {
            }

            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
            }

            @Override
            public void onActivityDestroyed(Activity activity) {
            }
        });
    }
}

資源優化

有幾個方面,一個天然是優化佈局、佈局層級,一個是優化資源,儘量的精簡資源、避免垃圾資源,這些能夠經過混淆和tinyPNG這些工具來實現。

甩鍋方案

下面是兩種不一樣的方案,都是在Style中進行配置:

<item name="android:windowDisablePreview">true</item>

<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>

咱們先來看看這樣作的效果:

設置效果相似,即經過取消、透明化系統的統一的加載頁面來達到啓動的『加速』,實際上,是一個『甩鍋』的過程。強烈建議開發者不要經過這種方式去作『所謂的啓動加速』,這種方式雖然看上去本身的App啓動很是快,瞬間就完成了,但實際上,是將真正的啓動界面給隱藏了。

系統說:這鍋,咱們不背!

無解

對應5.0如下的65535問題,目前只能經過Multidex來進行處理,而在5.0如下的機器上,系統在加載前的合併Dex的過程,有可能很是長,這也是暫時無解的問題,只能但願後面Multidex進行優化。

OK,App的啓動優化基本如上,其重點過程,依然是分析耗時的操做,以及如何設計合理的啓動順序,但願各位可以經過文中介紹的方式來進行App的啓動優化。

更多內容,請關注個人微信公衆號:

相關文章
相關標籤/搜索