關(guān)于 Android啟動優(yōu)化你應(yīng)該了解的知識點(diǎn)

一、啟動優(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ù)

冷啟動之前:

  1. 啟動App;
  2. 加載空白Window;
  3. 創(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)行的是:

  1. 創(chuàng)建Application;
  2. 啟動主線程;
  3. 創(chuàng)建MainActivity;
  4. 加載布局;
  5. 布置屏幕;
  6. 首幀繪制。

我們的優(yōu)化方向: Application和Activity生命周期的這個(gè)階段,這是開發(fā)者真正可以控制的時(shí)間。

二、啟動時(shí)間測量方式

這里介紹兩種啟動時(shí)間的測量方式:

  1. adb命令
  2. 手動打點(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

使用方式:

我這里放一張我自己運(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):

  1. 無侵入性
  2. 修改方便

五、異步優(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):

  1. wall time和cpu time的區(qū)別
  • cpu time才是優(yōu)化方向
  • 按照systrace及cpu time跑滿cpu
  1. 監(jiān)控的完善
  • 線上監(jiān)控多階段時(shí)間(App、Activity、聲明周期間隔時(shí)間)
  • 將監(jiān)控信息上報(bào)后臺,處理聚合看趨勢
  1. 收斂啟動代碼修改權(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í)吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容