Android啓動優化你真的瞭解嗎?

目錄

寫在前面

一、啓動優化簡介

1.1、爲什麼要做啓動優化?

1.2、啓動分類

1.3、相關任務

二、啓動時間測量方式

2.1、adb命令  

2.2、手動打點

三、啓動優化工具

3.1、traceview

3.2、systrace(python腳本)

四、優雅獲取方法耗時

4.1、常規方式

4.2、AOP介紹

五、異步優化

5.1、Theme切換

5.2、常規異步優化

5.3、啓動器

六、延遲初始化

6.1、常規方案

6.2、優雅實現延遲初始化

七、啓動優化其他方案

7.1、優化總方針

7.2、啓動優化其他方案


寫在前面

關於Android的性能優化,一直都沒有進行過總結,最近在學習和研究這方面的知識,所以從這一篇開始會在這個專欄裏對性能優化的幾個方向分別進行系統性的總結和輸出,方便自己知識體系的建立。Android性能優化這個體系大致會分爲啓動優化、內存優化、佈局優化、卡頓優化、線程優化、網絡優化、電量優化、瘦身優化、穩定性優化等幾個方面來依次總結,本篇就首先來說說app的啓動優化。

一、啓動優化簡介

1.1、爲什麼要做啓動優化?

因爲啓動速度是app給用戶的第一體驗,如果一個app啓動速度非常慢,那麼即使它界面特效做的在精美炫酷,給用戶的第一感受都是很不好的,互聯網中有一個8秒定律,大家可以去百度或者谷歌搜索一下,大致意思就是說比如我們作爲用戶去訪問一個網頁的時候,如果打開了8秒還沒有打開,那麼70%的用戶會放棄等待,同樣的道理,如果我們的app啓動非常慢,這就會導致我們的app的用戶留存率降低,所以我們必須要做啓動優化。

1.2、啓動分類

先來看一下官方文檔上的介紹,官方將啓動方式分爲了三種,分別是冷溫熱,這裏我截了張圖方便閱讀:

冷啓動:特點是耗時最多,同時它也是衡量標準,我們在線上做的各種優化都是以它作爲標準,從下面這張圖片可以看出冷啓動它經歷了一系列的流程,所以它的耗時也是最多的。

熱啓動:特點是最快,我們所說的熱啓動是指app從後臺切換到前臺,它沒有application的創建和各種生命週期的調用,所以說這種啓動方式是最快的。

溫啓動:特點是較快,它的速度介於冷啓動和熱啓動之間,對於這種方式它會重走activity的生命週期,不會重走進程的創建,application的創建和生命週期等流程。

1.3、相關任務

冷啓動之前:1、啓動App;2、加載空白Window;3、創建進程。這三個任務都是系統行爲,無法進行真正的干預。網上大多介紹啓動優化的都是針對第2條,但其實這是一個假的干預,只是對我們肉眼感知上的一個優化。

之後進行的是:1、創建Application;2、啓動主線程;3、創建MainActivity;4、加載佈局;5、佈置屏幕;6、首幀繪製。

我們的優化方向:Application和Activity生命週期的這個階段,這是開發者真正可以控制的時間。

二、啓動時間測量方式

這裏介紹兩種啓動時間的測量方式:1、adb命令 2、手動打點

2.1、adb命令  

這種方式是我們通過在終端輸入一條adb命令,然後它會打開我們要測試的app,同時進行結果的輸出。具體的命令如下:

adb shell am start -W packagename/首屏Activity(這裏需要使用全類名) 

這裏我以自己寫的一個簡單的列表展示的Demo工程舉例說明:

ThisTime:最後一個Activity啓動耗時

TotalTime:所有Activity啓動耗時(這裏ThisTime和TotalTime值是一致的,因爲我的Demo中只有一個MainActivity

WaitTime:AMS啓動Activity的總耗時,對於一個通用的app(包含SplashActivity),ThisTime肯定是小於TotalTime的,即:

ThisTime < TotalTime < WaitTime

總結:這種方式線下使用方便,可以使用這種方式測量競品爲競品分析提供需要的數據,不能帶到線上,並且測量出來的時間也是一個非嚴謹精確的時間。

2.2、手動打點

這種方式是在app啓動開始時埋點,啓動結束時埋點,然後計算二者差值。

實際使用中,一般將開始時間這個點埋在ApplicationattachBaseContext(Context base)這個方法中,這是整個應用所能接收到的最早的回調時機。開始時間有了,那麼結束時間該怎麼計算呢,也就是我們應該把結束時間這個點埋在什麼位置呢?網上很多資料裏都會說是在onWindowFocusChanged()這個方法裏做啓動結束的時間計算,但是實際上寫在這裏其實是有問題的。

誤區:onWindowFocusChanged它只是Activit的首幀時間,是activity首次繪製的時間,並不能代表activity已經展現出來。我們做性能優化的目的是爲了改善用戶的體驗,並不是單純的爲了把啓動時間縮短,因爲這樣做是不準確的,我們需要的是用戶真正看到界面的時間,所以正確的情況應該是在真實的數據展示(一般取第一條)出來,纔算結束的時間節點。

下面我們就來實戰一下該如何在代碼中埋點統計啓動時間?

首先我們定義一個工具類LaunchTime,用來計算差值時間:

package com.jarchie.performance.utils;

import android.util.Log;

/**
 * 作者: 喬布奇
 * 日期: 2020-03-30 22:36
 * 郵箱: [email protected]
 * 描述: 打點計算啓動時間
 */
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中埋下開始時間點:

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    LaunchTime.startRecord();
}

然後在我們列表適配器中的onBindViewHolder中綁定數據時統計第一條Item展示出來的時間點:

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()方法中統計一下Activity的首幀時間:

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    LaunchTime.endRecord("onWindowFocusChanged");
}

現在來運行我們的程序,看一下最終統計出來的時間值究竟是多少?

由上面的結果可以看出首幀時間是904毫秒,列表數據的第一條展示的時間是1573毫秒,兩者之間的時間差值是超過200毫秒的,這也就表明如果我們僅以Activity的首幀時間作爲啓動結束,那麼這個時間明顯是偏早的,不符合我們做啓動優化的初衷。

三、啓動優化工具

以下所介紹的兩種方式是互相補充的,我們需要正確認識工具並且能夠在不同的場景下選擇適合的工具。

3.1、traceview

特點:

  • 圖形的形式展示執行時間、調用棧等 
  • 信息全面,包含所有線程

使用方式:在代碼中需要做性能分析的地方開始位置和結束位置插入以下代碼

  • Debug.startMethodTracing("");  //該方法具有重載方法,可設置收取信息的路徑,大小
  • Debug.stopMethodTracing("");
  • 生成文件在sd卡:Android/data/packagename/files

代碼實戰:

將我們的項目運行之後(注意開啓運行時權限,這部分不是本節重點,我直接到應用裏將權限開啓了)生成文件如下:

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

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

下面這張圖是Top Down,它比較直觀的展示了函數的調用列表,比如下圖中首先main()函數調用了init(),init()又調用了g()等等,相當於Call Chart詳細版,並且你將鼠標放在對應函數上右鍵有個Jump to Source可以跳轉到具體的代碼中。Total Time是某個函數執行的總時間,Self Time是該函數體內部自有代碼執行的時間,Childre Time是該函數內部調用別的函數所需的時間,後面二者的時間總和一定是等於前面的Total Time的,這點需要注意。Self Time上方有一欄下拉菜單,即我們第一張圖中紅色標註的菜單欄WallClockTime和ThreadTime,前者是這段代碼執行所消耗的時間,後者是CPU執行的時間,一般情況下是前者大於後者,因爲一般情況下某個函數消耗的時間並不等於CPU真正消耗的時間。

最後是Bottom Up,這個的作用也是比較小了,它和Top Down是相反的,它會告訴你某個函數具體是誰調用了它:

總結:(一般比較關注的是Call Chart和Top Down

  • 運行時開銷嚴重,整體都會變慢(它會抓取當前運行的所有線程的所有執行函數和順序)
  • 由於它非常嚴重的運行時開銷,所以它很有可能迴帶偏優化方向

3.2、systrace(python腳本)

特點:

  • 結合Android內核的數據,生成Html報告
  • API18以上使用,推薦TraceCompat

使用方式:

我這裏放一張我自己運行的示例,僅供參考:

代碼實戰:

首先將項目運行讓我們寫的代碼生效,然後運行我們的python腳本,啓動tracing之後,點擊我們的app讓它開始收集信息,tracing完成之後到對應的目錄就會發現已經生成了我們的Performance.html文件,我們到瀏覽器中打開,如下圖所示:

由上圖中左側可以看到有CPU的核心數,往下滑動還可以看到各個線程名稱,然後還可以根據代碼中打的Tag來搜索,下方會展示比較詳細的trace信息(上圖中也舉例說明了,需要注意的Wall Time和CPU Time紅色部分圈出),點到右側圖中具體的位置都會展示出比較詳細的方法名稱執行時間等信息。

總結:

  • 輕量級,開銷小(它是你在哪裏埋點,它就處理哪裏,這點和traceview不同,需要注意)
  • 直觀反映cpu利用率
  • 需要注意cputime與walltime區別:walltime是代碼執行時間,cputime是代碼消耗cpu的時間(優化的重點指標)。舉個栗子:鎖衝突(比如現在調用了A方法,進入A方法之後需要一把鎖,但此時這把鎖被B所持有,導致代碼在A這裏停下了,實際上可能這個A函數並不耗時,但是由於一直拿不到鎖,所以一直處於等待狀態,這就導致walltime時間很長,但是它實際上對CPU並沒有多少消耗)

關於上述工具的詳細使用方法大家可以自行百度或者谷歌查找相關資料,認真學習一下這些分析工具的使用。

四、優雅獲取方法耗時

4.1、常規方式

我們在做啓動優化的時候通常需要知道啓動階段所有方法的耗時,這樣可以有針對性的分析出耗時較多的方法。一般的實現方式就是通過手動埋點來實現,比如在某個方法開始和結束的位置分別插入以下代碼:

long time = System.currentTimeMillis();
initJpush();
long cost = System.currentTimeMillis() - time;
//或者可以使用這行代碼:SystemClock.currentThreadTimeMillis(); //CPU真正執行的時間

當有多個方法需要埋點時,同理這樣寫就可以獲取到每個方法的執行時間了,但是這樣操作存在的問題也是顯而易見的,當然我相信你肯定也發現了,主要總結爲以下幾點:

  • 代碼重複、耦合度高並且看起來非常噁心
  • 侵入性強
  • 工作量大

那麼針對這種方式的劣勢,如何才能更加優雅的實現獲取方法的耗時呢?答案就是採用AOP的方式來實現。

4.2、AOP介紹

AOP簡介:Aspect Oriented Programming,面向切面編程

  • 針對同一類問題的統一處理
  • 無侵入添加代碼

AspectJ簡介:它就是輔助AOP用來實現切面編程

使用時首先需要添加如下的依賴:

//工程目錄下的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'

添加完了依賴之後,再來介紹一下相關知識點,然後我們再到代碼中去真正的使用它。

Join Points:程序運行時的執行點,常用的可以作爲切面的地方如下所示:

  • 函數調用、執行
  • 獲取、設置變量
  • 類初始化

PointCut:帶條件的JoinPoints

Advice:一種Hook,要插入代碼的位置

  • Before:PointCut之前執行
  • After:PointCut之後執行
  • Around:PointCut之前、之後分別執行
  • 舉個栗子:
    @Before("execution(* android.app.Activity.on**(..))")
    public void onActivityCalled(JoinPoint joinPoint) throws Throwable{ ... }

語法簡介:

  • Before:Advice,具體插入的位置
  • execution:處理Join Point的類型,call、execution
  • (* android.app.Activity.on**(..)):匹配規則,匹配android.app.Activity類中任意返回值類型的on開頭的是否有參數的方法都行
  • onActivityCalled:要插入的代碼

代碼實現:

/**
 * 作者:created by Jarchie
 * 時間:2020/5/18 10:02:09
 * 郵箱:[email protected]
 * 說明:使用AOP方式來統計方法耗時
 */
@Aspect //通過該註解,AOP框架可以知道該類即是需要需要插入的代碼
public class PerformanceAop {

    @Around("call(* com.jarchie.performance.app.BaseApp.**(..))") //匹配規則
    public void calculateTime(ProceedingJoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature(); //拿到切點簽名
        String name = signature.toShortString(); //拿到對應的方法信息
        long time = System.currentTimeMillis();
        try {
            joinPoint.proceed(); //手動執行
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        Log.i("執行時間", name + "--->>>" + (System.currentTimeMillis() - time));
    }

}

可以看到這裏是新建了一個類採用AOP的方式來獲取方法耗時,並沒有在BaseApp中添加任何的代碼,運行結果如下所示:

總結:採用AOP實現:①無侵入性②修改方便

五、異步優化

5.1、Theme切換

首先需要說明的是這種方式僅僅是給用戶感官上的快,just a feeling,對應用真實的啓動速度沒有任何的影響。它的實現原理是App在打開首屏Activity之前會首先顯示出一張圖片,當Activity頁面真正展示出來之後再把Theme改變回來,因爲冷啓動中有一步是創建一個空白的Window,這種實現方式正式利用了這個空白的Window。下面來看下具體怎麼操作:

首先定義一個背景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中定義一個主題作爲啓動主題:

    <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的清單文件中設置這個主題:

        <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()方法中調用父類onCreate()方法之前將設置的啓動主題改爲默認主題:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        setTheme(R.style.AppTheme);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
    }

來看下我們修改後的效果,如下圖所示:

5.2、常規異步優化

核心思想:子線程分擔主線程任務,並行減少時間

下面還以Application的onCreate()爲例分析常規的異步優化:現在的App一般情況下都是運行在八核的設備上,不同的設備廠商可能分配給應用的核數有的四核有的八核,但是如果像我們這裏的代碼將所有的初始化工作都放在一個線程中最多佔用一個核,別的三個核或者七個核都處於一個浪費狀態,那麼爲了讓CPU的利用率達到一個更加高效的狀態,這裏就需要使用異步初始化了。

說到異步,那大家想到的肯定是要創建子線程了,這裏使用線程池來創建線程,這種方式更加優雅,不僅可以在很大程度上避免內存泄露,而且還可以讓線程得到複用(這裏線程數的設置是參考了Android AsyncTask源碼中的設計):

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

來看下運行結果:

可以看到時間確實是非常短的,那現在有個問題:是不是以後代碼都可以放在子線程中執行呢?答案當然是否定的,有些場景下並不能很好的實現異步的方案,比如:①有些代碼必須要在主線程中執行;②有些方法必須在onCreate()方法結束後執行完畢。

針對上面這兩種情況,異步的方案其實就不太好解決了,對於第一種情況你只能放棄異步方案,對於第二種情況,我們可以採用CountDownLatch這個類來解決,下面這段代碼的含義大致就是:只要countDownLatch不被滿足,它將一直處於等待狀態,直到被滿足1次,因爲我們構造函數中傳入的數值是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();
        }

異步優化注意事項:

  • 不符合異步要求(如果能修改成符合要求的則修改,不能修改則放棄異步方案)
  • 需要在某階段完成
  • 區分CPU密集型和IO密集型任務

5.3、啓動器

通過上面的常規異步操作過程可以發現還是存在很多問題的,主要有以下幾點:

  • 代碼不優雅(假如方法數較多時,則會寫很多重複代碼)
  • 場景不好處理(特定階段執行完畢、依賴關係)
  • 維護成本高

正是因爲有上面這些問題的存在,纔有了下面的解決方案的產生——啓動器。

啓動器介紹:

核心思想:充分利用CPU多核,自動梳理任務順序

啓動器流程:

  • 代碼Task化,啓動邏輯抽象爲Task
  • 根據所有任務依賴關係排序生成一個有向無環圖(自動生成的)
  • 多線程按照排序後的優先級依次執行

啓動器流程圖:

代碼實戰:首先構建啓動器部分的代碼因爲這個過程還是有點複雜的,代碼相對也不少,這裏就不貼了,大家可以自行百度啓動器相關的實現代碼,這裏只針對使用情況做一個說明:

首先我們需要將上面做異步操作的幾個方法抽成對應的任務,比如這裏InitBuglyTask這個任務就是對應用來解決需要在特定階段完成初始化的問題,重寫needWait()方法設置爲true即需要等待,並且MainTask是運行在主線程的:

public class InitBuglyTask extends MainTask {

    //解決特定階段執行完成問題
    @Override
    public boolean needWait() {
        return true;
    }

    @Override
    public void run() {
        CrashReport.initCrashReport(mContext, "e296ad7fc8", false);
    }
}

然後定義InitDeviceIdTask這個用來獲取設備ID的任務,該任務是在子線程執行的:

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

然後定義初始化極光推送的任務InitJpushTask,重寫dependsOn()方法用來解決依賴關係的問題,該任務的執行依賴於設備ID:

public class InitJpushTask extends Task {

    //解決依賴關係問題
    @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);
    }
}

然後將這些任務添加到啓動器裏面即可,代碼看起來還是比較美觀的:

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,通過上面這幾行代碼啓動器就搞定了,可見它相比較於傳統的異步方式還是好處多多啊,最後來看下運行結果吧:

六、延遲初始化

6.1、常規方案

對於實際項目經驗較多的朋友你會發現其實在Application或者MainActivity中有些任務它的優先級並不是很高,所以對於這類任務通常都可以將它們進行延遲初始化,一般都是延遲到列表數據展示之後再進行加載。我們首先來看下常規的方案是如何實現的呢?最簡單的做法就是將代碼移到列表顯示之後進行調用,或者是通過new Handler().postDelayed延遲一個時間調用。即:

  • New Handler().postDelayed
  • Feed展示後調用

下面我們在代碼中舉個栗子說明一下這種方案是如何實現的?

這裏首先定義一個回調接口是在列表展示出來之後的回調:

public interface OnFeedShowCallBack {
    void onFeedShow();
}

然後在列表適配器中定義這個接口,並給它一個setXXX()方法,並且在列表item第一條展示出來之後回調這個接口:

private OnFeedShowCallBack mCallBack;
...
public void setOnFeedShowCallBack(OnFeedShowCallBack callBack){
    this.mCallBack = callBack;
}
...
if (mCallBack!=null){
    mCallBack.onFeedShow();
}

接着在MainActivity的onCreate()中設置這個回調,並且讓MainActivity實現回調接口重寫回調方法,在回調方法中模擬執行兩個Task,整個這個流程如果熟悉接口回調機制的兄弟應該很好理解了:

mAdapter.setOnFeedShowCallBack(this);
...
@Override
public void onFeedShow() {
    //模擬執行了兩個Task,TaskA和TaskB
    new DispatchRunnable(new DelayInitTaskA()).run();
    new DispatchRunnable(new DelayInitTaskB()).run();
}

以上就是常規方案的實現方法,大家仔細思考一下會發現這其中是有很多問題的:首先,我們的列表展示是發生在主線程中,直接執行mCallBack.onFeedShow()方法,會跑到MainActivity重寫的onFeedShow()中,如果模擬的任務執行時間較長,那麼主線程就會相應的卡住對應的時長,如果此時用戶滑動列表很明顯會造成列表滑動卡頓,給用戶的體驗就很不好了。如果你採用new Handler().postDelayed發送延時消息來處理,當然一定程度上是可以緩解這種卡段,但是這種方案總結下來延時的時機不太好控制並且如果任務數量較多也不易維護,所以我們需要去尋求更加優雅的解決方案。

6.2、優雅實現延遲初始化

核心思想:對延遲任務進行分批初始化,這裏利用IdleHandler特性,空閒執行

針對這種方案我們在代碼中來實踐一下看看具體該如何操作?

首先來創建一個針對延遲初始化任務執行的啓動器:

public class DelayInitDispatcher {

    //創建任務隊列
    private Queue<Task> mDelayTasks = new LinkedList<>();

    //IdleHandler分批處理並在系統空閒時執行
    private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() { //系統空閒時回調
            if(mDelayTasks.size()>0){
                Task task = mDelayTasks.poll(); //分批執行,每次只取一個Task
                new DispatchRunnable(task).run();
            }
            return !mDelayTasks.isEmpty(); //DelayTasks爲空則移除
        }
    };

    //添加任務
    public DelayInitDispatcher addTask(Task task){
        mDelayTasks.add(task);
        return this;
    }

    //啓動
    public void start(){
        Looper.myQueue().addIdleHandler(mIdleHandler);
    }

}

具體的代碼含義都加了註釋了,主要就是利用了IdleHandler的特性在空閒時期執行,接着在onFeedShow()的回調中添加任務並執行即可:

@Override
public void onFeedShow() {
    DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();
    delayInitDispatcher.addTask(new DelayInitTaskA())
        .addTask(new DelayInitTaskB())
        .start();
}

通過代碼我們來比對一下兩種方案的差別:對於常規方案回調接口中有多少個任務都會一次性執行完成,也就意味着主線程會卡在那裏對應的時間;對於第二種方案,我們是添加了多個任務進來,執行的時機是在系統空閒的時候進行執行,並且一次只執行一個,所以第二種方案的優點就顯而易見了:①執行時機明確;②有效緩解列表卡頓,它可以真正的提升用戶的體驗。

七、啓動優化其他方案

7.1、優化總方針

  • 異步、延遲、懶加載(與實際業務強相關,哪裏使用哪裏加載)
  • 技術、業務相結合

注意事項:

①、wall time和cpu time的區別

  • cpu time纔是優化方向
  • 按照systrace及cpu time跑滿cpu

②、監控的完善

  • 線上監控多階段時間(App、Activity、聲明週期間隔時間)
  • 將監控信息上報後臺,處理聚合看趨勢

③、收斂啓動代碼修改權限

  • 結合Ci修改啓動代碼需要Review或通知

7.2、啓動優化其他方案

這一部分只是簡單介紹一下其他的啓動優化的方案,有些方案實現起來還是比較複雜的,有需要的朋友可以查找相關資料結合自身項目實踐一下。

①、提前加載SharedPreferences:使用之前會調用getSharedPreference()方法,此時會去異步加載文件中它的配置文件xml並將它load到內存之中,當我們put或者get某個屬性時如果load沒有完成則會阻塞一直等待

  • Multidex之前加載,利用此階段CPU
  • 覆寫getApplicationContext返回this

②、啓動階段不啓動子進程

  • 子進程會共享CPU資源、導致主進程CPU資源緊張
  • 注意啓動順序:App onCreate之前是ContentProvider(啓動階段不要啓動其他組件)

③、類加載優化:提前異步類加載

  • Class.forName()只加載類本身及其靜態變量的引用類(需要發生在異步線程中)
  • new類實例可以額外加載類成員變量的引用類

④、啓動階段抑制GC(Native Hook)

⑤、CPU鎖屏(可能會導致耗電量增加)

OK,寫到這裏相信你已經對Android啓動優化有了自己的瞭解了,可能我這裏介紹的不夠全面,因爲個人能力有限,所以對於哪些說的不夠清楚的地方大家就再查找相關的資料進行更加細緻的學習吧,今天到這裏就跟大家說再會了,下期見!

祝:工作順利!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章