一、啟動優(yōu)化概念
1.1、為什么要做啟動優(yōu)化?
APP優(yōu)化是我們進(jìn)階高級開發(fā)工程師的必經(jīng)之路,而APP啟動速度的優(yōu)化,也是我們開啟APP優(yōu)化的第一步。用戶在使用我們的軟件時(shí),交互最多最頻繁的也就是APP的啟動頁面,如果啟動頁面加載過慢,很可能造成用戶對我們APP的印象過差,進(jìn)而消耗了用戶的耐心,更嚴(yán)重可能導(dǎo)致用戶的卸載行為。這也是微信始終堅(jiān)持使用“一個(gè)小人望著地球”作為啟動頁面的背景,并且堅(jiān)持不添加啟動廣告的的原因。
1.2、啟動分類
冷啟動: 特點(diǎn)是耗時(shí)最多,同時(shí)它也是衡量標(biāo)準(zhǔn),我們在線上做的各種優(yōu)化都是以它作為標(biāo)準(zhǔn),從下面這張圖片可以看出冷啟動它經(jīng)歷了一系列的流程,所以它的耗時(shí)也是最多的。

熱啟動: 特點(diǎn)是最快,我們所說的熱啟動是指app從后臺切換到前臺,它沒有application的創(chuàng)建和各種生命周期的調(diào)用,所以說這種啟動方式是最快的。
溫啟動: 特點(diǎn)是較快,它的速度介于冷啟動和熱啟動之間,對于這種方式它會重走activity的生命周期,不會重走進(jìn)程的創(chuàng)建,application的創(chuàng)建和生命周期等流程。
1.3、相關(guān)任務(wù)
冷啟動之前:
- 啟動App;
- 加載空白Window;
- 創(chuàng)建進(jìn)程。
這三個(gè)任務(wù)都是系統(tǒng)行為,無法進(jìn)行真正的干預(yù)。網(wǎng)上大多介紹啟動優(yōu)化的都是針對第2條,但其實(shí)這是一個(gè)假的干預(yù),只是對我們?nèi)庋鄹兄系囊粋€(gè)優(yōu)化。
之后進(jìn)行的是:
- 創(chuàng)建Application;
- 啟動主線程;
- 創(chuàng)建MainActivity;
- 加載布局;
- 布置屏幕;
- 首幀繪制。
我們的優(yōu)化方向: Application和Activity生命周期的這個(gè)階段,這是開發(fā)者真正可以控制的時(shí)間。
二、啟動時(shí)間測量方式
這里介紹兩種啟動時(shí)間的測量方式:
- adb命令
- 手動打點(diǎn)
2.1、adb命令
這種方式是我們通過在終端輸入一條adb命令,然后它會打開我們要測試的app,同時(shí)進(jìn)行結(jié)果的輸出。具體的命令如下:
adb shell am start -W packagename/首屏Activity(這里需要使用全類名)
這里我以自己寫的一個(gè)簡單的列表展示的Demo工程舉例說明:

ThisTime:最后一個(gè)Activity啟動耗時(shí)
TotalTime:所有Activity啟動耗時(shí)(這里ThisTime和TotalTime值是一致的,因?yàn)槲业腄emo中只有一個(gè)MainActivity)
WaitTime:AMS啟動Activity的總耗時(shí),對于一個(gè)通用的app(包含SplashActivity),ThisTime肯定是小于TotalTime的,即:
ThisTime < TotalTime < WaitTime
總結(jié):這種方式線下使用方便,可以使用這種方式測量競品為競品分析提供需要的數(shù)據(jù),不能帶到線上,并且測量出來的時(shí)間也是一個(gè)非嚴(yán)謹(jǐn)精確的時(shí)間。
2.2、手動打點(diǎn)
這種方式是在app啟動開始時(shí)埋點(diǎn),啟動結(jié)束時(shí)埋點(diǎn),然后計(jì)算二者差值。
實(shí)際使用中,一般將開始時(shí)間這個(gè)點(diǎn)埋在Application的attachBaseContext(Context base)這個(gè)方法中,這是整個(gè)應(yīng)用所能接收到的最早的回調(diào)時(shí)機(jī)。開始時(shí)間有了,那么結(jié)束時(shí)間該怎么計(jì)算呢,也就是我們應(yīng)該把結(jié)束時(shí)間這個(gè)點(diǎn)埋在什么位置呢?網(wǎng)上很多資料里都會說是在onWindowFocusChanged()這個(gè)方法里做啟動結(jié)束的時(shí)間計(jì)算,但是實(shí)際上寫在這里其實(shí)是有問題的。
誤區(qū):onWindowFocusChanged它只是Activit的首幀時(shí)間,是activity首次繪制的時(shí)間,并不能代表activity已經(jīng)展現(xiàn)出來。我們做性能優(yōu)化的目的是為了改善用戶的體驗(yàn),并不是單純的為了把啟動時(shí)間縮短,因?yàn)檫@樣做是不準(zhǔn)確的,我們需要的是用戶真正看到界面的時(shí)間,所以正確的情況應(yīng)該是在真實(shí)的數(shù)據(jù)展示(一般取第一條)出來,才算結(jié)束的時(shí)間節(jié)點(diǎn)。
下面我們就來實(shí)戰(zhàn)一下該如何在代碼中埋點(diǎn)統(tǒng)計(jì)啟動時(shí)間? 首先我們定義一個(gè)工具類LaunchTime,用來計(jì)算差值時(shí)間:
package com.jarchie.performance.utils;
import android.util.Log;
/**
* 描述: 打點(diǎn)計(jì)算啟動時(shí)間
*/
public class LaunchTime {
private static long sTime;
public static void startRecord() {
sTime = System.currentTimeMillis();
}
public static void endRecord(String msg) {
long cost = System.currentTimeMillis() - sTime;
Log.i(msg, "--->cost" + cost);
}
}
然后在Application中埋下開始時(shí)間點(diǎn):
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
LaunchTime.startRecord();
}
然后在我們列表適配器中的onBindViewHolder中綁定數(shù)據(jù)時(shí)統(tǒng)計(jì)第一條Item展示出來的時(shí)間點(diǎn):
if (position ==0 && !mHasRecorded){
mHasRecorded = true;
holder.mAllLayout.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
holder.mAllLayout.getViewTreeObserver().removeOnPreDrawListener(this);
LaunchTime.endRecord("FirstShow");
return true;
}
});
}
最后我們在MainActivity中的onWindowFocusChanged()方法中統(tǒng)計(jì)一下Activity的首幀時(shí)間:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
LaunchTime.endRecord("onWindowFocusChanged");
}
現(xiàn)在來運(yùn)行我們的程序,看一下最終統(tǒng)計(jì)出來的時(shí)間值究竟是多少?

由上面的結(jié)果可以看出首幀時(shí)間是904毫秒,列表數(shù)據(jù)的第一條展示的時(shí)間是1573毫秒,兩者之間的時(shí)間差值是超過200毫秒的,這也就表明如果我們僅以Activity的首幀時(shí)間作為啟動結(jié)束,那么這個(gè)時(shí)間明顯是偏早的,不符合我們做啟動優(yōu)化的初衷。
三、啟動優(yōu)化工具
以下所介紹的兩種方式是互相補(bǔ)充的,我們需要正確認(rèn)識工具并且能夠在不同的場景下選擇適合的工具。
3.1、traceview
特點(diǎn):
- 圖形的形式展示執(zhí)行時(shí)間、調(diào)用棧等
- 信息全面,包含所有線程
使用方式:在代碼中需要做性能分析的地方開始位置和結(jié)束位置插入以下代碼
- Debug.startMethodTracing(""); //該方法具有重載方法,可設(shè)置收取信息的路徑,大小
- Debug.stopMethodTracing("");
- 生成文件在sd卡:Android/data/packagename/files
代碼實(shí)戰(zhàn):

將我們的項(xiàng)目運(yùn)行之后(注意開啟運(yùn)行時(shí)權(quán)限,這部分不是本節(jié)重點(diǎn),我直接到應(yīng)用里將權(quán)限開啟了)生成文件如下:

將生成的文件打開,如下圖所示,可以看到Threads中是這個(gè)應(yīng)用所有的線程數(shù),我們可以看到線程總數(shù)以及對應(yīng)的每個(gè)線程在具體的時(shí)間都做了哪些操作,然后下面有四個(gè)Tab可以切換,首先來看Call Chart,可以看到在具體的每一行都指向了具體的函數(shù)調(diào)用,將鼠標(biāo)移到對應(yīng)的每一行上面都有具體的執(zhí)行時(shí)間等信息,沿著垂直方向看是具體的調(diào)用者,比如a調(diào)用b,則a在上方b在下方,而且不同的api它的顏色也是不一樣的,對于系統(tǒng)api是橙色的,對于應(yīng)用自身的函數(shù)調(diào)用顏色是綠色的,對于第三方api調(diào)用顏色是藍(lán)色的(包括java語言的api)。

接著來看Flame Chart(又叫火焰圖),它是一個(gè)倒置的調(diào)用圖表,一般來說它的作用沒有第一個(gè)大,它會收集相同的調(diào)用方順序完全相同的函數(shù),比如a調(diào)b調(diào)c并且調(diào)用了多次,它會將它們收集在一起:

下面這張圖是Top Down,它比較直觀的展示了函數(shù)的調(diào)用列表,比如下圖中首先main()函數(shù)調(diào)用了init(),init()又調(diào)用了g()等等,相當(dāng)于Call Chart詳細(xì)版,并且你將鼠標(biāo)放在對應(yīng)函數(shù)上右鍵有個(gè)Jump to Source可以跳轉(zhuǎn)到具體的代碼中。
Total Time是某個(gè)函數(shù)執(zhí)行的總時(shí)間,Self Time是該函數(shù)體內(nèi)部自有代碼執(zhí)行的時(shí)間,Childre Time是該函數(shù)內(nèi)部調(diào)用別的函數(shù)所需的時(shí)間,后面二者的時(shí)間總和一定是等于前面的Total Time的,這點(diǎn)需要注意。Self Time上方有一欄下拉菜單,即我們第一張圖中紅色標(biāo)注的菜單欄WallClockTime和ThreadTime,前者是這段代碼執(zhí)行所消耗的時(shí)間,后者是CPU執(zhí)行的時(shí)間,一般情況下是前者大于后者,因?yàn)橐话闱闆r下某個(gè)函數(shù)消耗的時(shí)間并不等于CPU真正消耗的時(shí)間。

最后是Bottom Up,這個(gè)的作用也是比較小了,它和Top Down是相反的,它會告訴你某個(gè)函數(shù)具體是誰調(diào)用了它:

總結(jié):一般比較關(guān)注的是Call Chart和Top Down
- 運(yùn)行時(shí)開銷嚴(yán)重,整體都會變慢(它會抓取當(dāng)前運(yùn)行的所有線程的所有執(zhí)行函數(shù)和順序)
- 由于它非常嚴(yán)重的運(yùn)行時(shí)開銷,所以它很有可能回帶偏優(yōu)化方向
3.2、systrace(python腳本)
特點(diǎn):
- 結(jié)合Android內(nèi)核的數(shù)據(jù),生成Html報(bào)告
- API18以上使用,推薦TraceCompat
使用方式:
- 一段python腳本:python systrace.py -t 10 [other-options][categories]
- 詳見官方文檔:developer.android.google.cn/topic/perfo…
我這里放一張我自己運(yùn)行的示例,僅供參考:

代碼實(shí)戰(zhàn):

首先將項(xiàng)目運(yùn)行讓我們寫的代碼生效,然后運(yùn)行我們的python腳本,啟動tracing之后,點(diǎn)擊我們的app讓它開始收集信息,tracing完成之后到對應(yīng)的目錄就會發(fā)現(xiàn)已經(jīng)生成了我們的Performance.html文件,我們到瀏覽器中打開,如下圖所示:

由上圖中左側(cè)可以看到有CPU的核心數(shù),往下滑動還可以看到各個(gè)線程名稱,然后還可以根據(jù)代碼中打的Tag來搜索,下方會展示比較詳細(xì)的trace信息(上圖中也舉例說明了,需要注意的Wall Time和CPU Time紅色部分圈出),點(diǎn)到右側(cè)圖中具體的位置都會展示出比較詳細(xì)的方法名稱執(zhí)行時(shí)間等信息。
總結(jié):
- 輕量級,開銷小(它是你在哪里埋點(diǎn),它就處理哪里,這點(diǎn)和traceview不同,需要注意)
- 直觀反映cpu利用率
- 需要注意cputime與walltime區(qū)別: walltime是代碼執(zhí)行時(shí)間,cputime是代碼消耗cpu的時(shí)間(優(yōu)化的重點(diǎn)指標(biāo))。舉個(gè)栗子:鎖沖突(比如現(xiàn)在調(diào)用了A方法,進(jìn)入A方法之后需要一把鎖,但此時(shí)這把鎖被B所持有,導(dǎo)致代碼在A這里停下了,實(shí)際上可能這個(gè)A函數(shù)并不耗時(shí),但是由于一直拿不到鎖,所以一直處于等待狀態(tài),這就導(dǎo)致walltime時(shí)間很長,但是它實(shí)際上對CPU并沒有多少消耗)
關(guān)于上述工具的詳細(xì)使用方法大家可以自行百度或者谷歌查找相關(guān)資料,認(rèn)真學(xué)習(xí)一下這些分析工具的使用。
四、優(yōu)雅獲取方法耗時(shí)
4.1、常規(guī)方式
我們在做啟動優(yōu)化的時(shí)候通常需要知道啟動階段所有方法的耗時(shí),這樣可以有針對性的分析出耗時(shí)較多的方法。一般的實(shí)現(xiàn)方式就是通過手動埋點(diǎn)來實(shí)現(xiàn),比如在某個(gè)方法開始和結(jié)束的位置分別插入以下代碼:
long time = System.currentTimeMillis();
initJpush();
long cost = System.currentTimeMillis() - time;
//或者可以使用這行代碼:SystemClock.currentThreadTimeMillis();
//CPU真正執(zhí)行的時(shí)間
當(dāng)有多個(gè)方法需要埋點(diǎn)時(shí),同理這樣寫就可以獲取到每個(gè)方法的執(zhí)行時(shí)間了,但是這樣操作存在的問題也是顯而易見的,當(dāng)然我相信你肯定也發(fā)現(xiàn)了,主要總結(jié)為以下幾點(diǎn):
- 代碼重復(fù)、耦合度高并且看起來非常惡心
- 侵入性強(qiáng)
- 工作量大
那么針對這種方式的劣勢,如何才能更加優(yōu)雅的實(shí)現(xiàn)獲取方法的耗時(shí)呢?答案就是采用AOP的方式來實(shí)現(xiàn)。
4.2、AOP介紹
AOP簡介:Aspect Oriented Programming,面向切面編程
- 針對同一類問題的統(tǒng)一處理
- 無侵入添加代碼
AspectJ簡介:它就是輔助AOP用來實(shí)現(xiàn)切面編程
使用時(shí)首先需要添加如下的依賴:
//工程目錄下的build.gradle
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'
//app module目錄下的build.gradle
apply plugin: 'android-aspectjx'
implementation 'org.aspectj:aspectjrt:1.8.14'
添加完了依賴之后,再來介紹一下相關(guān)知識點(diǎn),然后我們再到代碼中去真正的使用它。
Join Points:程序運(yùn)行時(shí)的執(zhí)行點(diǎn),常用的可以作為切面的地方如下所示:
- 函數(shù)調(diào)用、執(zhí)行
- 獲取、設(shè)置變量
- 類初始化
PointCut:帶條件的JoinPoints
Advice:一種Hook,要插入代碼的位置
Before:PointCut之前執(zhí)行
After:PointCut之后執(zhí)行
Around:PointCut之前、之后分別執(zhí)行
-
舉個(gè)栗子:
@Before("execution(* android.app.Activity.on**(..))") public void onActivityCalled(JoinPoint joinPoint) throws Throwable{ ... }
語法簡介:
- Before:Advice,具體插入的位置
- execution:處理Join Point的類型,call、execution
- (* android.app.Activity.on**(..)):匹配規(guī)則,匹配android.app.Activity類中任意返回值類型的on開頭的是否有參數(shù)的方法都行
- onActivityCalled:要插入的代碼
代碼實(shí)現(xiàn):
/**
* 說明:使用AOP方式來統(tǒng)計(jì)方法耗時(shí)
*/
@Aspect //通過該注解,AOP框架可以知道該類即是需要需要插入的代碼
public class PerformanceAop {
@Around("call(* com.jarchie.performance.app.BaseApp.**(..))") //匹配規(guī)則
public void calculateTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature(); //拿到切點(diǎn)簽名
String name = signature.toShortString(); //拿到對應(yīng)的方法信息
long time = System.currentTimeMillis();
try {
joinPoint.proceed(); //手動執(zhí)行
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Log.i("執(zhí)行時(shí)間", name + "--->>>" + (System.currentTimeMillis() - time));
}
}
可以看到這里是新建了一個(gè)類采用AOP的方式來獲取方法耗時(shí),并沒有在BaseApp中添加任何的代碼,運(yùn)行結(jié)果如下所示:

總結(jié):
采用AOP實(shí)現(xiàn):
- 無侵入性
- 修改方便
五、異步優(yōu)化
5.1、Theme切換
首先需要說明的是這種方式僅僅是給用戶感官上的快,just a feeling,對應(yīng)用真實(shí)的啟動速度沒有任何的影響。它的實(shí)現(xiàn)原理是App在打開首屏Activity之前會首先顯示出一張圖片,當(dāng)Activity頁面真正展示出來之后再把Theme改變回來,因?yàn)槔鋯又杏幸徊绞莿?chuàng)建一個(gè)空白的Window,這種實(shí)現(xiàn)方式正式利用了這個(gè)空白的Window。下面來看下具體怎么操作:
首先定義一個(gè)背景drawable,這里起名為launcher.drawable:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
android:opacity="opaque">
<item android:drawable="@color/colorPrimary" />
<item>
<bitmap
android:gravity="center"
android:src="@drawable/liying" />
</item>
</layer-list>
然后在styles中定義一個(gè)主題作為啟動主題:
<style name="Theme.Splash" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowBackground">@drawable/launcher</item>
<item name="windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
</style>
然后在首屏Activity的清單文件中設(shè)置這個(gè)主題:
<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>
最后在首屏Activity的onCreate()方法中調(diào)用父類onCreate()方法之前將設(shè)置的啟動主題改為默認(rèn)主題:
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
}
來看下我們修改后的效果,如下圖所示:

5.2、常規(guī)異步優(yōu)化
核心思想:子線程分擔(dān)主線程任務(wù),并行減少時(shí)間
下面還以Application的onCreate()為例分析常規(guī)的異步優(yōu)化:現(xiàn)在的App一般情況下都是運(yùn)行在八核的設(shè)備上,不同的設(shè)備廠商可能分配給應(yīng)用的核數(shù)有的四核有的八核,但是如果像我們這里的代碼將所有的初始化工作都放在一個(gè)線程中最多占用一個(gè)核,別的三個(gè)核或者七個(gè)核都處于一個(gè)浪費(fèi)狀態(tài),那么為了讓CPU的利用率達(dá)到一個(gè)更加高效的狀態(tài),這里就需要使用異步初始化了。
說到異步,那大家想到的肯定是要創(chuàng)建子線程了,這里使用線程池來創(chuàng)建線程,這種方式更加優(yōu)雅,不僅可以在很大程度上避免內(nèi)存泄露,而且還可以讓線程得到復(fù)用(這里線程數(shù)的設(shè)置是參考了Android AsyncTask源碼中的設(shè)計(jì)):
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));
@SuppressLint("MissingPermission")
@Override
public void onCreate() {
super.onCreate();
LaunchTime.startRecord();
mApplication = this;
ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
service.submit(this::initDeviceId);
service.submit(this::initJpush);
service.submit(this::initBugly);
LaunchTime.endRecord("AppOnCreate");
}
來看下運(yùn)行結(jié)果:

可以看到時(shí)間確實(shí)是非常短的,那現(xiàn)在有個(gè)問題:是不是以后代碼都可以放在子線程中執(zhí)行呢?答案當(dāng)然是否定的,有些場景下并不能很好的實(shí)現(xiàn)異步的方案,比如:①有些代碼必須要在主線程中執(zhí)行;②有些方法必須在onCreate()方法結(jié)束后執(zhí)行完畢。
針對上面這兩種情況,異步的方案其實(shí)就不太好解決了,對于第一種情況你只能放棄異步方案,對于第二種情況,我們可以采用CountDownLatch這個(gè)類來解決,下面這段代碼的含義大致就是:只要countDownLatch不被滿足,它將一直處于等待狀態(tài),直到被滿足1次,因?yàn)槲覀儤?gòu)造函數(shù)中傳入的數(shù)值是1:
private CountDownLatch mCountDownLatch = new CountDownLatch(1);
ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
service.submit(() -> {
initBugly();
mCountDownLatch.countDown();
});
try {
mCountDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
異步優(yōu)化注意事項(xiàng):
- 不符合異步要求(如果能修改成符合要求的則修改,不能修改則放棄異步方案)
- 需要在某階段完成
- 區(qū)分CPU密集型和IO密集型任務(wù)
5.3、啟動器
通過上面的常規(guī)異步操作過程可以發(fā)現(xiàn)還是存在很多問題的,主要有以下幾點(diǎn):
- 代碼不優(yōu)雅(假如方法數(shù)較多時(shí),則會寫很多重復(fù)代碼)
- 場景不好處理(特定階段執(zhí)行完畢、依賴關(guān)系)
- 維護(hù)成本高
正是因?yàn)橛猩厦孢@些問題的存在,才有了下面的解決方案的產(chǎn)生——啟動器。
推薦阿里開源的一個(gè)啟動器庫alpha:github.com/alibaba/alp…
啟動器介紹:
核心思想:充分利用CPU多核,自動梳理任務(wù)順序
啟動器流程:
- 代碼Task化,啟動邏輯抽象為Task
- 根據(jù)所有任務(wù)依賴關(guān)系排序生成一個(gè)有向無環(huán)圖(自動生成的)
- 多線程按照排序后的優(yōu)先級依次執(zhí)行
啟動器流程圖:

代碼實(shí)戰(zhàn):首先構(gòu)建啟動器部分的代碼因?yàn)檫@個(gè)過程還是有點(diǎn)復(fù)雜的,代碼相對也不少,這里就不貼了,大家可以自行百度啟動器相關(guān)的實(shí)現(xiàn)代碼,這里只針對使用情況做一個(gè)說明:
首先我們需要將上面做異步操作的幾個(gè)方法抽成對應(yīng)的任務(wù),比如這里InitBuglyTask這個(gè)任務(wù)就是對應(yīng)用來解決需要在特定階段完成初始化的問題,重寫needWait()方法設(shè)置為true即需要等待,并且MainTask是運(yùn)行在主線程的:
public class InitBuglyTask extends MainTask {
//解決特定階段執(zhí)行完成問題
@Override
public boolean needWait() {
return true;
}
@Override
public void run() {
CrashReport.initCrashReport(mContext, "e296ad7fc8", false);
}
}
然后定義InitDeviceIdTask這個(gè)用來獲取設(shè)備ID的任務(wù),該任務(wù)是在子線程執(zhí)行的:
public class InitDeviceIdTask extends Task {
private String mDeviceId;
@SuppressLint("MissingPermission")
@Override
public void run() {
//真正自己的代碼
TelephonyManager tManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
mDeviceId = tManager.getDeviceId();
}
}
然后定義初始化極光推送的任務(wù)InitJpushTask,重寫dependsOn()方法用來解決依賴關(guān)系的問題,該任務(wù)的執(zhí)行依賴于設(shè)備ID:
public class InitJpushTask extends Task {
//解決依賴關(guān)系問題
@Override
public List<Class<? extends Task>> dependsOn() {
List<Class<? extends Task>> task = new ArrayList<>();
task.add(InitDeviceIdTask.class);
return task;
}
@Override
public void run() {
//推送
JPushInterface.init(mContext);
}
}
然后將這些任務(wù)添加到啟動器里面即可,代碼看起來還是比較美觀的:
LaunchTime.startRecord();
TaskDispatcher.init(this);
TaskDispatcher dispatcher = TaskDispatcher.createInstance();
dispatcher.addTask(new InitBuglyTask())
.addTask(new InitJpushTask())
.addTask(new InitDeviceIdTask())
.start();
dispatcher.await();
LaunchTime.endRecord("AppOnCreate");
OK,通過上面這幾行代碼啟動器就搞定了,可見它相比較于傳統(tǒng)的異步方式還是好處多多啊,最后來看下運(yùn)行結(jié)果吧:

六、延遲初始化
6.1、常規(guī)方案
對于實(shí)際項(xiàng)目經(jīng)驗(yàn)較多的朋友你會發(fā)現(xiàn)其實(shí)在Application或者M(jìn)ainActivity中有些任務(wù)它的優(yōu)先級并不是很高,所以對于這類任務(wù)通常都可以將它們進(jìn)行延遲初始化,一般都是延遲到列表數(shù)據(jù)展示之后再進(jìn)行加載。我們首先來看下常規(guī)的方案是如何實(shí)現(xiàn)的呢?最簡單的做法就是將代碼移到列表顯示之后進(jìn)行調(diào)用,或者是通過new Handler().postDelayed延遲一個(gè)時(shí)間調(diào)用。即:
- New Handler().postDelayed
- Feed展示后調(diào)用
下面我們在代碼中舉個(gè)栗子說明一下這種方案是如何實(shí)現(xiàn)的?
這里首先定義一個(gè)回調(diào)接口是在列表展示出來之后的回調(diào):
public interface OnFeedShowCallBack {
void onFeedShow();
}
然后在列表適配器中定義這個(gè)接口,并給它一個(gè)setXXX()方法,并且在列表item第一條展示出來之后回調(diào)這個(gè)接口:
private OnFeedShowCallBack mCallBack;
...
public void setOnFeedShowCallBack(OnFeedShowCallBack callBack){
this.mCallBack = callBack;
}
...
if (mCallBack!=null){
mCallBack.onFeedShow();
}
接著在MainActivity的onCreate()中設(shè)置這個(gè)回調(diào),并且讓MainActivity實(shí)現(xiàn)回調(diào)接口重寫回調(diào)方法,在回調(diào)方法中模擬執(zhí)行兩個(gè)Task,整個(gè)這個(gè)流程如果熟悉接口回調(diào)機(jī)制的兄弟應(yīng)該很好理解了:
mAdapter.setOnFeedShowCallBack(this);
...
@Override
public void onFeedShow() {
//模擬執(zhí)行了兩個(gè)Task,TaskA和TaskB
new DispatchRunnable(new DelayInitTaskA()).run();
new DispatchRunnable(new DelayInitTaskB()).run();
}
以上就是常規(guī)方案的實(shí)現(xiàn)方法,大家仔細(xì)思考一下會發(fā)現(xiàn)這其中是有很多問題的:首先,我們的列表展示是發(fā)生在主線程中,直接執(zhí)行mCallBack.onFeedShow()方法,會跑到MainActivity重寫的onFeedShow()中,如果模擬的任務(wù)執(zhí)行時(shí)間較長,那么主線程就會相應(yīng)的卡住對應(yīng)的時(shí)長,如果此時(shí)用戶滑動列表很明顯會造成列表滑動卡頓,給用戶的體驗(yàn)就很不好了。如果你采用new Handler().postDelayed發(fā)送延時(shí)消息來處理,當(dāng)然一定程度上是可以緩解這種卡段,但是這種方案總結(jié)下來延時(shí)的時(shí)機(jī)不太好控制并且如果任務(wù)數(shù)量較多也不易維護(hù),所以我們需要去尋求更加優(yōu)雅的解決方案。
6.2、優(yōu)雅實(shí)現(xiàn)延遲初始化
核心思想: 對延遲任務(wù)進(jìn)行分批初始化,這里利用IdleHandler特性,空閑執(zhí)行
針對這種方案我們在代碼中來實(shí)踐一下看看具體該如何操作?
首先來創(chuàng)建一個(gè)針對延遲初始化任務(wù)執(zhí)行的啟動器:
public class DelayInitDispatcher {
//創(chuàng)建任務(wù)隊(duì)列
private Queue<Task> mDelayTasks = new LinkedList<>();
//IdleHandler分批處理并在系統(tǒng)空閑時(shí)執(zhí)行
private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() { //系統(tǒng)空閑時(shí)回調(diào)
if(mDelayTasks.size()>0){
Task task = mDelayTasks.poll(); //分批執(zhí)行,每次只取一個(gè)Task
new DispatchRunnable(task).run();
}
return !mDelayTasks.isEmpty(); //DelayTasks為空則移除
}
};
//添加任務(wù)
public DelayInitDispatcher addTask(Task task){
mDelayTasks.add(task);
return this;
}
//啟動
public void start(){
Looper.myQueue().addIdleHandler(mIdleHandler);
}
}
具體的代碼含義都加了注釋了,主要就是利用了IdleHandler的特性在空閑時(shí)期執(zhí)行,接著在onFeedShow()的回調(diào)中添加任務(wù)并執(zhí)行即可:
@Override
public void onFeedShow() {
DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();
delayInitDispatcher.addTask(new DelayInitTaskA())
.addTask(new DelayInitTaskB())
.start();
}
通過代碼我們來比對一下兩種方案的差別:對于常規(guī)方案回調(diào)接口中有多少個(gè)任務(wù)都會一次性執(zhí)行完成,也就意味著主線程會卡在那里對應(yīng)的時(shí)間;對于第二種方案,我們是添加了多個(gè)任務(wù)進(jìn)來,執(zhí)行的時(shí)機(jī)是在系統(tǒng)空閑的時(shí)候進(jìn)行執(zhí)行,并且一次只執(zhí)行一個(gè),所以第二種方案的優(yōu)點(diǎn)就顯而易見了:
- 執(zhí)行時(shí)機(jī)明確;
- 有效緩解列表卡頓,它可以真正的提升用戶的體驗(yàn)。
七、啟動優(yōu)化其他方案
7.1、優(yōu)化總方針
- 異步、延遲、懶加載(與實(shí)際業(yè)務(wù)強(qiáng)相關(guān),哪里使用哪里加載)
- 技術(shù)、業(yè)務(wù)相結(jié)合
注意事項(xiàng):
- wall time和cpu time的區(qū)別
- cpu time才是優(yōu)化方向
- 按照systrace及cpu time跑滿cpu
- 監(jiān)控的完善
- 線上監(jiān)控多階段時(shí)間(App、Activity、聲明周期間隔時(shí)間)
- 將監(jiān)控信息上報(bào)后臺,處理聚合看趨勢
- 收斂啟動代碼修改權(quán)限
- 結(jié)合CI修改啟動代碼需要Review或通知
7.2、啟動優(yōu)化其他方案
這一部分只是簡單介紹一下其他的啟動優(yōu)化的方案,有些方案實(shí)現(xiàn)起來還是比較復(fù)雜的,有需要的朋友可以查找相關(guān)資料結(jié)合自身項(xiàng)目實(shí)踐一下。
1. 提前加載SharedPreferences:使用之前會調(diào)用getSharedPreference()方法,此時(shí)會去異步加載文件中它的配置文件xml并將它load到內(nèi)存之中,當(dāng)我們put或者get某個(gè)屬性時(shí)如果load沒有完成則會阻塞一直等待
- Multidex之前加載,利用此階段CPU
- 覆寫getApplicationContext返回this
2. 啟動階段不啟動子進(jìn)程
- 子進(jìn)程會共享CPU資源、導(dǎo)致主進(jìn)程CPU資源緊張
- 注意啟動順序:App onCreate之前是ContentProvider(啟動階段不要啟動其他組件)
3. 類加載優(yōu)化:提前異步類加載
- Class.forName()只加載類本身及其靜態(tài)變量的引用類(需要發(fā)生在異步線程中)
- new類實(shí)例可以額外加載類成員變量的引用類
4. 啟動階段抑制GC(Native Hook)
OK,寫到這里相信你已經(jīng)對Android啟動優(yōu)化有了自己的了解了,可能我這里介紹的不夠全面,因?yàn)閭€(gè)人能力有限,所以對于哪些說的不夠清楚的地方大家就再查找相關(guān)的資料進(jìn)行更加細(xì)致的學(xué)習(xí)吧。