App啟動卡慢會影響一個App的卸載率和使用率。啟動速度快會給人一種輕快的感覺,減少用戶等待時間。如果一個App從點擊桌面圖標到看到主界面花了10秒,請問你能接受麽?忍耐不好的估計直接就卸載了,或者沒等打開就直接Home鍵按出去,然後殺進程了。這樣一來App卸載率提升了,使用率下降了。所以對於有大量... ...
Android性能優化之啟動速度優化
Android app 啟動速度優化,首先談談為什麼會走到優化這一步,如果一開始創建 app 項目的時候就把這個啟動速度考慮進去,那麼肯定就不需要重新再來優化一遍了。這是因為在移動互聯網時代,大家都追求快,什麼功能都是先做出來再說,其他的可以先不考慮,先占據先機,或者驗證是否值得做。那為什麼要這麼做呢?我個人的觀點有以下幾點
- 如果 app 不能快速開發出來,先放出去驗證一下可行性,可能連是否值得做都不知道,如果花很長時間做了一個對用戶無價值的功能,那麼還不如不做
- 如果 app 不能快速做出來,可能被競爭對手捕獲先機,那麼可能錯失最佳商業時機
- 如果一開始就規定不能影響啟動速度的這個目標,那麼做功能的時候就會有束縛,快不起來
- app 初期大家都忙著開發新功能,迭代新版本,沒有時間停下來做優化
- 同類型 app 變多,競爭對手變多,大家才開始關註啟動性能,才開始做啟動速度優化(有主動出擊也有被動優化)
一、引起性能問題的原因
隨著項目不斷的快速迭代,往往會造成App啟動卡慢現象,因為可能在App主進程啟動階段或者在主界面啟動階段放了很多初始化其他業務的邏輯,而這些業務落地可能一開始並不需要用到。本文從作者的親身經歷給大家闡述啟動速度優化相關的點點滴滴,為啟動速度優化提供一種思路給大家參考。
二、為什麼要做啟動速度優化
App啟動卡慢會影響一個App的卸載率和使用率。啟動速度快會給人一種輕快的感覺,減少用戶等待時間。如果一個App從點擊桌面圖標到看到主界面花了10秒,請問你能接受麽?忍耐不好的估計直接就卸載了,或者沒等打開就直接Home鍵按出去,然後殺進程了。這樣一來App卸載率提升了,使用率下降了。所以對於有大量用戶的App來說,這些性能細節是很重要的,畢竟用戶就是錢啊。
三、分析制定優化技術路線
3.1 分析啟動性能瓶頸
在具體的優化之前,首先我們得找到需要優化的地方,怎麼找?這就要求瞭解Android App的啟動原理,我們要知道一個App從點擊桌面圖標到我們看到App的主界面整個過程中經過了哪些步驟,哪些地方是我們可以優化的地方。下圖是App啟動過程的一個大概描述。
具體的代碼流程,分析關鍵的函數耗時
圖中onFirstDrawFinish和onWindowFocusChanged的前後順序可能會顛倒,但是時間差不大。
3.2 制定優化方向
從上面的分析可以看出,App啟動過程中我們優化的地方包括主進程啟動流程和主界面啟動流程,主進程啟動就是Application的創建過程,主界面啟動就是MainActivity的創建過程。只需要分別對這兩個部分進行優化即可。
- Application中attachBaseContext最早被調用,隨後是onCreate方法,儘量在這兩個方法中不要有耗時操作。
MainActivity中重點關註onCreate,onResume,onWindowFocusChange,Activity啟動完成結束標誌這裡採用沒有使用生命周期函數,而是以主界面View的第一次繪製作為啟動完成的標誌,View被第一次繪製證明View即將展示出來被我們看到。所以我們在Activity根佈局中加入一個自定義View,以它的onDraw方法第一次回調作為Activity啟動完成的標誌。
public class FirstDrawListenView extends View { private boolean isFirstDrawFinish = false; private IFirstDrawListener mIFirstDrawListener; public FirstDrawListenView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (!isFirstDrawFinish) { isFirstDrawFinish = true; if (mIFirstDrawListener != null) { mIFirstDrawListener.onFirstDrawFinish(); } } } public void setFirstDrawListener(IFirstDrawListener firstDrawListener) { mIFirstDrawListener = firstDrawListener; } public interface IFirstDrawListener { void onFirstDrawFinish(); } }
四、怎麼統計數據查看優化前後的數據對比
通過上面的分析,我們可以統計進程啟動各個階段的耗時點,以及Activity啟動各個階段的耗時點(這個步驟需要額外在主佈局中加入一個自定義的空View,監聽它的onDraw方法的第一次回調),可以通過埋點數據收集這些數據,在優化之前可以先加入埋點數據,統計上報各個時間段的埋點,所以需要先發個版本驗證一下優化之前的情況。統計數據的機制加入之後,就可以著手優化了,一邊優化一邊對比,可以很清楚看到優化前後的對比。
五、制定優化的目標
由於App啟動速度在不同是設備上差別很大,所以目標不太好定,但是做事情總得要有個目標吧。首先我們使用大家都熟悉的一個概念“秒開”,其次是冷啟動熱啟動分開算,再次是分出不同的機型(高端機,中端機型,低端機型),最後是需要先看看沒優化之前的啟動數據。這樣就可以定義出類似下麵的目標:
- 高端機型1秒內打開(比如小米5,Android6.0以上)
- 中端機型1.5秒內打開
- 低端機型2.5秒內打開
上面是終極目標,真正優化的時候,要結合App實際數據以及團隊實際情況來定自己的優化目標。
六、優化具體步驟
一般來說,快速優化最好的方式就是把不必要提前做的操作放到非同步線程中去做,也就是我們經常做的非同步載入。除了非同步載入,一些真正有性能影響的代碼需要做具體優化。下麵依次介紹一些具體的優化實施步驟。
6.1 封裝一個列印耗時點日誌的輔助類
優化的時候為了快速定位耗時的代碼塊,我們需要在耗時代碼塊的前後加上日誌,統計耗時具體的時間。這個能在Debug模式下幫助我們快速分析定位到耗時的代碼塊,然後我們在針對具體的耗時代碼塊去下手,看看怎麼優化。
6.2 非同步載入一:Application中加入非同步線程
在Application中封裝兩個方法:onSyncLoad(同步載入)和 onAsyncLoad(非同步載入,在Thread中執行),把不需要同步載入的部分全部放到onAsyncLoad方法,需要同步的方法放到onSyncLoad中去做,就這種簡單的分類就可以帶來一個很好的優化效果。
public class StartUpApplication extends Application {
@Override
public void onCreate() {
// 程式創建時調用,次方法應該執行應該儘量快,否則會拖慢整個app的啟動速度
super.onCreate();
onSyncLoadForCreate();
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
onSyncLoad();
onAsyncLoad();
}
private void onSyncLoadForCreate() {
AppStartUpTimeLog.isColdStart = true; // 設置為冷啟動標誌
AppLog.log("StartUpApplication onCreate");
AppStartUpTimeLog.logTimeDiff("App onCreate start", false, true);
BlockingUtil.simulateBlocking(500); // 模擬阻塞100毫秒
AppStartUpTimeLog.logTimeDiff("App onCreate end");
}
private void onSyncLoad() {
AppLog.log("StartUpApplication attachBaseContext");
AppStartUpTimeLog.markStartTime("App attachBaseContext", true);
BlockingUtil.simulateBlocking(200); // 模擬阻塞100毫秒
AppStartUpTimeLog.logTimeDiff("App attachBaseContext end", true);
}
public void onAsyncLoad() {
new Thread(new Runnable() {
@Override
public void run() {
// 非同步載入邏輯
}
}, "ApplicationAsyncLoad").start();
}
}
6.3 非同步載入二:MainActivity中加入非同步線程
這一步驟與Application的優化思路一樣,也是封裝onSyncLoad和onAsyncLoad方法對現有代碼進行一個分類,但是這兩個方法的調用時機要晚一點,是在主界面首屏繪製完成的時候調用。這個步驟也需要new一個Thead,屬於額外的開銷,不過這不影響我們整體性能。
6.4 延遲載入功能:首屏繪製完成之後載入
還有些操作必須要在UI線程做,但是不需要那麼快速就做,這裡放到首屏繪製完成之後,我們之前在主佈局中加入一個空的View來監聽它的第一次onDraw回調,我們通過介面的方式把這個事件接到我們的MainActivity中去(Activity中實現介面的onFirstDrawFinish方法)。為了讓用戶儘快看到主界面,我們就可以把一些需要在UI線程執行,但是又不需要那麼快的執行的操作放到onFirstDrawFinish中去。
6.5 動態載入佈局:主佈局文件優化
把主界面中不需要第一次就用到的佈局全部使用動態載入的方式來處理,使用ViewStub或者直接在使用時動態addView的方式。
6.6 主佈局文件深度優化
如果做了上面這些優化還是會發現進入主界面還是有些慢,那麼需要重點關註主佈局文件了。主佈局文件的複雜度直接影響到了Activity的載入速度,這個時候需要對主佈局文件進行深度優化了。Activity在載入佈局的時候,會對整個佈局文件進行解析,測量(measure),佈局(layout)和繪製(draw),所以設計簡單合理的佈局尤為重要。佈局的優化不做詳細介紹,網上很多文章的。幾個重要的優化如下:
- 減少佈局層級
- 減少首次載入View的數量
- 減少過度繪製
如果需要看看主佈局載入具體用了多少時間,需要用自定ViewGroup作為根佈局根元素,然後監控它的onInflateFinished,onMeasure,onLayout,onDraw方法,通過我們之前寫好的列印時間日誌的輔助類,列印一些關鍵日誌,可以分析出具體的耗時的步驟,還可以定位哪個View載入耗時最長。
6.7 功能代碼深度優化
前面的優化步驟中,我們有部分耗時操作放到了首屏繪製onFirstDrawFinish之後來做了,這裡會帶來一個體驗上的問題,雖然進入主界面變快了,但是可能進入之後短暫的時間類UI線程是阻塞的,如果有其他的UI操作可能會卡主,因為onFirstDrawFinish中掛了很多耗時的操作,需要等這些做完之後UI線程才能空閑。所以我們還需要對一些功能代碼進行優化,確保其真正用時少。另外我們非同步載入線程中的操作是有一定的安全風險的,如果有些操作很耗時,可能導致我們進入主界面需要用到數據時還沒有準備好,所以非同步載入我們要註意代碼塊的順序,如果有些非常耗時的操作考慮用單獨的線程去處理。
七、總結
優化是一條持續之路,通過優化我們可以瞭解到影響啟動性能的因素有哪些,這樣我們平時在編碼的過程中就會多註意自己的代碼性能。本文從全局的角度去看待整個啟動性能優化,看起來好像還挺容易,但是可能實際過程中優化並不會很順利,不同的設備上可能表現不一樣,有時候可能啟動一個服務都會耗時。所以要想真正的不耗時,那就是大招:刪除它吧。
八、項目地址
模擬耗時點,列印日誌觀察生命周期函數回調情況
https://github.com/PopFisher/AppStartUpSpeedOpt