一觸即發(fā) App啟動(dòng)優(yōu)化最佳實(shí)踐

一觸即發(fā) App啟動(dòng)優(yōu)化最佳實(shí)踐

文中的很多圖都是Google性能優(yōu)化指南第六季中的一些截圖

Google給出的優(yōu)化指南來鎮(zhèn)樓
https://developer.android.com/topic/performance/launch-time.html

閃屏定義

Android官方的性能優(yōu)化典范,從第六季開始,發(fā)起了一系列針對(duì)App啟動(dòng)的優(yōu)化實(shí)踐,地址如下:
https://www.youtube.com/watch?v=Vw1G1s73DsY&index=74&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE

可想而知,App的啟動(dòng)性能是非常重要的。同時(shí),Google針對(duì)App閃屏,也給出了非常詳細(xì)的設(shè)計(jì)定義,如下所示。

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

1.png

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

App啟動(dòng)流程

App啟動(dòng)的整個(gè)過程,可以分解成下面幾個(gè)過程:

  1. 用戶在Launcher上點(diǎn)擊App Icon
  2. 系統(tǒng)為App創(chuàng)建進(jìn)程,顯示啟動(dòng)窗口
  3. App在進(jìn)程中創(chuàng)建自己的組件

這個(gè)過程可以用下面這幅圖來描述:

2.png

而我們能夠優(yōu)化的,也就是下面Application的創(chuàng)建部分,系統(tǒng)的進(jìn)程分配以及一些窗口切換的動(dòng)畫效果等,都是跟ROM相關(guān)的,我們無法處理。所以,我們需要把重點(diǎn)放到Application的創(chuàng)建過程。

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

當(dāng)用戶點(diǎn)擊桌面icon的時(shí)候,系統(tǒng)準(zhǔn)備好了,給App分配進(jìn)程空間,就好像去酒店開房,但是你又不能直接進(jìn)入房間,你得坐電梯去房間,那么你坐電梯的這個(gè)時(shí)間,實(shí)際上就是系統(tǒng)的準(zhǔn)備時(shí)間,那么系統(tǒng)的這個(gè)準(zhǔn)備時(shí)間一般來說不會(huì)太長,但假如的開的是一個(gè)總統(tǒng)套房呢,系統(tǒng)就得花不少時(shí)間來打理,所以系統(tǒng)給所有用戶都準(zhǔn)備了一個(gè)過渡界面,這個(gè)界面,就是啟動(dòng)時(shí)的黑屏\白屏,也就是你坐電梯里面看的小廣告,看完小廣告,你就到房間了,然后你想干嘛都可以了,這個(gè)想干嘛的速度,就完全取決于你開門的速度了,你門開得快,自然那啥快,所以這里是開發(fā)者可以優(yōu)化的地方,有些開發(fā)者掏個(gè)鑰匙要好幾秒,有的只要幾百毫秒,完全影響了后面那啥的效率。

那么一般來說,故事到這里就結(jié)束了,但是,系統(tǒng),也就是這個(gè)酒店,并不是一個(gè)野雞酒店,他也想盡量做得讓顧客滿意,這樣才會(huì)有回頭客啊,所以,酒店做了一個(gè)優(yōu)化,可以讓每個(gè)顧客自己定義在坐電梯的時(shí)候想看什么!也就是說,系統(tǒng)在加載App的時(shí)候,首先是加載了資源文件,這里就包括了要啟動(dòng)的Activity的Theme,而這個(gè)Theme呢,是可以自定義的,也就是顧客在坐電梯時(shí)想看的東西,而不是千篇一律的白屏或者黑屏,他可以定制很多東西,例如ActionBar、背景、StatBar等等。

啟動(dòng)時(shí)間的測(cè)量

關(guān)于Activity啟動(dòng)時(shí)間的定義

對(duì)于Activity來說,啟動(dòng)時(shí),首先執(zhí)行的是onCreate()、onStart()、onResume()這些生命周期函數(shù),但即使這些生命周期方法回調(diào)結(jié)束了,應(yīng)用也不算已經(jīng)完全啟動(dòng),還需要等View樹全部構(gòu)建完畢,一般認(rèn)為,setContentView中的View全部顯示結(jié)束了,算作是應(yīng)用完全啟動(dòng)了。

Display Time

從API19之后,Android在系統(tǒng)Log中增加了Display的Log信息,通過過濾ActivityManager以及Display這兩個(gè)關(guān)鍵字,可以找到系統(tǒng)中的這個(gè)Log:

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

抓到的Log如圖所示:

3.png

那么這個(gè)時(shí)間,實(shí)際上是Activity啟動(dòng),到Layout全部顯示的過程,但是要注意,這里并不包括數(shù)據(jù)的加載,因?yàn)楹芏郃pp在加載時(shí)會(huì)使用懶加載模式,即數(shù)據(jù)拉取后,再刷新默認(rèn)的UI。

reportFullyDrawn

前面說了,系統(tǒng)日志中的Display Time只是布局的顯示時(shí)間,并不包括一些數(shù)據(jù)的懶加載等消耗的時(shí)間,所以,系統(tǒng)給我們定義了一個(gè)類似的『自定義上報(bào)時(shí)間』——reportFullyDrawn。

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

4.png

reportFullyDrawn是由我們自己調(diào)用的,一般在數(shù)據(jù)全部加載完畢后,手動(dòng)調(diào)用,這樣就會(huì)在Log中增加一條日志:

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

一般來說,使用的場(chǎng)景如下:

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) {
        // 加載數(shù)據(jù)
        // ……
        // 上報(bào)reportFullyDrawn
        reportFullyDrawn();
    }

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

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

    }
}

但是要注意,這個(gè)方式需要API19+,所以,這里需要對(duì)SDK版本進(jìn)行判斷。

計(jì)算啟動(dòng)時(shí)間——ADB

通過ADB命令可以統(tǒng)計(jì)應(yīng)用的啟動(dòng)時(shí)間,指令如下所示:

?  ~  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

該指令一共給出了三個(gè)時(shí)間:

  • ThisTime:最后一個(gè)啟動(dòng)的Activity的啟動(dòng)耗時(shí)
  • TotalTime:自己的所有Activity的啟動(dòng)耗時(shí)
  • WaitTime: ActivityManagerService啟動(dòng)App的Activity時(shí)的總時(shí)間(包括當(dāng)前Activity的onPause()和自己Activity的啟動(dòng))

這三個(gè)時(shí)間不是很好理解,我們可以把整個(gè)過程分解

1.上一個(gè)Activity的onPause()——2.系統(tǒng)調(diào)用AMS耗時(shí)——3.第一個(gè)Activity(也許是閃屏頁)啟動(dòng)耗時(shí)——4.第一個(gè)Activity的onPause()耗時(shí)——5.第二個(gè)Activity啟動(dòng)耗時(shí)

那么,ThisTime表示5(最后一個(gè)Activity的啟動(dòng)耗時(shí))。TotalTime表示3.4.5總共的耗時(shí)(如果啟動(dòng)時(shí)只有一個(gè)Activity,那么ThisTime與TotalTime應(yīng)該是一樣的)。WaitTime則表示所有的操作耗時(shí),即1.2.3.4.5所有的耗時(shí)。

每次給出的時(shí)間可能并不一樣,而且應(yīng)用從首次安裝啟動(dòng)到后面每次正常啟動(dòng),時(shí)間都會(huì)不同,區(qū)別于系統(tǒng)是否要分配進(jìn)程空間。

計(jì)算啟動(dòng)時(shí)間——Screen Record

通過錄屏進(jìn)行啟動(dòng)的分析,是一個(gè)很好的辦法,在API21+,Android給我們提供了一個(gè)更加方便、準(zhǔn)確的方式:

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

Android在screenrecord中新增了一個(gè)參數(shù)——bugreport,那么加了這個(gè)參數(shù)之后,錄制出來的視頻,在左上角就會(huì)增加一行數(shù)字的顯示,如圖所示。

在視頻開始前,會(huì)顯示設(shè)備信息和一些參數(shù):

6.png

視頻開始后,左上角會(huì)有一行數(shù)字:

5.png

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

其中,前面的4個(gè)數(shù)字,就是時(shí)間戳,即15點(diǎn)31分22秒261,f=后面的數(shù)字是當(dāng)前的幀數(shù),注意,不是幀率,而是代表當(dāng)前是第幾幀,括號(hào)中的數(shù)字,代表的是『Dropped frames
count』,即掉幀數(shù)。

有了這個(gè)東西,再結(jié)合視頻就可以非常清楚的看見這些信息了。

啟動(dòng)時(shí)間的調(diào)試

模擬啟動(dòng)延時(shí)

在測(cè)試的時(shí)候,我們可以通過下面的方式來進(jìn)行啟動(dòng)的延遲模擬:

SystemClock.sleep(2000)

或者直接通過:

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

或者通過:

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

}, 2000);

這些方案都可以進(jìn)行啟動(dòng)延遲的模擬。

強(qiáng)制冷啟動(dòng)

在『開發(fā)者選項(xiàng)』中的Background Process Limit中設(shè)置為No Background Processes

7.png

優(yōu)化點(diǎn)

Static Block

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

Application

Application是程序的主入口,特別是很多第三方SDK都會(huì)需要在Application的onCreate里面做很多初始化操作,不得不說,各種第三方SDK,都特別喜歡這個(gè)『兵家必爭(zhēng)之地』,再加上自己的一些庫的初始化,會(huì)讓整個(gè)Application不堪重負(fù)。

優(yōu)化的方法,無非是通過以下幾個(gè)方面:

  • 延遲初始化
  • 后臺(tái)任務(wù)
  • 界面預(yù)加載

阻塞

阻塞有很多種情況,例如磁盤IO阻塞(讀寫文件、SharedPerfences)、網(wǎng)絡(luò)阻塞(現(xiàn)在應(yīng)該不會(huì)了)以及高CPU占用的代碼(加解密、渲染、解析等等)。

View層級(jí)

見《Android群英傳》

耗時(shí)方法

通過使用TraceView && Systrace && Method Tracing工具來進(jìn)行排查,見《Android群英傳:神兵利器》

App啟動(dòng)優(yōu)化的一般過程

  1. 通過TraceView、Systrace來分析耗時(shí)的方法與組件。
  2. 梳理啟動(dòng)加載的每一個(gè)庫、組件。
  3. 將梳理出來的庫,按功能和需求進(jìn)行劃分,設(shè)計(jì)該庫的啟動(dòng)時(shí)機(jī)。
  4. 與交互溝通,設(shè)計(jì)啟動(dòng)畫面,按前文方法進(jìn)行優(yōu)化。

解決方案

Theme

當(dāng)系統(tǒng)加載一個(gè)Activity的時(shí)候,onCreate()是一個(gè)耗時(shí)過程,那么在這個(gè)過程中,系統(tǒng)為了讓用戶能有一個(gè)比較好的體驗(yàn),實(shí)際上會(huì)先繪制一些初始界面,類似于PlaceHolder。

系統(tǒng)首先會(huì)讀取當(dāng)前Activity的Theme,然后根據(jù)Theme中的配置來繪制,當(dāng)Activity加載完畢后,才會(huì)替換為真正的界面。所以,Google官方提供的解決方案,就是通過android:windowBackground屬性,來進(jìn)行加載前的配置,同時(shí),這里不僅可以配置顏色,還能配置圖片,例如,我們可以使用一個(gè)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來實(shí)現(xiàn)圖片的疊加,讓開發(fā)者可以自由組合。

配置中的android:opacity="opaque"參數(shù)是為了防止在啟動(dòng)的時(shí)候出現(xiàn)背景的閃爍。

接下來可以設(shè)置一個(gè)新的Style,這個(gè)Style就是Activity預(yù)加載的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指定需要預(yù)加載的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。

最后,我們?cè)贏ctivity加載真正的界面之前,將Theme設(shè)置回正常的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);
    }
}

在這個(gè)Activity中,我使用SystemClock.sleep(2000),模擬了一個(gè)Activity加載的耗時(shí)過程,在super.onCreate(savedInstanceState)調(diào)用前,將主題重新設(shè)置為原來的主題。

通過這種方式設(shè)置的效果如下:

9.gif

啟動(dòng)的時(shí)候,會(huì)先展示一個(gè)畫面,這個(gè)畫面就是系統(tǒng)解析到的Style,等Activity加載完全完畢后,才會(huì)加載Activity的界面,而在Activity的界面中,我們將主題重新設(shè)置為正常的主題,從而達(dá)到一個(gè)友好的啟動(dòng)體驗(yàn),這種方式其實(shí)并沒有真正的加速啟動(dòng)過程,而是通過交互體驗(yàn)來優(yōu)化了展示的效果。

異步初始化

這個(gè)很簡單,就是讓App在onCreate里面盡可能的少做事情,而利用手機(jī)的多核特性,盡可能的利用多線程,例如一些第三方框架的初始化,如果能放線程,就盡量的放入線程中,最簡單的,你可以直接new Thread(),當(dāng)然,你也可以通過公共的線程池來進(jìn)行異步的初始化工作,這個(gè)是最能夠壓縮啟動(dòng)時(shí)間的方式

延遲初始化

延遲初始化并不是減少了啟動(dòng)時(shí)間,而是讓耗時(shí)操作讓位、讓資源給UI繪制,將耗時(shí)的操作延遲到UI加載完畢后,所以,這里建議通過mDecoView.post方法,來進(jìn)行延遲加載,代碼如下:

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

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

我們的ContentView就是通過mDecoView.addView加入到根布局的,所以,通過這種方式,可以讓延遲加載的內(nèi)容,在ContentView初始化完畢后,再進(jìn)行執(zhí)行,保證了UI繪制的流暢性。

IntentService

IntentService是繼承于Service并處理異步請(qǐng)求的一個(gè)類,在IntentService的內(nèi)部,有一個(gè)工作線程來處理耗時(shí)操作,啟動(dòng)IntentService的方式和啟動(dòng)傳統(tǒng)Service一樣,同時(shí),當(dāng)任務(wù)執(zhí)行完后,IntentService會(huì)自動(dòng)停止,而不需要去手動(dòng)控制。

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

我們將耗時(shí)任務(wù)丟到IntentService中去處理,系統(tǒng)會(huì)自動(dòng)開啟線程去處理,同時(shí),在任務(wù)結(jié)束后,還能自己結(jié)束Service,多么的人性化!OK,只需要在Application或者Activity的onCreate中去啟動(dòng)這個(gè)IntentService即可:

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

最后不要忘記在Mainifest注冊(cè)Service。

使用ActivityLifecycleCallbacks

Framework提供的這個(gè)方法可以監(jiān)控到所有Activity的生命周期,在這里,我們就可以通過onActivityCreated這樣一個(gè)回調(diào),來將一些UI相關(guān)的初始化操作放到這里,同時(shí),通過unregisterActivityLifecycleCallbacks來避免重復(fù)的初始化。同時(shí),這里onActivityCreated回調(diào)的參數(shù)Bundle,可以用來區(qū)別是否是被系統(tǒng)所回收的Activity。

public class MainApplication extends Application {

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

            @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) {
            }
        });
    }
}

資源優(yōu)化

有幾個(gè)方面,一個(gè)自然是優(yōu)化布局、布局層級(jí),一個(gè)是優(yōu)化資源,盡可能的精簡資源、避免垃圾資源,這些可以通過混淆和tinyPNG這些工具來實(shí)現(xiàn)。

甩鍋方案

下面是兩種不同的方案,都是在Style中進(jìn)行配置:

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

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

我們先來看看這樣做的效果:

8.gif

設(shè)置效果類似,即通過取消、透明化系統(tǒng)的統(tǒng)一的加載頁面來達(dá)到啟動(dòng)的『加速』,實(shí)際上,是一個(gè)『甩鍋』的過程。強(qiáng)烈建議開發(fā)者不要通過這種方式去做『所謂的啟動(dòng)加速』,這種方式雖然看上去自己的App啟動(dòng)非??欤查g就完成了,但實(shí)際上,是將真正的啟動(dòng)界面給隱藏了。

系統(tǒng)說:這鍋,我們不背!

無解

對(duì)應(yīng)5.0以下的65535問題,目前只能通過Multidex來進(jìn)行處理,而在5.0以下的機(jī)器上,系統(tǒng)在加載前的合并Dex的過程,有可能非常長,這也是暫時(shí)無解的問題,只能希望后面Multidex進(jìn)行優(yōu)化。

OK,App的啟動(dòng)優(yōu)化基本如上,其重點(diǎn)過程,依然是分析耗時(shí)的操作,以及如何設(shè)計(jì)合理的啟動(dòng)順序,希望各位能夠通過文中介紹的方式來進(jìn)行App的啟動(dòng)優(yōu)化。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容