一、前言
隨著專案版本的迭代,App的效能問題會逐漸暴露出來,而好的使用者體驗與效能表現緊密相關,從本篇文章開始,我將開啟一個Android應用效能優化的專題,從理論到實戰,從入門到深挖,手把手將效能優化實踐到專案中,歡迎持續關注!
那麼第一篇文章我就從應用的啟動優化開始,根據實際案例,打造閃電般的App啟動速度。
二、初識啟動加速
來看一下Google官方文件《Launch-Time Performance》對應用啟動優化的概述;
應用的啟動分為冷啟動、熱啟動、溫啟動,而啟動最慢、挑戰最大的就是冷啟動:系統和App本身都有更多的工作要從頭開始! 應用在冷啟動之前,要執行三個任務:
- 載入啟動App;
- App啟動之後立即展示出一個空白的Window;
- 建立App的程式;
而這三個任務執行完畢之後會馬上執行以下任務:
- 建立App物件;
- 啟動Main Thread;
- 建立啟動的Activity物件;
- 載入View;
- 佈置螢幕;
- 進行第一次繪製;
而一旦App程式完成了第一次繪製,系統程式就會用Main Activity替換已經展示的Background Window,此時使用者就可以使用App了。
作為普通應用,App程式的建立等環節我們是無法主動控制的,可以優化的也就是Application、Activity建立以及回撥等過程。同樣,Google也給出了啟動加速的方向:
- 利用提前展示出來的Window,快速展示出來一個介面,給使用者快速反饋的體驗;
- 避免在啟動時做密集沉重的初始化(Heavy app initialization);
- 定位問題:避免I/O操作、反序列化、網路操作、佈局巢狀等。
備註:方向1屬於治標不治本,只是表面上快;方向2、3可以真實的加快啟動速度。 接下來我們就在專案中實際應用。
三、啟動加速之主題切換
按照官方文件的說明:使用Activity的windowBackground主題屬性來為啟動的Activity提供一個簡單的drawable。 Layout XML file:
Manifest file:這樣在啟動的時候,會先展示一個介面,這個介面就是Manifest中設定的Style,等Activity載入完畢後,再去載入Activity的介面,而在Activity的介面中,我們將主題重新設定為正常的主題,從而產生一種快的感覺。不過如上文總結這種方式其實並沒有真正的加速啟動過程,而是通過互動體驗來優化了展示的效果。 備註:截圖同樣來自官方文件《Launch-Time Performance》。
四、啟動加速之Avoid Heavy App Initialization
通過程式碼分析我們可以得到App啟動的業務工作流程圖:
這一章節我們重點關注初始化的部分:在Application以及首屏Activity中我們主要做了:
- MultiDex以及Tinker的初始化,最先執行;關於MultiDex的優化本文不再贅述,參考我之前Multidex的系列文章。
- Application中主要做了各種三方元件的初始化;
專案中**除聽雲之外其餘所有三方元件都搶佔先機,在Application主執行緒初始化。**這樣的初始化方式肯定是過重的:
- 考慮非同步初始化三方元件,不阻塞主執行緒;
- 延遲部分三方元件的初始化;實際上我們粗粒度的把所有三方元件都放到非同步任務裡,可能會出現WorkThread中尚未初始化完畢但MainThread中已經使用的錯誤,因此這種情況建議延遲到使用前再去初始化;
- 而如何開啟WorkThread同樣也有講究,這個話題在下文詳談。
專案修改:
- 將友盟、Bugly、聽雲、GrowingIO、BlockCanary等元件放在WorkThread中初始化;
- 延遲地圖定位、ImageLoader、自有統計等元件的初始化:地圖及自有統計延遲4秒,此時應用已經開啟;而ImageLoader 因為呼叫關係不能非同步以及過久延遲,初始化從Application延遲到SplashActivity;而EventBus因為再Activity中使用所以必須在Application中初始化。
注意:閃屏頁的2秒停留可以利用,把耗時操作延遲到這個時間間隔裡。
五、啟動加速之Diagnosing The Problem
本節我們實際定位耗時的操作,在開發階段我們一般使用BlockCanary或者ANRWatchDog找耗時操作,簡單明瞭,但是無法得到每一個方法的執行時間以及更詳細的對比資訊。我們可以通過Method Tracing或者DDMS來獲得更全面詳細的資訊。 啟動應用,點選 Start Method Tracing,應用啟動後再次點選,會自動開啟剛才操作所記錄下的.trace檔案,建議使用DDMS來檢視,功能更加方便全面。
左側為發生的具體執行緒,右側為發生的時間軸,下面是發生的具體方法資訊。注意兩列:Real Time/Call(實際發生時間),Calls+RecurCalls/Total(發生次數); 上圖我們可以得到以下資訊:
- 可以直觀看到MainThread的時間軸很長,說明大多數任務都是在MainThread中執行;
- 通過Real Time/Call 降序排列可以看到程式中的部分程式碼確實非常耗時;
- 在下一頁可以看出來部分三方SDK也比較耗時;
即便是耗時操作,但是隻要正確發生在WorkThread就沒問題。因此我們**需要確認這些方法執行的執行緒以及發生的時機。這些操作如果發生在主執行緒,可能不構成ANR的發生條件,但是卡頓是再算難免的!**結合上章節圖App冷啟動業務工作流程圖中業務操作以及分析圖,再次檢視程式碼我們可以看到:部分耗時操作例如IO讀取等確實發生在主執行緒。事實上在traceview裡點選執行函式的名稱不僅可以跟蹤到父類及子類的方法耗時,也可以在方法執行時間軸中看到具體在哪個執行緒以及耗時的介面閃動。
分析到部分耗時操作發生在主執行緒,那我們把耗時操作都改到子執行緒是不是就萬事大吉了?非也!!
- 卡頓不能都靠非同步來解決,錯誤的使用工程執行緒不僅不能改善卡頓,反而可能加劇卡頓。是否需要開啟工作執行緒需要根據具體的效能瓶頸根源具體分析,對症下藥,不可一概而論;
- 而如何開啟執行緒同樣也有學問:Thread、ThreadPoolExecutor、AsyncTask、HandlerThread、IntentService等都各有利弊;例如通常情況下ThreadPoolExecutor比Thread更加高效、優勢明顯,但是特定場景下單個時間點的表現Thread會比ThreadPoolExecutor好:同樣的建立物件,ThreadPoolExecutor的開銷明顯比Thread大;
- 正確的開啟執行緒也不能包治百病,例如執行網路請求會建立執行緒池,而在Application中正確的建立執行緒池勢必也會降低啟動速度;因此延遲操作也必不可少。
通過對traceview的詳細跟蹤以及程式碼的詳細比對,我發現卡頓發生在:
- 部分資料庫及IO的操作發生在首屏Activity主執行緒;
- Application中建立了執行緒池;
- 首屏Activity網路請求密集;
- 工作執行緒使用未設定優先順序;
- 資訊未快取,重複獲取同樣資訊;
- 流程問題:例如閃屏圖每次下載,當次使用;
以及其它細節問題:
- 執行無用老程式碼;
- 執行開發階段使用的程式碼;
- 執行重複邏輯;
- 呼叫三方SDK裡或者Demo裡的多餘程式碼;
專案修改: 1. 資料庫及IO操作都移到工作執行緒,並且設定執行緒優先順序為THREAD_PRIORITY_BACKGROUND,這樣工作執行緒最多能獲取到10%的時間片,優先保證主執行緒執行。
2. 流程梳理,延後執行; 實際上,這一步對專案啟動加速最有效果。通過流程梳理發現部分流程呼叫時機偏早、失誤等,例如:
- 更新等操作無需在首屏尚未展示就呼叫,造成資源競爭;
- 呼叫了IOS為了規避稽核而做的開關,造成網路請求密集;
- 自有統計在Application的呼叫裡建立數量固定為5的執行緒池,造成資源競爭,在上圖traceview功能說明圖中最後一行可以看到編號12執行5次,耗時排名前列;此處執行緒池的建立是必要但可以延後的。
- 修改廣告閃屏邏輯為下次生效。
3.其它優化;
- 去掉無用但被執行的老程式碼;
- 去掉開發階段使用但線上被執行的程式碼;
- 去掉重複邏輯執行程式碼;
- 去掉呼叫三方SDK裡或者Demo裡的多餘程式碼;
- 資訊快取,常用資訊只在第一次獲取,之後從快取中取;
- 專案是多程式架構,只在主程式執行Application的onCreate();
通過以上三步及三方元件的優化:Application以及首屏Activity回撥期間主執行緒就沒有耗時、爭搶資源等情況了。此外還涉及佈局優化、記憶體優化等部分技術,因對於應用冷啟動一般不是瓶頸點,這裡不展開詳談,可根據實際專案實際處理。
六、對比效果:
通過ADB命令統計應用的啟動時間:adb shell am start -W 首屏Activity。 同等條件下使用MX3及Nexus6P,啟動5次,比較優化前與優化後的啟動時間;
優化前: MX3
ThisTime | TotalTime | WaitTime |
---|---|---|
1237 | 2205 | 2214 |
1280 | 2181 | 2189 |
1622 | 2508 | 2513 |
1485 | 2434 | 2443 |
1442 | 2418 | 2429 |
Nexus6P
ThisTime | TotalTime | WaitTime |
---|---|---|
1229 | 1832 | 1868 |
1268 | 1849 | 1880 |
1184 | 1780 | 1812 |
1262 | 1845 | 1876 |
1164 | 1766 | 1807 |
優化後:
MX3
ThisTime | TotalTime | WaitTime |
---|---|---|
865 | 1516 | 1523 |
911 | 1565 | 1573 |
812 | 1406 | 1418 |
962 | 1564 | 1574 |
925 | 1566 | 1577 |
Nexus6P
ThisTime | TotalTime | WaitTime |
---|---|---|
603 | 1192 | 1243 |
614 | 1076 | 1115 |
650 | 1120 | 1163 |
642 | 1107 | 1139 |
624 | 1084 | 1124 |
對比:
MX3提升35%
ThisTime平均數 | TotalTime平均數 | WaitTime平均數 |
---|---|---|
優化前 | 1413 | 2349 |
優化後 | 895 | 1523 |
Nexus6P提升39%
ThisTime平均數 | TotalTime平均數 | WaitTime平均數 |
---|---|---|
優化前 | 1221 | 1814 |
優化後 | 626 | 1115 |
- 命令含義:
ThisTime:最後一個啟動的Activity的啟動耗時;
TotalTime:自己的所有Activity的啟動耗時;
WaitTime: ActivityManagerService啟動App的Activity時的總時間(包括當前Activity的onPause()和自己Activity的啟動)。
七、問題:
1、還可以繼續優化的方向?
- 專案裡使用Retrofit網路請求庫,FastConverterFactory做Json解析器,TraceView中看到FastConverterFactory在建立過程中也比較耗時,考慮將其換為GsonConverterFactory。但是因為類的繼承關係短時間內無法直接替換,作為優化點暫時遺留;
- 可以考慮根據實際情況將啟動時部分介面合併為一,減少網路請求次數,降低頻率;
- 相同功能的元件只保留一個,例如:友盟、GrowingIO、自有統計等功能重複;
- 使用ReDex進行優化;實驗Redex發現Apk體積確實是小了一點,但是啟動速度沒有變化,或許需要繼續研究。
2、非同步、延遲初始化及操作的依據? 注意一點:並不是每一個元件的初始化以及操作都可以非同步或延遲;是否可以取決元件的呼叫關係以及自己專案具體業務的需要。保證一個準則:可以非同步的都非同步,不可以非同步的儘量延遲。讓應用先啟動,再操作。
3、通用應用啟動加速套路?
- 利用主題快速顯示介面;
- ** 非同步初始化元件;**
- ** 梳理業務邏輯,延遲初始化元件、操作;**
- ** 正確使用執行緒;**
- ** 去掉無用程式碼、重複邏輯等。**
4、其它
- 將啟動速度加快了35%不代表之前的程式碼都是問題,從業務角度上將,程式碼並沒有錯誤,實現了業務需求。但是在啟動時這個注重速度的階段,忽略的細節就會導致效能的瓶頸。
- 開發過程中,對核心模組與應用階段如啟動時,使用TraceView進行分析,儘早發現瓶頸。
參考文章:《官方文件——Launch-Time Performance》
廣告時間
今日頭條各Android客戶端團隊招人火爆進行中,各個級別和應屆實習生都需要,業務增長快、日活高、挑戰大、待遇給力,各位大佬走過路過千萬不要錯過!
本科以上學歷、非頻繁跳槽(如兩年兩跳),歡迎加我的微信詳聊:KOBE8242011
歡迎關注微信公眾號:定期分享Java、Android乾貨!