一觸即發——App啟動優化最佳實踐
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow
也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!
一觸即發 App啟動優化最佳實踐
文中的很多圖都是Google效能優化指南第六季中的一些截圖
Google給出的優化指南來鎮樓
https://developer.android.com/topic/performance/launch-time.html
閃屏定義
Android官方的效能優化典範,從第六季開始,發起了一系列針對App啟動的優化實踐,地址如下:
https://www.youtube.com/watch?v=Vw1G1s73DsY&index=74&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE
可想而知,App的啟動效能是非常重要的。同時,Google針對App閃屏,也給出了非常詳細的設計定義,如下所示。
其實最早的時候,閃屏是用來在App未完全啟動的時候,讓使用者不至於困惑App是否啟動而加入的一個設計。而現在的很多App,基本上都把閃屏當做一個廣告、宣傳的頁面了,貌似已經失去了原本的意義,但閃屏,不管怎麼說,在一個App啟動的時候,都是非常重要的,設計的事情,交給UE吧,開發要做的,就是讓App的啟動體驗,做到最好。
App啟動流程
App啟動的整個過程,可以分解成下面幾個過程:
- 使用者在Launcher上點選App Icon
- 系統為App建立程序,顯示啟動視窗
- 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
- 1
- 2
抓到的Log如圖所示:
那麼這個時間,實際上是Activity啟動,到Layout全部顯示的過程,但是要注意,這裡並不包括資料的載入,因為很多App在載入時會使用懶載入模式,即資料拉取後,再重新整理預設的UI。
reportFullyDrawn
前面說了,系統日誌中的Display Time只是佈局的顯示時間,並不包括一些資料的懶載入等消耗的時間,所以,系統給我們定義了一個類似的『自定義上報時間』——reportFullyDrawn。
同樣是借用Google的一張圖來說明:
reportFullyDrawn是由我們自己呼叫的,一般在資料全部載入完畢後,手動呼叫,這樣就會在Log中增加一條日誌:
$ adb logcat | grep “ActivityManager”ActivityManager: Displayed com.example.launcher/. LauncherActivity: +999msActivityManager: Fully drawn com.example.launcher/. LauncherActivity: +1s999ms
- 1
- 2
- 3
一般來說,使用的場景如下:
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) { }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
但是要注意,這個方式需要API19+,所以,這裡需要對SDK版本進行判斷。
計算啟動時間——ADB
通過ADB命令可以統計應用的啟動時間,指令如下所示:
➜ ~ adb shell am start -W com.xys.preferencetest/.MainActivityStarting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.xys.preferencetest/.MainActivity }Status: okActivity: com.xys.preferencetest/.MainActivityThisTime: 1047TotalTime: 1047WaitTime: 1059Complete
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
該指令一共給出了三個時間:
- 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
- 1
Android在screenrecord中新增了一個引數——bugreport,那麼加了這個引數之後,錄製出來的視訊,在左上角就會增加一行數字的顯示,如圖所示。
在視訊開始前,會顯示裝置資訊和一些引數:
視訊開始後,左上角會有一行數字:
例如圖中的:15:31:22.261 f=171(0)
其中,前面的4個數字,就是時間戳,即15點31分22秒261,f=後面的數字是當前的幀數,注意,不是幀率,而是代表當前是第幾幀,括號中的數字,代表的是『Dropped frames
count』,即掉幀數。
有了這個東西,再結合視訊就可以非常清楚的看見這些資訊了。
啟動時間的除錯
模擬啟動延時
在測試的時候,我們可以通過下面的方式來進行啟動的延遲模擬:
SystemClock.sleep(2000)
- 1
或者直接通過:
try { Thread.sleep(2000);} catch (InterruptedException e) { e.printStackTrace();}
- 1
- 2
- 3
- 4
- 5
或者通過:
new Handler().postDelayed(new Runnable() { @Override public void run() { // Delay }}, 2000);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
這些方案都可以進行啟動延遲的模擬。
強制冷啟動
在『開發者選項』中的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啟動優化的一般過程
- 通過TraceView、Systrace來分析耗時的方法與元件。
- 梳理啟動載入的每一個庫、元件。
- 將梳理出來的庫,按功能和需求進行劃分,設計該庫的啟動時機。
- 與互動溝通,設計啟動畫面,按前文方法進行優化。
解決方案
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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
可以看見,這裡通過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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
這裡需要注意下,一定是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); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
在這個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() { …… }});
- 1
- 2
- 3
- 4
- 5
- 6
我們的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: "); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
我們將耗時任務丟到IntentService中去處理,系統會自動開啟執行緒去處理,同時,在任務結束後,還能自己結束Service,多麼的人性化!OK,只需要在Application或者Activity的onCreate中去啟動這個IntentService即可:
@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); InitIntentService.start(this);}
- 1
- 2
- 3
- 4
- 5
- 6
最後不要忘記在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) { } }); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
資源優化
有幾個方面,一個自然是優化佈局、佈局層級,一個是優化資源,儘可能的精簡資源、避免垃圾資源,這些可以通過混淆和tinyPNG這些工具來實現。
甩鍋方案
下面是兩種不同的方案,都是在Style中進行配置:
<item name="android:windowDisablePreview">true</item>
- 1
與
<item name="android:windowIsTranslucent">true</item><item name="android:windowNoTitle">true</item>
- 1
- 2
我們先來看看這樣做的效果:
設定效果類似,即通過取消、透明化系統的統一的載入頁面來達到啟動的『加速』,實際上,是一個『甩鍋』的過程。強烈建議開發者不要通過這種方式去做『所謂的啟動加速』,這種方式雖然看上去自己的App啟動非常快,瞬間就完成了,但實際上,是將真正的啟動介面給隱藏了。
系統說:這鍋,我們不背!
無解
對應5.0以下的65535問題,目前只能通過Multidex來進行處理,而在5.0以下的機器上,系統在載入前的合併Dex的過程,有可能非常長,這也是暫時無解的問題,只能希望後面Multidex進行優化。
OK,App的啟動優化基本如上,其重點過程,依然是分析耗時的操作,以及如何設計合理的啟動順序,希望各位能夠通過文中介紹的方式來進行App的啟動優化。
更多內容,請關注我的微信公眾號: