一觸即發(fā) App啟動優(yōu)化最佳實踐
文中的很多圖都是Google性能優(yōu)化指南第六季中的一些截圖
Google給出的優(yōu)化指南來鎮(zhèn)樓
https://developer.android.com/topic/performance/launch-time.html
閃屏定義
Android官方的性能優(yōu)化典范赋续,從第六季開始捧书,發(fā)起了一系列針對App啟動的優(yōu)化實踐,地址如下:
https://www.youtube.com/watch?v=Vw1G1s73DsY&index=74&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE
可想而知琼梆,App的啟動性能是非常重要的肢簿。同時湾碎,Google針對App閃屏球昨,也給出了非常詳細的設計定義烤蜕,如下所示辕近。
https://material.google.com/patterns/launch-screens.html
其實最早的時候韵吨,閃屏是用來在App未完全啟動的時候,讓用戶不至于困惑App是否啟動而加入的一個設計移宅。而現(xiàn)在的很多App归粉,基本上都把閃屏當做一個廣告、宣傳的頁面了漏峰,貌似已經(jīng)失去了原本的意義糠悼,但閃屏,不管怎么說浅乔,在一個App啟動的時候倔喂,都是非常重要的席噩,設計的事情悼枢,交給UE吧,開發(fā)要做的双揪,就是讓App的啟動體驗渴邦,做到最好。
App啟動流程
App啟動的整個過程,可以分解成下面幾個過程:
- 用戶在Launcher上點擊App Icon
- 系統(tǒng)為App創(chuàng)建進程踢步,顯示啟動窗口
- App在進程中創(chuàng)建自己的組件
這個過程可以用下面這幅圖來描述:
而我們能夠優(yōu)化的兼丰,也就是下面Application的創(chuàng)建部分面徽,系統(tǒng)的進程分配以及一些窗口切換的動畫效果等,都是跟ROM相關的眶蕉,我們無法處理饭入。所以,我們需要把重點放到Application的創(chuàng)建過程。
上面是官方的說明,下面我們用更加通俗的語言來解釋一遍蹄葱。
當用戶點擊桌面icon的時候,系統(tǒng)準備好了,給App分配進程空間,就好像去酒店開房,但是你又不能直接進入房間泉蝌,你得坐電梯去房間诅愚,那么你坐電梯的這個時間泳赋,實際上就是系統(tǒng)的準備時間,那么系統(tǒng)的這個準備時間一般來說不會太長,但假如的開的是一個總統(tǒng)套房呢泵三,系統(tǒng)就得花不少時間來打理,所以系統(tǒng)給所有用戶都準備了一個過渡界面,這個界面,就是啟動時的黑屏\白屏,也就是你坐電梯里面看的小廣告,看完小廣告,你就到房間了,然后你想干嘛都可以了桐经,這個想干嘛的速度,就完全取決于你開門的速度了誓沸,你門開得快,自然那啥快,所以這里是開發(fā)者可以優(yōu)化的地方,有些開發(fā)者掏個鑰匙要好幾秒罪佳,有的只要幾百毫秒蕾管,完全影響了后面那啥的效率掏熬。
那么一般來說捆蜀,故事到這里就結束了疮丛,但是誊薄,系統(tǒng)锰茉,也就是這個酒店著瓶,并不是一個野雞酒店威酒,他也想盡量做得讓顧客滿意苏遥,這樣才會有回頭客啊田炭,所以,酒店做了一個優(yōu)化欺缘,可以讓每個顧客自己定義在坐電梯的時候想看什么栋豫!也就是說,系統(tǒng)在加載App的時候谚殊,首先是加載了資源文件丧鸯,這里就包括了要啟動的Activity的Theme,而這個Theme呢嫩絮,是可以自定義的丛肢,也就是顧客在坐電梯時想看的東西围肥,而不是千篇一律的白屏或者黑屏,他可以定制很多東西蜂怎,例如ActionBar穆刻、背景、StatBar等等杠步。
啟動時間的測量
關于Activity啟動時間的定義
對于Activity來說氢伟,啟動時,首先執(zhí)行的是onCreate()幽歼、onStart()朵锣、onResume()這些生命周期函數(shù),但即使這些生命周期方法回調(diào)結束了甸私,應用也不算已經(jīng)完全啟動诚些,還需要等View樹全部構建完畢,一般認為皇型,setContentView中的View全部顯示結束了诬烹,算作是應用完全啟動了。
Display Time
從API19之后犀被,Android在系統(tǒng)Log中增加了Display的Log信息椅您,通過過濾ActivityManager以及Display這兩個關鍵字,可以找到系統(tǒng)中的這個Log:
$ adb logcat | grep “ActivityManager”
ActivityManager: Displayed com.example.launcher/.LauncherActivity: +999ms
抓到的Log如圖所示:
那么這個時間寡键,實際上是Activity啟動掀泳,到Layout全部顯示的過程,但是要注意西轩,這里并不包括數(shù)據(jù)的加載员舵,因為很多App在加載時會使用懶加載模式,即數(shù)據(jù)拉取后藕畔,再刷新默認的UI马僻。
reportFullyDrawn
前面說了,系統(tǒng)日志中的Display Time只是布局的顯示時間注服,并不包括一些數(shù)據(jù)的懶加載等消耗的時間韭邓,所以,系統(tǒng)給我們定義了一個類似的『自定義上報時間』——reportFullyDrawn溶弟。
同樣是借用Google的一張圖來說明:
reportFullyDrawn是由我們自己調(diào)用的女淑,一般在數(shù)據(jù)全部加載完畢后,手動調(diào)用辜御,這樣就會在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) {
// 加載數(shù)據(jù)
// ……
// 上報reportFullyDrawn
reportFullyDrawn();
}
@Override
public Loader<Void> onCreateLoader(int id, Bundle args) {
return null;
}
@Override
public void onLoaderReset(Loader<Void> loader) {
}
}
但是要注意,這個方式需要API19+,所以袱巨,這里需要對SDK版本進行判斷阁谆。
計算啟動時間——ADB
通過ADB命令可以統(tǒng)計應用的啟動時間,指令如下所示:
? ~ 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.系統(tǒng)調(diào)用AMS耗時——3.第一個Activity(也許是閃屏頁)啟動耗時——4.第一個Activity的onPause()耗時——5.第二個Activity啟動耗時
那么场绿,ThisTime表示5(最后一個Activity的啟動耗時)。TotalTime表示3.4.5總共的耗時(如果啟動時只有一個Activity俺夕,那么ThisTime與TotalTime應該是一樣的)裳凸。WaitTime則表示所有的操作耗時,即1.2.3.4.5所有的耗時劝贸。
每次給出的時間可能并不一樣,而且應用從首次安裝啟動到后面每次正常啟動逗宁,時間都會不同映九,區(qū)別于系統(tǒng)是否要分配進程空間。
計算啟動時間——Screen Record
通過錄屏進行啟動的分析瞎颗,是一個很好的辦法件甥,在API21+,Android給我們提供了一個更加方便哼拔、準確的方式:
? ~ adb shell screenrecord --bugreport /sdcard/test.mp4
Android在screenrecord中新增了一個參數(shù)——bugreport引有,那么加了這個參數(shù)之后,錄制出來的視頻倦逐,在左上角就會增加一行數(shù)字的顯示譬正,如圖所示。
在視頻開始前檬姥,會顯示設備信息和一些參數(shù):
視頻開始后曾我,左上角會有一行數(shù)字:
例如圖中的:15:31:22.261 f=171(0)
其中,前面的4個數(shù)字健民,就是時間戳抒巢,即15點31分22秒261,f=后面的數(shù)字是當前的幀數(shù)秉犹,注意蛉谜,不是幀率,而是代表當前是第幾幀崇堵,括號中的數(shù)字型诚,代表的是『Dropped frames
count』,即掉幀數(shù)筑辨。
有了這個東西俺驶,再結合視頻就可以非常清楚的看見這些信息了。
啟動時間的調(diào)試
模擬啟動延時
在測試的時候,我們可以通過下面的方式來進行啟動的延遲模擬:
SystemClock.sleep(2000)
或者直接通過:
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
或者通過:
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// Delay
}
}, 2000);
這些方案都可以進行啟動延遲的模擬暮现。
強制冷啟動
在『開發(fā)者選項』中的Background Process Limit中設置為No Background Processes
優(yōu)化點
Static Block
很多代碼中的Static Block还绘,都是做一些初始化工作,特別是ContentProvider中在Static Block中初始化一些UriMatcher栖袋,這些東西可以做成懶加載模式拍顷。
Application
Application是程序的主入口,特別是很多第三方SDK都會需要在Application的onCreate里面做很多初始化操作塘幅,不得不說昔案,各種第三方SDK,都特別喜歡這個『兵家必爭之地』电媳,再加上自己的一些庫的初始化踏揣,會讓整個Application不堪重負。
優(yōu)化的方法匾乓,無非是通過以下幾個方面:
- 延遲初始化
- 后臺任務
- 界面預加載
阻塞
阻塞有很多種情況捞稿,例如磁盤IO阻塞(讀寫文件、SharedPerfences)拼缝、網(wǎng)絡阻塞(現(xiàn)在應該不會了)以及高CPU占用的代碼(加解密娱局、渲染、解析等等)咧七。
View層級
見《Android群英傳》
耗時方法
通過使用TraceView && Systrace && Method Tracing工具來進行排查衰齐,見《Android群英傳:神兵利器》
App啟動優(yōu)化的一般過程
- 通過TraceView、Systrace來分析耗時的方法與組件继阻。
- 梳理啟動加載的每一個庫耻涛、組件。
- 將梳理出來的庫穴翩,按功能和需求進行劃分犬第,設計該庫的啟動時機。
- 與交互溝通芒帕,設計啟動畫面歉嗓,按前文方法進行優(yōu)化。
解決方案
Theme
當系統(tǒng)加載一個Activity的時候背蟆,onCreate()是一個耗時過程鉴分,那么在這個過程中,系統(tǒng)為了讓用戶能有一個比較好的體驗带膀,實際上會先繪制一些初始界面志珍,類似于PlaceHolder。
系統(tǒng)首先會讀取當前Activity的Theme垛叨,然后根據(jù)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來實現(xiàn)圖片的疊加抡谐,讓開發(fā)者可以自由組合裁奇。
配置中的android:opacity="opaque"參數(shù)是為了防止在啟動的時候出現(xiàn)背景的閃爍。
接下來可以設置一個新的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)調(diào)用前莉撇,將主題重新設置為原來的主題呢蛤。
通過這種方式設置的效果如下:
啟動的時候,會先展示一個畫面棍郎,這個畫面就是系統(tǒng)解析到的Style其障,等Activity加載完全完畢后,才會加載Activity的界面涂佃,而在Activity的界面中励翼,我們將主題重新設置為正常的主題,從而達到一個友好的啟動體驗辜荠,這種方式其實并沒有真正的加速啟動過程汽抚,而是通過交互體驗來優(yōu)化了展示的效果。
異步初始化
這個很簡單伯病,就是讓App在onCreate里面盡可能的少做事情造烁,而利用手機的多核特性,盡可能的利用多線程,例如一些第三方框架的初始化惭蟋,如果能放線程苗桂,就盡量的放入線程中,最簡單的敞葛,你可以直接new Thread()誉察,當然,你也可以通過公共的線程池來進行異步的初始化工作惹谐,這個是最能夠壓縮啟動時間的方式
延遲初始化
延遲初始化并不是減少了啟動時間持偏,而是讓耗時操作讓位、讓資源給UI繪制氨肌,將耗時的操作延遲到UI加載完畢后鸿秆,所以,這里建議通過mDecoView.post方法怎囚,來進行延遲加載卿叽,代碼如下:
getWindow().getDecorView().post(new Runnable() {
@Override public void run() {
……
}
});
我們的ContentView就是通過mDecoView.addView加入到根布局的,所以恳守,通過這種方式考婴,可以讓延遲加載的內(nèi)容,在ContentView初始化完畢后催烘,再進行執(zhí)行沥阱,保證了UI繪制的流暢性。
IntentService
IntentService是繼承于Service并處理異步請求的一個類伊群,在IntentService的內(nèi)部考杉,有一個工作線程來處理耗時操作,啟動IntentService的方式和啟動傳統(tǒng)Service一樣舰始,同時崇棠,當任務執(zhí)行完后,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中去處理,系統(tǒng)會自動開啟線程去處理及老,同時抽莱,在任務結束后,還能自己結束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提供的這個方法可以監(jiān)控到所有Activity的生命周期虐呻,在這里象泵,我們就可以通過onActivityCreated這樣一個回調(diào),來將一些UI相關的初始化操作放到這里斟叼,同時偶惠,通過unregisterActivityLifecycleCallbacks來避免重復的初始化。同時朗涩,這里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相關的內(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)化
有幾個方面谢床,一個自然是優(yōu)化布局兄一、布局層級,一個是優(yōu)化資源识腿,盡可能的精簡資源出革、避免垃圾資源,這些可以通過混淆和tinyPNG這些工具來實現(xiàn)渡讼。
甩鍋方案
下面是兩種不同的方案骂束,都是在Style中進行配置:
<item name="android:windowDisablePreview">true</item>
與
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
我們先來看看這樣做的效果:
設置效果類似,即通過取消成箫、透明化系統(tǒng)的統(tǒng)一的加載頁面來達到啟動的『加速』展箱,實際上,是一個『甩鍋』的過程蹬昌。強烈建議開發(fā)者不要通過這種方式去做『所謂的啟動加速』,這種方式雖然看上去自己的App啟動非澄雠海快,瞬間就完成了凳厢,但實際上,是將真正的啟動界面給隱藏了竞慢。
系統(tǒng)說:這鍋先紫,我們不背!
無解
對應5.0以下的65535問題筹煮,目前只能通過Multidex來進行處理遮精,而在5.0以下的機器上,系統(tǒng)在加載前的合并Dex的過程败潦,有可能非常長本冲,這也是暫時無解的問題,只能希望后面Multidex進行優(yōu)化劫扒。
OK檬洞,App的啟動優(yōu)化基本如上,其重點過程沟饥,依然是分析耗時的操作添怔,以及如何設計合理的啟動順序湾戳,希望各位能夠通過文中介紹的方式來進行App的啟動優(yōu)化。