Android性能優化之啓動優化實戰

本文首發於微信公衆號「Android開發之旅」,歡迎關注html

前言

本文將帶領你們來看看啓動優化相關方面的介紹以及各類優化的方法。但願你在讀完本章後會有所收穫。python

相信不少同窗都聽過八秒定律,八秒定律是在互聯網領域存在的一個定律,即指用戶訪問一個網站時,若是等待網頁打開的時間超過了8秒,就有超過70%的用戶放棄等待。足見啓動的時間是多麼的重要。放到移動APP中,那就是應用啓動的時間不能過久,不然就會形成用戶的流失。android

谷歌官方曾給出一篇App startup time的文章,這篇文章詳細介紹了關於啓動優化的切入點以及思路。感興趣的同窗能夠去看下。App Startup Time 這是官方地址。本篇文章也主要是官方思路的一個擴展。shell

啓動分類

App的啓動主要分爲:冷啓動、熱啓動和溫啓動。性能優化

冷啓動:

耗時最多,也是整個應用啓動時間的衡量標準。咱們經過一張圖來看下冷啓動經歷的流程:bash

冷啓動經歷的流程

熱啓動:

啓動最快,應用直接由後臺切換到前臺。微信

溫啓動:

啓動較快,是介於冷啓動和熱啓動之間的一種啓動方式,溫啓動只會執行Activity相關的生命週期方法,不會執行進程的建立等操做。網絡

咱們優化的方向和重點主要是冷啓動。由於它纔是表明了應用從被用戶點擊到最後的頁面繪製完成所耗費的全部時間。下面咱們經過一張流程圖來看下冷啓動相關的任務流程:多線程

冷啓動任務的流程

看上面的任務的流程圖,讀者朋友們以爲哪些是咱們優化的方向呢?其實咱們能作的只有Application和Activity的生命週期階段,由於其餘的都是系統建立的咱們無法干預,好比:啓動App,加載空白Window,建立進程等。這裏面加載空白Window咱們其實能夠作一個假的優化就是使用一張啓動圖來替換空白Window,具體操做咱們在下文中介紹。併發

啓動的測量方式

這裏主要介紹兩種方式:ADB命令和手動打點。下面咱們就來看下二者的使用以及優缺點。

ADB命令:

在Android Studio的Terminal中輸入如下命令

adb shell am start  -W packagename/[packagename].首屏Activity
複製代碼

執行以後控制檯中輸出以下內容:

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界面,應用執行完Application後直接就開始了MainActivity了。因此正常狀況下的啓動耗時應是這樣的:ThisTime < TotalTime < WaitTime

這就是ADB方式統計的啓動時間,細心的讀者應該能想到了就是這種方式在線下使用很方便,可是卻不能帶到線上,並且這種統計的方式是非嚴謹、精確的時間。

手動打點方式:

手動打點方式就是啓動時埋點,啓動結束埋點,取兩者差值便可。

咱們首先須要定義一個統計時間的工具類:

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中進行打點。那麼啓動結束應該在哪裏打點呢?這裏存在一個誤區:網上不少資料建議是在Activity的onWindowFocusChange中進行打點,可是onWindowFocusChange這個回調只是表示首幀開始繪製了,並不能表示用戶已經看到頁面數據了,咱們既然作啓動優化,那麼就要切切實實的得出用戶從點擊應用圖標到看到頁面數據之間的時間差值。因此結束埋點建議是在頁面數據展現出來進行埋點。好比頁面是個列表那就是第一條數據顯示出來,或者其餘的任何view的展現。

class MyApplication : Application() {
    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)
        //開始打點
        LaunchRecord.startRecord()
    }
}
複製代碼

咱們分別監聽頁面view的繪製完成時間和onWindowFocusChanged回調兩個值進行對比。

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")
    }
}
複製代碼

打印的數據爲:

===onWindowFocusChanged===322
===onDraw===328

複製代碼

能夠很明顯看到onDraw所須要的時長是大於onWindowFocusChanged的時間的。由於咱們這個只是簡單的數據展現沒有進行網絡相關請求和複雜佈局因此差異不大。

這裏須要說明下:addOnDrawListener 須要大於API 16纔可使用,若是爲了兼顧老版本用戶可使用addOnPre DrawListener來代替。

手動打點方式統計的啓動時間比較精確並且能夠帶到線上使用,推薦這種方式。但在使用的時候要避開一個誤區就是啓動結束的埋點咱們要採用Feed第一條數據展現出來來進行統計。同時addOnDrawListener要求API 16,這兩點在使用的時候須要注意的。

優化工具的選擇

在作啓動優化的時候咱們能夠藉助三方工具來更好的幫助咱們理清各個階段的方法或者線程、CPU的執行耗時等狀況。主要介紹如下兩個工具,我在這裏就簡單介紹下,讀者朋友們能夠線下本身取嘗試下。

TraceView:

TraceView是以圖形的形式展現執行時間、調用棧等信息,信息比較全面,包含全部線程。

使用:

開始:Debug.startMethodTracing("name" )
結束:Debug.stopMethodTracing("" )
複製代碼

最後會生成一個文件在SD卡中,路徑爲:Andrid/data/packagename/files。

由於traceview收集的信息比較全面,因此會致使運行開銷嚴重,總體APP的運行會變慢,這就有可能會帶偏咱們優化的方向,由於咱們沒法區分是否是traceview影響了啓動時間。

SysTrace:

Systrace是結合Android內核數據,生成HTML報告,從報告中咱們能夠看到各個線程的執行時間以及方法耗時和CPU執行時間等。API 18以上使用,推薦使用TraceCompat,由於這是兼容的API。

使用:

開始:TraceCompat.beginSection("tag ")
結束:TraceCompat.endSection()
複製代碼

而後執行腳本:

python systrace.py -b 32768 -t 10 -a packagename -o outputfile.html sched gfx view wm am app
複製代碼

給你們解釋下各個字端的含義:

  • -b 收集數據的大小
  • -t 時間
  • -a 監聽的應用包名
  • -o 生成文件的名稱

Systrace開銷較小,屬於輕量級的工具,而且能夠直觀反映CPU的利用率。這裏須要說明下在生成的報告中,當你看某個線程執行耗時時會看到兩個字端分別好似walltime和cputime,這兩個字端給你們解釋下就是walltime是代碼執行的時間,cputime是代碼真正消耗cpu的執行時間,cputime纔是咱們優化的重點指標。這點很容易被你們忽視。

優雅獲取方法耗時

上文中主要是講解了如何監聽總體的應用啓動耗時,那麼咱們如何識別某個方法所執行的耗時呢?

咱們常規的作法和上文中同樣也是打點,如:

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");
    }
}
複製代碼

控制檯打印:

=====initFresco=====278
=====initBugly=====76
=====initWeex=====83
複製代碼

可是這種方式致使代碼不夠優雅,而且侵入性強並且工做量大,不利於後期維護和擴展。

下面我給你們介紹另一種方式就是AOP。AOP是面向切面變成,針對同一類問題的統一處理,無侵入添加代碼。

咱們主要使用的是AspectJ框架,在使用以前呢給你們簡單介紹下相關的API:

  • Join Points 切面的地方:函數調用、執行,獲取設置變量,類初始化
  • PointCut:帶條件的JoinPoints
  • Advice:Hook 要插入代碼的位置。
  • Before:PointCut以前執行
  • After:PointCut以後執行
  • Around:PointCut以前以後分別執行

具體代碼以下:

@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));
​
    }
}
複製代碼

控制檯打印結果以下:

MyApp.initFresco() == cost ==288
MyApp.initBugly() == cost ==76
MyApp.initWeex() == cost ==85
複製代碼

可是咱們沒有在MyApp中作任何改動,因此採用AOP的方式來統計方法耗時更加方便而且代碼無侵入性。具體AspectJ的使用學習後續文章來介紹。

異步優化

上文中咱們主要是講解了一些耗時統計的方法策略,下面咱們就來具體看下如何進行啓動耗時的優化。

在啓動分類中咱們講過應用啓動任務中有一個空白window,這是能夠做爲優化的一個小技巧就是Theme的切換,使用一個背景圖設置給Activity,當Activity打開後再將主題設置回來,這樣會讓用戶感受很快。但其實從技術角度講這種優化並無效果,只是感官上的快。

首先如今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>
複製代碼

將其設置給第一個打開的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()中將其設置會原來的主題:

override fun onCreate(savedInstanceState: Bundle?) {
        setTheme(R.style.AppTheme)
        super.onCreate(savedInstanceState)
        }
​
    }
複製代碼

這樣就完成了Theme主題的切換。

下面咱們說下異步優化,異步優化顧名思義就是採用異步的方式進行任務的初始化。新建子線程(線程池)分擔主線稱任務併發的時間,充分利用CPU。

若是使用線程池那麼設置多少個線程合適呢?這裏咱們參考了AsyncTask源碼中的設計,獲取可用CPU的數量,而且根據這個數量計算一個合理的數值。

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();
            }
        });
​
    }
複製代碼

這樣咱們就將全部的任務進行異步初始化了。咱們看下未異步的時間和異步的對比:

未異步時間:======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();
                //調用一次countDown
                mCountDownLatch.countDown();
            }
        });
​
        pool.submit(new Runnable() {
            @Override
            public void run() {
                initBugly();
            }
        });
​
        pool.submit(new Runnable() {
            @Override
            public void run() {
                initWeex();
            }
        });
​
        try {
            //若是await以前沒有調用countDown那麼就會一直阻塞在這裏
            mCountDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
​
    }

複製代碼

這樣就會一直阻塞在await這裏,直到Fresco初始化完成。

以上這種方式你們以爲如何呢?能夠解決異步問題,可是個人Demo中只有三個須要初始化的任務,在咱們真實的項目中可不止,因此在項目中咱們須要書寫不少的子線程代碼,這樣顯然是不夠優雅的。部分代碼須要在初始化的時候就要完成,雖然可使用countDowmLatch,可是任務較多的話,也是比較麻煩的,另外就是若是任務之間存在依賴關係,這種使用異步就很難處理了。

針對上面這些問題,我給你們介紹一種新的異步方式就是啓動器。核心思想就是充分利用CPU多核,自動梳理任務順序。核心流程:

  • 任務代碼Task化,啓動邏輯抽象爲Task
  • 根據全部任務依賴關係排序生成一個有向無環圖
  • 多線程按照排序後的優先級依次執行
TaskDispatcher.init(PerformanceApp.)TaskDispatcher dispatcher = TaskDispatcher.createInstance()dispatcher.addTask(InitWeexTask())
        .addTask(InitBuglyTask())
        .addTask(InitFrescoTask())
        .start()dispatcher.await()LaunchTimer.endRecord()
複製代碼

最後代碼會變成這樣,具體的實現有向無環圖邏輯由於代碼量不少,不方便貼出來,你們能夠關注公衆號獲取。

使用有向無環圖能夠很好的梳理出每一個任務的執行邏輯,以及它們之間的依賴關係

延遲初始化

關於延遲初始化方案這裏介紹二者方式,一種是比較常規的作法,另一個是利用IdleHandler來實現。

常規作法就是在Feed顯示完第一條數據後進行異步任務的初始化。好比:

override fun onCreate(savedInstanceState: Bundle?) {
        setTheme(R.style.AppTheme)
        super.onCreate(savedInstanceState)
        
        mTextView.viewTreeObserver.addOnDrawListener {
            // initTask()
        }
​
    }

複製代碼

這裏有個問題就是更新UI是在Main線程執行的,因此作初始化任務等耗時操做時會發生UI的卡頓,這時咱們可使用Handler.postDelay(),可是delay多久呢?這個時間是很差控制的。因此這種常規的延遲初始化方案有可能會致使頁面的卡頓,而且延遲加載的時機很差控制。

IdleHandler方式就是利用其特性,只有CPU空閒的時候纔會執行相關任務,而且咱們能夠分批進行任務初始化,能夠有效緩解界面的卡頓。代碼以下:

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);
    }
​
}

複製代碼

咱們在界面顯示的後進行調用:

override fun onCreate(savedInstanceState: Bundle?) {
        setTheme(R.style.AppTheme)
        super.onCreate(savedInstanceState)
        
        mTextView.viewTreeObserver.addOnDrawListener {
            val delayInitDispatcher = DelayInitDispatcher()
            delayInitDispatcher.addTask(DelayInitTaskA())
                    .addTask(DelayInitTaskB())
                    .start()
        }
    }
複製代碼

這樣就能夠利用系統空閒時間來延遲初始化任務了。

懶加載

懶加載就是有些Task只有在特定的頁面纔會使用,這時候咱們就不必將這些Task放在Application中初始化了,咱們能夠將其放在進入頁面後在進行初始化。

其餘方案

提早加載SharedPreferences,當咱們項目的sp很大的時候初次加載很耗內存和時間的,咱們能夠將其提早在初始化Multidex(若是使用的話)以前進行初始化,充分利用此階段的CPU。

啓動階段不啓動子進程,子進程會共享CPU資源,致使主CPU資源緊張,另一點就是在Application生命週期中也不要啓動其餘的組件如:service、contentProvider。

異步類加載方式,如何肯定哪些類是須要提早異步加載呢?這裏咱們能夠自定義classload,替換掉系統的classload,在咱們的classload中打印日誌,每一個類在加載的時候都會觸發的log日誌,而後在項目中運行一遍,這樣就拿到了全部須要加載的類了,這些就是須要咱們異步加載的類。

  • Class.forName()只加載類自己及其靜態變量的引用類
  • new實例能夠額外加載類成員的引用類

總結

本文主要是講解了啓動耗時的檢測,從總體流程的耗時到各個方法的耗時以及線程的耗時,也介紹了工具的選擇和使用,介紹了啓動時間的優化,異步加載、延遲加載、懶加載等等,從常規方法到更優解,講解了不少方式方法,但願能給你們提供一些新的思路和解決問題的方式。也但願你們能在本身的項目中實戰總結。

推薦閱讀:

App性能概覽與平臺化實踐理論

Android性能優化之佈局優化實戰

如何監測Android應用卡頓?這篇就夠了

掃描下方二維碼關注公衆號,及時獲取文章推送。

掃一掃 關注公衆號
相關文章
相關標籤/搜索