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

Dreamtale 8年前發布 | 6K 次閱讀 安卓開發 Android開發 移動開發

閃屏定義

Android官方的性能優化典范,從第六季開始,發起了一系列針對App啟動的優化實踐,地址如下:

https://www.油Tube.com/watch?v=Vw1G1s73DsY&index=74&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE

可想而知,App的啟動性能是非常重要的。同時,Google針對App閃屏,也給出了非常詳細的設計定義,如下所示。

https://material.google.com/patterns/launch-screens.html

其實最早的時候,閃屏是用來在App未完全啟動的時候,讓用戶不至于困惑App是否啟動而加入的一個設計。而現在的很多App,基本上都把閃屏當做一個廣告、宣傳的頁面了,貌似已經失去了原本的意義,但閃屏,不管怎么說,在一個App啟動的時候,都是非常重要的,設計的事情,交給UE吧,開發要做的,就是讓App的啟動體驗,做到最好。

App啟動流程

App啟動的整個過程,可以分解成下面幾個過程:

  1. 用戶在Launcher上點擊App Icon
  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) {

}

}</code></pre>

但是要注意,這個方式需要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);</code></pre>

這些方案都可以進行啟動延遲的模擬。

強制冷啟動

在『開發者選項』中的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></code></pre>

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

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="

<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></code></pre>

這里需要注意下,一定是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);
}

}</code></pre>

在這個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() { …… } });</code></pre>

我們的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: ");
}

}</code></pre>

我們將耗時任務丟到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) {
        }
    });
}

}</code></pre>

資源優化

有幾個方面,一個自然是優化布局、布局層級,一個是優化資源,盡可能的精簡資源、避免垃圾資源,這些可以通過混淆和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的啟動優化。

 

來自:http://www.androidchina.net/5932.html

 

 本文由用戶 Dreamtale 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!