Android性能優(yōu)化之啟動優(yōu)化實戰(zhàn)
前言
本文將帶領(lǐng)大家來看看啟動優(yōu)化相關(guān)方面的介紹以及各種優(yōu)化的方法。希望你在讀完本章后會有所收獲。
相信很多同學(xué)都聽過八秒定律,八秒定律是在互聯(lián)網(wǎng)領(lǐng)域存在的一個定律,即指用戶訪問一個網(wǎng)站時,如果等待網(wǎng)頁打開的時間超過了8秒,就有超過70%的用戶放棄等待。足見啟動的時間是多么的重要。放到移動APP中,那就是應(yīng)用啟動的時間不能太久,否則就會造成用戶的流失。
谷歌官方曾給出一篇App startup time的文章,這篇文章詳細介紹了關(guān)于啟動優(yōu)化的切入點以及思路。感興趣的同學(xué)可以去看下。App Startup Time 這是官方地址。本篇文章也主要是官方思路的一個擴展。
啟動分類
App的啟動主要分為:冷啟動、熱啟動和溫啟動。
冷啟動:
耗時最多,也是整個應(yīng)用啟動時間的衡量標(biāo)準(zhǔn)。我們通過一張圖來看下冷啟動經(jīng)歷的流程:
熱啟動:
啟動最快,應(yīng)用直接由后臺切換到前臺。
溫啟動:
啟動較快,是介于冷啟動和熱啟動之間的一種啟動方式,溫啟動只會執(zhí)行Activity相關(guān)的生命周期方法,不會執(zhí)行進程的創(chuàng)建等操作。
我們優(yōu)化的方向和重點主要是冷啟動。因為它才是代表了應(yīng)用從被用戶點擊到最后的頁面繪制完成所耗費的所有時間。下面我們通過一張流程圖來看下冷啟動相關(guān)的任務(wù)流程:
看上面的任務(wù)的流程圖,讀者朋友們覺得哪些是我們優(yōu)化的方向呢?其實我們能做的只有Application和Activity的生命周期階段,因為其他的都是系統(tǒng)創(chuàng)建的我們沒法干預(yù),比如:啟動App,加載空白Window,創(chuàng)建進程等。這里面加載空白Window我們其實可以做一個假的優(yōu)化就是使用一張啟動圖來替換空白Window,具體操作我們在下文中介紹。
啟動的測量方式
這里主要介紹兩種方式:ADB命令和手動打點。下面我們就來看下兩者的使用以及優(yōu)缺點。
ADB命令:
在Android Studio的Terminal中輸入以下命令
- adb shell am start -W packagename/[packagename].首屏Activity
執(zhí)行之后控制臺中輸出如下內(nèi)容:
- Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.optimize.performance/.MainActivity }
- Status: ok
- Activity: com.optimize.performance/.MainActivity
- ThisTime: 563
- TotalTime: 563
- WaitTime: 575
- Complete
其中主要有三個字端:ThisTime、TotalTime和WaitTime,分別解釋下這三個字端的含義:
ThisTime:最后一個Activity啟動耗時
TotalTime:所有Activity啟動耗時
WaitTime:AMS啟動Activity的總耗時
ThisTime和TotalTime時間相同是因為我們的Demo中沒有Splash界面,應(yīng)用執(zhí)行完Application后直接就開始了MainActivity了。所以正常情況下的啟動耗時應(yīng)是這樣的:ThisTime < TotalTime < WaitTime
這就是ADB方式統(tǒng)計的啟動時間,細心的讀者應(yīng)該能想到了就是這種方式在線下使用很方便,但是卻不能帶到線上,而且這種統(tǒng)計的方式是非嚴謹、精確的時間。
手動打點方式:
手動打點方式就是啟動時埋點,啟動結(jié)束埋點,取二者差值即可。
我們首先需要定義一個統(tǒng)計時間的工具類:
- class LaunchRecord {
- companion object {
- private var sStart: Long = 0
- fun startRecord() {
- sStart = System.currentTimeMillis()
- }
- fun endRecord() {
- endRecord("")
- }
- fun endRecord(postion: String) {
- val cost = System.currentTimeMillis() - sStart
- println("===$postion===$cost")
- }
- }
- }
啟動時埋點我們直接在Application的attachBaseContext中進行打點。那么啟動結(jié)束應(yīng)該在哪里打點呢?這里存在一個誤區(qū):網(wǎng)上很多資料建議是在Activity的onWindowFocusChange中進行打點,但是onWindowFocusChange這個回調(diào)只是表示首幀開始繪制了,并不能表示用戶已經(jīng)看到頁面數(shù)據(jù)了,我們既然做啟動優(yōu)化,那么就要切切實實的得出用戶從點擊應(yīng)用圖標(biāo)到看到頁面數(shù)據(jù)之間的時間差值。所以結(jié)束埋點建議是在頁面數(shù)據(jù)展示出來進行埋點。比如頁面是個列表那就是第一條數(shù)據(jù)顯示出來,或者其他的任何view的展示。
- class MyApplication : Application() {
- override fun attachBaseContext(base: Context?) {
- super.attachBaseContext(base)
- //開始打點
- LaunchRecord.startRecord()
- }
- }
我們分別監(jiān)聽頁面view的繪制完成時間和onWindowFocusChanged回調(diào)兩個值進行對比。
- class MainActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- mTextView.viewTreeObserver.addOnDrawListener {
- LaunchRecord.endRecord("onDraw")
- }
- }
- override fun onWindowFocusChanged(hasFocus: Boolean) {
- super.onWindowFocusChanged(hasFocus)
- LaunchRecord.endRecord("onWindowFocusChanged")
- }
- }
打印的數(shù)據(jù)為:
- ===onWindowFocusChanged===322
- ===onDraw===328
可以很明顯看到onDraw所需要的時長是大于onWindowFocusChanged的時間的。因為我們這個只是簡單的數(shù)據(jù)展示沒有進行網(wǎng)絡(luò)相關(guān)請求和復(fù)雜布局所以差別不大。
這里需要說明下:addOnDrawListener 需要大于API 16才可以使用,如果為了兼顧老版本用戶可以使用addOnPre DrawListener來代替。
手動打點方式統(tǒng)計的啟動時間比較精確而且可以帶到線上使用,推薦這種方式。但在使用的時候要避開一個誤區(qū)就是啟動結(jié)束的埋點我們要采用Feed第一條數(shù)據(jù)展示出來來進行統(tǒng)計。同時addOnDrawListener要求API 16,這兩點在使用的時候需要注意的。
優(yōu)化工具的選擇
在做啟動優(yōu)化的時候我們可以借助三方工具來更好的幫助我們理清各個階段的方法或者線程、CPU的執(zhí)行耗時等情況。主要介紹以下兩個工具,我在這里就簡單介紹下,讀者朋友們可以線下自己取嘗試下。
TraceView:
TraceView是以圖形的形式展示執(zhí)行時間、調(diào)用棧等信息,信息比較全面,包含所有線程。
使用:
- 開始:Debug.startMethodTracing("name" )
- 結(jié)束:Debug.stopMethodTracing("" )
最后會生成一個文件在SD卡中,路徑為:Andrid/data/packagename/files。
因為traceview收集的信息比較全面,所以會導(dǎo)致運行開銷嚴重,整體APP的運行會變慢,這就有可能會帶偏我們優(yōu)化的方向,因為我們無法區(qū)分是不是traceview影響了啟動時間。
SysTrace:
Systrace是結(jié)合Android內(nèi)核數(shù)據(jù),生成HTML報告,從報告中我們可以看到各個線程的執(zhí)行時間以及方法耗時和CPU執(zhí)行時間等。API 18以上使用,推薦使用TraceCompat,因為這是兼容的API。
使用:
- 開始:TraceCompat.beginSection("tag ")
- 結(jié)束:TraceCompat.endSection()
然后執(zhí)行腳本:
- python systrace.py -b 32768 -t 10 -a packagename -o outputfile.html sched gfx view wm am app
給大家解釋下各個字端的含義:
- -b 收集數(shù)據(jù)的大小
- -t 時間
- -a 監(jiān)聽的應(yīng)用包名
- -o 生成文件的名稱
Systrace開銷較小,屬于輕量級的工具,并且可以直觀反映CPU的利用率。這里需要說明下在生成的報告中,當(dāng)你看某個線程執(zhí)行耗時時會看到兩個字端分別好似walltime和cputime,這兩個字端給大家解釋下就是walltime是代碼執(zhí)行的時間,cputime是代碼真正消耗cpu的執(zhí)行時間,cputime才是我們優(yōu)化的重點指標(biāo)。這點很容易被大家忽視。
優(yōu)雅獲取方法耗時
上文中主要是講解了如何監(jiān)聽整體的應(yīng)用啟動耗時,那么我們?nèi)绾巫R別某個方法所執(zhí)行的耗時呢?
我們常規(guī)的做法和上文中一樣也是打點,如:
- public class MyApp extends Application {
- @Override
- public void onCreate() {
- super.onCreate();
- initFresco();
- initBugly();
- initWeex();
- }
- private void initWeex(){
- LaunchRecord.Companion.startRecord();
- InitConfig config = new InitConfig.Builder().build();
- WXSDKEngine.initialize(this, config);
- LaunchRecord.Companion.endRecord("initWeex");
- }
- private void initFresco() {
- LaunchRecord.Companion.startRecord();
- Fresco.initialize(this);
- LaunchRecord.Companion.endRecord("initFresco");
- }
- private void initBugly() {
- LaunchRecord.Companion.startRecord();
- CrashReport.initCrashReport(getApplicationContext(), "注冊時申請的APPID", false);
- LaunchRecord.Companion.endRecord("initBugly");
- }
- }
控制臺打?。?nbsp;
- =====initFresco=====278
- =====initBugly=====76
- =====initWeex=====83
但是這種方式導(dǎo)致代碼不夠優(yōu)雅,并且侵入性強而且工作量大,不利于后期維護和擴展。
下面我給大家介紹另外一種方式就是AOP。AOP是面向切面變成,針對同一類問題的統(tǒng)一處理,無侵入添加代碼。
我們主要使用的是AspectJ框架,在使用之前呢給大家簡單介紹下相關(guān)的API:
- Join Points 切面的地方:函數(shù)調(diào)用、執(zhí)行,獲取設(shè)置變量,類初始化
- PointCut:帶條件的JoinPoints
- Advice:Hook 要插入代碼的位置。
- Before:PointCut之前執(zhí)行
- After:PointCut之后執(zhí)行
- Around:PointCut之前之后分別執(zhí)行
具體代碼如下:
- @Aspect
- public class AOPJava {
- @Around("call(* com.optimize.performance.MyApp.**(..))")
- public void applicationFun(ProceedingJoinPoint joinPoint) {
- Signature signature = joinPoint.getSignature();
- String name = signature.toShortString();
- long time = System.currentTimeMillis();
- try {
- joinPoint.proceed();
- } catch (Throwable throwable) {
- throwable.printStackTrace();
- }
- Log.d("AOPJava", name + " == cost ==" + (System.currentTimeMillis() - time));
- }
- }
控制臺打印結(jié)果如下:
- MyApp.initFresco() == cost ==288
- MyApp.initBugly() == cost ==76
- MyApp.initWeex() == cost ==85
但是我們沒有在MyApp中做任何改動,所以采用AOP的方式來統(tǒng)計方法耗時更加方便并且代碼無侵入性。具體AspectJ的使用學(xué)習(xí)后續(xù)文章來介紹。
異步優(yōu)化
上文中我們主要是講解了一些耗時統(tǒng)計的方法策略,下面我們就來具體看下如何進行啟動耗時的優(yōu)化。
在啟動分類中我們講過應(yīng)用啟動任務(wù)中有一個空白window,這是可以作為優(yōu)化的一個小技巧就是Theme的切換,使用一個背景圖設(shè)置給Activity,當(dāng)Activity打開后再將主題設(shè)置回來,這樣會讓用戶感覺很快。但其實從技術(shù)角度講這種優(yōu)化并沒有效果,只是感官上的快。
首先現(xiàn)在res/drawable中新建lanucher.xml文件:
- <layer-list xmlns:android="http://schemas.android.com/apk/res/android"
- android:opacity="opaque">
- <item android:drawable="@android:color/white"/>
- <item>
- <bitmap
- android:src="@mipmap/你的圖片"
- android:gravity="fill"/>
- </item>
- </layer-list>
將其設(shè)置給第一個打開的Activity,如MainActivity:
- <activity android:name=".MainActivity"
- android:theme="@style/Theme.Splash">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
- </activity>
最后在MainActivity中的onCreate的spuer.onCreate()中將其設(shè)置會原來的主題:
- override fun onCreate(savedInstanceState: Bundle?) {
- setTheme(R.style.AppTheme)
- super.onCreate(savedInstanceState)
- }
- }
這樣就完成了Theme主題的切換。
下面我們說下異步優(yōu)化,異步優(yōu)化顧名思義就是采用異步的方式進行任務(wù)的初始化。新建子線程(線程池)分擔(dān)主線稱任務(wù)并發(fā)的時間,充分利用CPU。
如果使用線程池那么設(shè)置多少個線程合適呢?這里我們參考了AsyncTask源碼中的設(shè)計,獲取可用CPU的數(shù)量,并且根據(jù)這個數(shù)量計算一個合理的數(shù)值。
- private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
- private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
- @Override
- public void onCreate() {
- super.onCreate();
- ExecutorService pool = Executors.newFixedThreadPool(CORE_POOL_SIZE);
- pool.submit(new Runnable() {
- @Override
- public void run() {
- initFresco();
- }
- });
- pool.submit(new Runnable() {
- @Override
- public void run() {
- initBugly();
- }
- });
- pool.submit(new Runnable() {
- @Override
- public void run() {
- initWeex();
- }
- });
- }
這樣我們就將所有的任務(wù)進行異步初始化了。我們看下未異步的時間和異步的對比:
- 未異步時間:======210
- 異步的時間:======3
可以看出這個時間差還是比較明顯的。這里還有另外一個問題就是,比如異步初始化Fresco,但是在MainActivity一加載就要使用而Fresco是異步加載的有可能這時候還沒有加載完成,這樣就會拋異常了,怎么辦呢?這里教大家一個新的技巧就是使用CountDownLatch,如:
- private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
- private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
- //1表示要被滿足一次countDown
- private CountDownLatch mCountDownLatch = new CountDownLatch(1);
- @Override
- public void onCreate() {
- super.onCreate();
- ExecutorService pool = Executors.newFixedThreadPool(CORE_POOL_SIZE);
- pool.submit(new Runnable() {
- @Override
- public void run() {
- initFresco();
- //調(diào)用一次countDown
- mCountDownLatch.countDown();
- }
- });
- pool.submit(new Runnable() {
- @Override
- public void run() {
- initBugly();
- }
- });
- pool.submit(new Runnable() {
- @Override
- public void run() {
- initWeex();
- }
- });
- try {
- //如果await之前沒有調(diào)用countDown那么就會一直阻塞在這里
- mCountDownLatch.await();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
這樣就會一直阻塞在await這里,直到Fresco初始化完成。
以上這種方式大家覺得如何呢?可以解決異步問題,但是我的Demo中只有三個需要初始化的任務(wù),在我們真實的項目中可不止,所以在項目中我們需要書寫很多的子線程代碼,這樣顯然是不夠優(yōu)雅的。部分代碼需要在初始化的時候就要完成,雖然可以使用countDowmLatch,但是任務(wù)較多的話,也是比較麻煩的,另外就是如果任務(wù)之間存在依賴關(guān)系,這種使用異步就很難處理了。
針對上面這些問題,我給大家介紹一種新的異步方式就是啟動器。核心思想就是充分利用CPU多核,自動梳理任務(wù)順序。核心流程:
- 任務(wù)代碼Task化,啟動邏輯抽象為Task
- 根據(jù)所有任務(wù)依賴關(guān)系排序生成一個有向無環(huán)圖
- 多線程按照排序后的優(yōu)先級依次執(zhí)行
- TaskDispatcher.init(PerformanceApp.)TaskDispatcher dispatcher = TaskDispatcher.createInstance()dispatcher.addTask(InitWeexTask())
- .addTask(InitBuglyTask())
- .addTask(InitFrescoTask())
- .start()dispatcher.await()LaunchTimer.endRecord()
最后代碼會變成這樣,具體的實現(xiàn)有向無環(huán)圖邏輯因為代碼量很多,不方便貼出來,大家可以關(guān)注公眾號獲取。
使用有向無環(huán)圖可以很好的梳理出每個任務(wù)的執(zhí)行邏輯,以及它們之間的依賴關(guān)系
延遲初始化
關(guān)于延遲初始化方案這里介紹兩者方式,一種是比較常規(guī)的做法,另外一個是利用IdleHandler來實現(xiàn)。
常規(guī)做法就是在Feed顯示完第一條數(shù)據(jù)后進行異步任務(wù)的初始化。比如:
- override fun onCreate(savedInstanceState: Bundle?) {
- setTheme(R.style.AppTheme)
- super.onCreate(savedInstanceState)
- mTextView.viewTreeObserver.addOnDrawListener {
- // initTask()
- }
- }
這里有個問題就是更新UI是在Main線程執(zhí)行的,所以做初始化任務(wù)等耗時操作時會發(fā)生UI的卡頓,這時我們可以使用Handler.postDelay(),但是delay多久呢?這個時間是不好控制的。所以這種常規(guī)的延遲初始化方案有可能會導(dǎo)致頁面的卡頓,并且延遲加載的時機不好控制。
IdleHandler方式就是利用其特性,只有CPU空閑的時候才會執(zhí)行相關(guān)任務(wù),并且我們可以分批進行任務(wù)初始化,可以有效緩解界面的卡頓。代碼如下:
- public class DelayInitDispatcher {
- private Queue<Task> mDelayTasks = new LinkedList<>();
- private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
- @Override
- public boolean queueIdle() {
- if (mDelayTasks.size() > 0) {
- Task task = mDelayTasks.poll();
- new DispatchRunnable(task).run();
- }
- return !mDelayTasks.isEmpty();
- }
- };
- public DelayInitDispatcher addTask(Task task) {
- mDelayTasks.add(task);
- return this;
- }
- public void start() {
- Looper.myQueue().addIdleHandler(mIdleHandler);
- }
- }
我們在界面顯示的后進行調(diào)用:
- override fun onCreate(savedInstanceState: Bundle?) {
- setTheme(R.style.AppTheme)
- super.onCreate(savedInstanceState)
- mTextView.viewTreeObserver.addOnDrawListener {
- val delayInitDispatcher = DelayInitDispatcher()
- delayInitDispatcher.addTask(DelayInitTaskA())
- .addTask(DelayInitTaskB())
- .start()
- }
- }
這樣就可以利用系統(tǒng)空閑時間來延遲初始化任務(wù)了。
懶加載
懶加載就是有些Task只有在特定的頁面才會使用,這時候我們就沒必要將這些Task放在Application中初始化了,我們可以將其放在進入頁面后在進行初始化。
其他方案
提前加載SharedPreferences,當(dāng)我們項目的sp很大的時候初次加載很耗內(nèi)存和時間的,我們可以將其提前在初始化Multidex(如果使用的話)之前進行初始化,充分利用此階段的CPU。
啟動階段不啟動子進程,子進程會共享CPU資源,導(dǎo)致主CPU資源緊張,另外一點就是在Application生命周期中也不要啟動其他的組件如:service、contentProvider。
異步類加載方式,如何確定哪些類是需要提前異步加載呢?這里我們可以自定義classload,替換掉系統(tǒng)的classload,在我們的classload中打印日志,每個類在加載的時候都會觸發(fā)的log日志,然后在項目中運行一遍,這樣就拿到了所有需要加載的類了,這些就是需要我們異步加載的類。
- Class.forName()只加載類本身及其靜態(tài)變量的引用類
- new實例可以額外加載類成員的引用類
總結(jié)
本文主要是講解了啟動耗時的檢測,從整體流程的耗時到各個方法的耗時以及線程的耗時,也介紹了工具的選擇和使用,介紹了啟動時間的優(yōu)化,異步加載、延遲加載、懶加載等等,從常規(guī)方法到更優(yōu)解,講解了很多方式方法,希望能給大家提供一些新的思路和解決問題的方式。也希望大家能在自己的項目中實戰(zhàn)總結(jié)。