Android性能優化(一)之啓動加速35%

1、前言

隨着項目版本的迭代,App的性能問題會逐漸暴露出來,而好的用戶體驗與性能表現緊密相關,從本篇文章開始,我將開啓一個Android應用性能優化的專題,從理論到實戰,從入門到深挖,手把手將性能優化實踐到項目中,歡迎持續關注!html

那麼第一篇文章我就從應用的啓動優化開始,根據實際案例,打造閃電般的App啓動速度。android

2、初識啓動加速

來看一下Google官方文檔《Launch-Time Performance》對應用啓動優化的概述;git

應用的啓動分爲冷啓動、熱啓動、溫啓動,而啓動最慢、挑戰最大的就是冷啓動:系統和App自己都有更多的工做要從頭開始! 應用在冷啓動以前,要執行三個任務:github

  1. 加載啓動App;
  2. App啓動以後當即展現出一個空白的Window;
  3. 建立App的進程;

而這三個任務執行完畢以後會立刻執行如下任務:shell

  1. 建立App對象;
  2. 啓動Main Thread;
  3. 建立啓動的Activity對象;
  4. 加載View;
  5. 佈置屏幕;
  6. 進行第一次繪製;

而一旦App進程完成了第一次繪製,系統進程就會用Main Activity替換已經展現的Background Window,此時用戶就可使用App了。數據庫

應用冷啓動流程圖
做爲普通應用,App進程的建立等環節咱們是沒法主動控制的, 能夠優化的也就是Application、Activity建立以及回調等過程

一樣,Google也給出了啓動加速的方向緩存

  1. 利用提早展現出來的Window,快速展現出來一個界面,給用戶快速反饋的體驗;
  1. 避免在啓動時作密集沉重的初始化(Heavy app initialization);
  2. 定位問題:避免I/O操做、反序列化、網絡操做、佈局嵌套等。

備註:方向1屬於治標不治本,只是表面上快;方向二、3能夠真實的加快啓動速度。 接下來咱們就在項目中實際應用。性能優化

3、啓動加速之主題切換

按照官方文檔的說明:使用Activity的windowBackground主題屬性來爲啓動的Activity提供一個簡單的drawable。 Layout XML file: 微信

資源文件配置
Manifest file:

Manifest文件中

Activity中

這樣在啓動的時候,會先展現一個界面,這個界面就是Manifest中設置的Style,等Activity加載完畢後,再去加載Activity的界面,而在Activity的界面中,咱們將主題從新設置爲正常的主題,從而產生一種快的感受。不過如上文總結這種方式其實並無真正的加速啓動過程,而是經過交互體驗來優化了展現的效果。 備註:截圖一樣來自官方文檔《Launch-Time Performance》markdown

4、啓動加速之Avoid Heavy App Initialization

經過代碼分析咱們能夠獲得App啓動的業務工做流程圖:

App冷啓動業務工做流程圖

這一章節咱們重點關注初始化的部分:在Application以及首屏Activity中咱們主要作了:

  • MultiDex以及Tinker的初始化,最早執行;關於MultiDex的優化本文再也不贅述,參考我以前Multidex的系列文章
  • Application中主要作了各類三方組件的初始化;

項目中**除聽雲以外其他全部三方組件都搶佔先機,在Application主線程初始化。**這樣的初始化方式確定是太重的:

  • 考慮異步初始化三方組件,不阻塞主線程;
  • 延遲部分三方組件的初始化;實際上咱們粗粒度的把全部三方組件都放到異步任務裏,可能會出現WorkThread中還沒有初始化完畢但MainThread中已經使用的錯誤,所以這種狀況建議延遲到使用前再去初始化;
  • 而如何開啓WorkThread一樣也有講究,這個話題在下文詳談。

項目修改:

  1. 將友盟、Bugly、聽雲、GrowingIO、BlockCanary等組件放在WorkThread中初始化;
  2. 延遲地圖定位、ImageLoader、自有統計等組件的初始化:地圖及自有統計延遲4秒,此時應用已經打開;而ImageLoader 由於調用關係不能異步以及太久延遲,初始化從Application延遲到SplashActivity;而EventBus由於再Activity中使用因此必須在Application中初始化。

三方組件調用優化示例代碼

注意:閃屏頁的2秒停留能夠利用,把耗時操做延遲到這個時間間隔裏。

5、啓動加速之Diagnosing The Problem

本節咱們實際定位耗時的操做,在開發階段咱們通常使用BlockCanary或者ANRWatchDog找耗時操做,簡單明瞭,可是沒法獲得每個方法的執行時間以及更詳細的對比信息。咱們能夠經過Method Tracing或者DDMS來得到更全面詳細的信息。 啓動應用,點擊 Start Method Tracing,應用啓動後再次點擊,會自動打開剛纔操做所記錄下的.trace文件,建議使用DDMS來查看,功能更加方便全面。

優化以前應用啓動trace文件分析圖

左側爲發生的具體線程,右側爲發生的時間軸,下面是發生的具體方法信息。注意兩列: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回調期間主線程就沒有耗時、爭搶資源等狀況了。此外還涉及佈局優化、內存優化等部分技術,因對於應用冷啓動通常不是瓶頸點,這裏不展開詳談,可根據實際項目實際處理。

6、對比效果:

經過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的啓動)。

7、問題:

一、還能夠繼續優化的方向?

  • 項目裏使用Retrofit網絡請求庫,FastConverterFactory作Json解析器,TraceView中看到FastConverterFactory在建立過程當中也比較耗時,考慮將其換爲GsonConverterFactory。可是由於類的繼承關係短期內沒法直接替換,做爲優化點暫時遺留;
  • 能夠考慮根據實際狀況將啓動時部分接口合併爲一,減小網絡請求次數,下降頻率;
  • 相同功能的組件只保留一個,例如:友盟、GrowingIO、自有統計等功能重複;
  • 使用ReDex進行優化;實驗Redex發現Apk體積確實是小了一點,可是啓動速度沒有變化,或許須要繼續研究。

二、異步、延遲初始化及操做的依據? 注意一點:並非每個組件的初始化以及操做均可以異步或延遲;是否能夠取決組件的調用關係以及本身項目具體業務的須要。保證一個準則:能夠異步的都異步,不能夠異步的儘可能延遲。讓應用先啓動,再操做。

三、通用應用啓動加速套路?

  • 利用主題快速顯示界面;
  • ** 異步初始化組件;**
  • ** 梳理業務邏輯,延遲初始化組件、操做;**
  • ** 正確使用線程;**
  • ** 去掉無用代碼、重複邏輯等。**

四、其它

  • 將啓動速度加快了35%不表明以前的代碼都是問題,從業務角度上將,代碼並無錯誤,實現了業務需求。可是在啓動時這個注重速度的階段,忽略的細節就會致使性能的瓶頸。
  • 開發過程當中,對核心模塊與應用階段如啓動時,使用TraceView進行分析,儘早發現瓶頸。

參考文章:《官方文檔——Launch-Time Performance》

歡迎關注微信公衆號:按期分享Java、Android乾貨!

歡迎關注
相關文章
相關標籤/搜索