Android高手筆記 - 啟動優(yōu)化

  • 啟動, 打開APP的必經(jīng)之路, 第一體驗,關系到用戶留存和轉化率等核心數(shù)據(jù);

啟動分析

啟動類型

  • Android Vitals可以對應用冷,熱,溫啟動時間做監(jiān)控。
  • 通過adb shell am start -W ... 命令執(zhí)行啟動并打印啟動耗時信息,下面的啟動監(jiān)控中會詳細講解
1. 冷啟動
  • 應用從頭開始啟動,系統(tǒng)進程在冷啟動后才創(chuàng)建應用進程
  • 啟動流程:Click Event -> IPC -> Process.start -> ActivityThread -> bindApplication -> LifeCycle -> ViewRootImpl
  • 冷啟動階段系統(tǒng)的三個任務:
    1. 加載并啟動應用
    2. 顯示應用的空白啟動窗口
    3. 創(chuàng)建應用進程
  • 應用進程負責后續(xù)階段:
    1. 創(chuàng)建應用對象(Application)
    2. 啟動主線程
    3. 創(chuàng)建主Activity
    4. 擴充視圖/加載布局
    5. 布局屏幕
    6. 執(zhí)行初始繪制/首幀繪制
  • 應用進程完成第一次繪制,系統(tǒng)進程就會換掉當前顯示的啟動窗口,替換為主 Activity。此時,用戶可以開始使用應用。
2. 熱啟動
  • 系統(tǒng)的所有工作就是將Activity帶到前臺,
  • 只要應用的所有 Activity 仍駐留在內(nèi)存中,應用就不必重復執(zhí)行對象初始化、布局膨脹和呈現(xiàn);
    但是,如果一些內(nèi)存為響應內(nèi)存整理事件(如 onTrimMemory())而被完全清除,則需要為了響應熱啟動事件而重新創(chuàng)建相應的對象;
  • 熱啟動顯示的屏幕上行為和冷啟動場景相同:在應用完成 Activity 呈現(xiàn)之前,系統(tǒng)進程將顯示空白屏幕。
3. 溫啟動
  • 包含了在冷啟動期間發(fā)生的部分操作;同時,它的開銷要比熱啟動高
  • 場景1:用戶在退出應用后又重新啟動應用(進程可能存活,通過 onCreate() 從頭開始重新創(chuàng)建Activity)
  • 場景2:系統(tǒng)將應用從內(nèi)存中逐出,然后用戶又重新啟動(進程和Activity都需要重啟,但傳遞到onCreate()的已保存的實例state bundle對于完成此任務有一定助益)
  • 下面說到的啟動一般指冷啟動

啟動過程

  1. (桌面) 點擊響應,應用解析
  2. (系統(tǒng)) 預覽窗口顯示(根據(jù)Theme屬性創(chuàng)建,如果Theme中指定為透明,看到的仍然是桌面)
  3. (應用) Application創(chuàng)建, 閃屏頁/啟動頁 Activity創(chuàng)建(一系列的inflateView、onMeasure、onLayout)
  4. (系統(tǒng)) 閃屏顯示
  5. (應用) MainActivity創(chuàng)建界面準備
  6. (系統(tǒng)) 主頁/首頁 顯示
  7. (應用) 其他工作(數(shù)據(jù)的加載,預加載,業(yè)務組件初始化)
  8. 窗口可操作

啟動問題分析

  • 由啟動過程可以推測出用戶可能遇到的三個問題
1. 點擊桌面圖標無響應:
  • 原因:theme中禁用預覽窗口或指定了透明背景
//優(yōu)點:避免啟動app時白屏黑屏等現(xiàn)象
//缺點:容易造成點擊桌面圖標無響應
//(可以配合三方庫懶加載,異步初始化等方案使用,減少初始化時長)
//實現(xiàn)如下
//0. appTheme
 <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/c_ff000000</item>
    <item name="colorPrimaryDark">@color/c_ff000000</item>
    <item name="colorAccent">@color/c_ff000000</item>
    <item name="android:windowActionBar">false</item>
    <item name="android:windowNoTitle">true</item>
</style>
//1. styles.xml中設置
//1.1 禁用預覽窗口
<style name="AppTheme.Launcher">
    <item name="android:windowBackground">@null</item>
    <item name="android:windowDisablePreview">true</item>
</style>
//1.2 指定透明背景
<style name="AppTheme.Launcher">
    <item name="android:windowBackground">@color/c_00ffffff</item>
    <item name="android:windowIsTranslucent">true</item>
</style>
//2. 為啟動頁/閃屏頁Activity設置theme
<activity
    android:name=".splash.SplashActivity"
    android:screenOrientation="portrait"
    android:theme="@style/AppTheme.Launcher">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
//3. 在該Activity.onCreate()中設置AppTheme(設置布局id之前)
//比如我是基類中單獨抽取的獲取布局id方法,那么在啟動頁中重寫此方法時加入如下配置:
 @Override
protected int getContentViewId() {
    setTheme(R.style.AppTheme_Launcher);
    return R.layout.activity_splash;
}

2. 首頁顯示慢
  • 原因:啟動流程復雜,初始化的組件&三方庫過多
3. 首頁顯示后無法操作
  • 原因:同上

啟動優(yōu)化

  • 方法和卡頓優(yōu)化基本相同,只是啟動太過重要,需要更加精打細算;

優(yōu)化工具

  • Traceview 性能損耗太大,得出的結果并不真實;
  • Nanoscope 非常真實,不過暫時只支持 Nexus 6P 和 x86 模擬器,無法針對中低端機做測試;
  • Simpleperf 的火焰圖并不適合做啟動流程分析;
  • systrace 可以很方便地追蹤關鍵系統(tǒng)調(diào)用的耗時情況,但是不支持應用程序代碼的耗時分析;
  • 綜合來看,在卡頓優(yōu)化中提到“systrace + 函數(shù)插樁” 似乎是比較理想的方案(可以參考課后作業(yè)),拿到整個啟動流程的全景圖之后,我們可以清楚地看到這段時間內(nèi)系統(tǒng)、應用各個進程和線程的運行情況;

優(yōu)化方法

1. 閃屏優(yōu)化:
  • 預覽閃屏(今日頭條),預覽窗口實現(xiàn)成閃屏效果,高端機上體驗非常好,不過低端機上會拉長總的閃屏時長(建議在Android6.0以上才啟用此方案);
//優(yōu)點:避免點擊桌面圖標無響應  
//缺點:拉長總的閃屏時長
//(可以配合三方庫懶加載,異步初始化等方案使用,減少初始化時長)
//1. 就是給windowBackground設置一個背景圖片
<style name="AppTheme.Launcher">
    <item name="android:windowBackground">@drawable/bg_splash</item>
    <item name="android:windowFullscreen">true</item>
</style>  
//2. bg_splash文件如下(使用layer-list實現(xiàn))  
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@color/color_ToolbarLeftItem" />
    <item>
        <bitmap
            android:antialias="true"
            android:gravity="center"
            android:src="@drawable/ic_splash" />
    </item>
</layer-list>
//3. 為啟動頁/閃屏頁Activity設置theme
<activity
    android:name=".splash.SplashActivity"
    android:screenOrientation="portrait"
    android:theme="@style/AppTheme.Launcher">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>  
//4. 在該Activity.onCreate()中設置AppTheme(設置布局id之前)
//比如我是基類中單獨抽取的獲取布局id方法,那么在啟動頁中重寫此方法時加入如下配置:
 @Override
protected int getContentViewId() {
    setTheme(R.style.AppTheme_Launcher);
    return R.layout.activity_splash;
}  
  1. 合并閃屏和主頁面的Activity(微信),不過違法單一職責原則,業(yè)務邏輯比較復雜;
2. 業(yè)務梳理
  • 理清啟動過程中的模塊,哪些需要,哪些可以砍掉,哪些可以懶加載(懶加載要防止集中化,避免首頁可見但用戶無法操作的情況);
  • 根據(jù)業(yè)務場景決定不同的啟動模式;
  • 對低端機降級,做功能取舍;
  • 啟動優(yōu)化帶來整體留存、轉化的正向價值,是大于某個業(yè)務取消預加載帶來的負面影響的;
3. 業(yè)務優(yōu)化
  • 抓大放小,解決主要耗時問題,如優(yōu)化解密算法;
  • 異步線程預加載,但過度使用會讓代碼邏輯更加復雜;
  • 償還技術債,如有必要,擇時對老代碼進行重構;
4. 線程優(yōu)化
  • 減少CPU調(diào)度帶來的波動,讓應用的啟動時間更加穩(wěn)定
  1. 控制線程的數(shù)量,避免線程太多互爭CPU資源,用統(tǒng)一線程池,根據(jù)機器性能來控制數(shù)量;
  2. 檢查線程間的鎖,特別是防止主線程出現(xiàn)長時間的空轉(主線程因為鎖而干等子線程任務);
  //通過sched查看線程切換數(shù)據(jù)
  proc/[pid]/sched:
  nr_voluntary_switches:     
  主動上下文切換次數(shù),因為線程無法獲取所需資源導致上下文切換,最普遍的是IO。    
  nr_involuntary_switches:   
  被動上下文切換次數(shù),線程被系統(tǒng)強制調(diào)度導致上下文切換,例如大量線程在搶占CPU。
  • 現(xiàn)在有很多啟動框架,使用Pipeline機制,根據(jù)業(yè)務優(yōu)先級規(guī)定業(yè)務初始化時機,如微信的mmkernel,阿里的alpha, 會為任務建立依賴關系,最終形成一個有向無環(huán)圖;
  • 下面是自定義的一個可以區(qū)分多類型任務的線程池工具類,也可以用于異步初始化
//- 注意區(qū)分任務類型:
//    - IO密集型任務:不消耗CPU,核心池可以很大,如文件讀寫,網(wǎng)絡請求等。
//    - CPU密集型任務:核心池大小和CPU核心數(shù)相關,如復雜的計算,需要使用大量的CPU計算單元。
//
// 執(zhí)行的任務是CPU密集型
DispatcherExecutor.getCPUExecutor().execute(YourRunable());

// 執(zhí)行的任務是IO密集型
DispatcherExecutor.getIOExecutor().execute(YourRunable());

/**
 * @Author: LiuJinYang
 * @CreateDate: 2020/12/16
 * 
 * 實現(xiàn)用于執(zhí)行多類型任務的基礎線程池
 */
public class DispatcherExecutor {

    /**
     * CPU 密集型任務的線程池
     */
    private static ThreadPoolExecutor sCPUThreadPoolExecutor;

    /**
     * IO 密集型任務的線程池
     */
    private static ExecutorService sIOThreadPoolExecutor;

    /**
     * 當前設備可以使用的 CPU 核數(shù)
     */
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();

    /**
     * 線程池核心線程數(shù),其數(shù)量在2 ~ 5這個區(qū)域內(nèi)
     */
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 5));

    /**
     * 線程池線程數(shù)的最大值:這里指定為了核心線程數(shù)的大小
     */
    private static final int MAXIMUM_POOL_SIZE = CORE_POOL_SIZE;

    /**
     * 線程池中空閑線程等待工作的超時時間,當線程池中
     * 線程數(shù)量大于corePoolSize(核心線程數(shù)量)或
     * 設置了allowCoreThreadTimeOut(是否允許空閑核心線程超時)時,
     * 線程會根據(jù)keepAliveTime的值進行活性檢查,一旦超時便銷毀線程。
     * 否則,線程會永遠等待新的工作。
     */
    private static final int KEEP_ALIVE_SECONDS = 5;

    /**
     * 創(chuàng)建一個基于鏈表節(jié)點的阻塞隊列
     */
    private static final BlockingQueue<Runnable> S_POOL_WORK_QUEUE = new LinkedBlockingQueue<>();

    /**
     * 用于創(chuàng)建線程的線程工廠
     */
    private static final DefaultThreadFactory S_THREAD_FACTORY = new DefaultThreadFactory();

    /**
     * 線程池執(zhí)行耗時任務時發(fā)生異常所需要做的拒絕執(zhí)行處理
     * 注意:一般不會執(zhí)行到這里
     */
    private static final RejectedExecutionHandler S_HANDLER = new RejectedExecutionHandler() {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            Executors.newCachedThreadPool().execute(r);
        }
    };

    /**
     * 獲取CPU線程池
     *
     * @return CPU線程池
     */
    public static ThreadPoolExecutor getCPUExecutor() {
        return sCPUThreadPoolExecutor;
    }

    /**
     * 獲取IO線程池
     *
     * @return IO線程池
     */
    public static ExecutorService getIOExecutor() {
        return sIOThreadPoolExecutor;
    }

    /**
     * 實現(xiàn)一個默認的線程工廠
     */
    private static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                    Thread.currentThread().getThreadGroup();
            namePrefix = "TaskDispatcherPool-" +
                    POOL_NUMBER.getAndIncrement() +
                    "-Thread-";
        }

        @Override
        public Thread newThread(Runnable r) {
            // 每一個新創(chuàng)建的線程都會分配到線程組group當中
            Thread t = new Thread(group, r,
                    namePrefix + threadNumber.getAndIncrement(),
                    0);
            if (t.isDaemon()) {
                // 非守護線程
                t.setDaemon(false);
            }
            // 設置線程優(yōu)先級
            if (t.getPriority() != Thread.NORM_PRIORITY) {
                t.setPriority(Thread.NORM_PRIORITY);
            }
            return t;
        }
    }

    static {
        sCPUThreadPoolExecutor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
                S_POOL_WORK_QUEUE, S_THREAD_FACTORY, S_HANDLER);
        // 設置是否允許空閑核心線程超時時,線程會根據(jù)keepAliveTime的值進行活性檢查,一旦超時便銷毀線程。否則,線程會永遠等待新的工作。
        sCPUThreadPoolExecutor.allowCoreThreadTimeOut(true);
        // IO密集型任務線程池直接采用CachedThreadPool來實現(xiàn),
        // 它最多可以分配Integer.MAX_VALUE個非核心線程用來執(zhí)行任務
        sIOThreadPoolExecutor = Executors.newCachedThreadPool(S_THREAD_FACTORY);
    }

}
5. GC優(yōu)化
  • 啟動過程,要盡量減少GC次數(shù),避免造成主線程長時間的卡頓
  //1. 通過 systrace 單獨查看整個啟動過程 GC 的時間
  python systrace.py dalvik -b 90960 -a com.sample.gc
  //2. 通過Debug.startAllocCounting監(jiān)控啟動過程總GC的耗時情況
  // GC使用的總耗時,單位是毫秒
  Debug.getRuntimeStat("art.gc.gc-time");
  // 阻塞式GC的總耗時
  Debug.getRuntimeStat("art.gc.blocking-gc-time");
  //如果發(fā)現(xiàn)主線程出現(xiàn)比較多的 GC 同步等待,就需要通過 Allocation 工具做進一步的分析  
  • 啟動過程避免大量字符串操作,序列化和反序列化,減少對象創(chuàng)建(提高服用或移到Native實現(xiàn));
  • java對象逃逸也很容易引起GC,應保證對象生命周期盡量短,在棧上就進行銷毀;
6. 系統(tǒng)調(diào)用優(yōu)化
  • 通過systrace的System Service類型,可以看到啟動過程System Server的 CPU 工作情況
  • 啟動過程盡量不要做系統(tǒng)調(diào)用,如PackageManagerService操作,Binder調(diào)用
  • 啟動過程也不要過早的拉起應用的其他進程,System Server和新的進程都會競爭CPU資源,內(nèi)存不足時可能觸發(fā)系統(tǒng)的low memory killer 機制,導致系統(tǒng)殺死和拉起(?;睿┐罅窟M程,進而影響前臺進程

啟動進階方法

1. IO優(yōu)化

  • 負載過高時,IO性能下降的會比較快,特別是對低端機;
  • 啟動過程不建議出現(xiàn)網(wǎng)絡IO
  • 磁盤IO要清楚啟動過程讀取了什么文件,多少字節(jié),buffer大小,耗時多少,在什么線程等
  • 重度用戶是啟動優(yōu)化一定要覆蓋的群體,如本地緩存,數(shù)據(jù)庫,SP文件非常多時的耗時
  • 數(shù)據(jù)結構的選擇,如啟動時可能只需要sp文件中的幾個字段,SharedPreference就需要分開存儲,避免解析全部sp數(shù)據(jù)耗時過長;
  • 啟動過程適合使用隨機讀寫的數(shù)據(jù)結構,可以將ArrayMap改造成支持隨機讀寫、延時解析的數(shù)據(jù)存儲方式。

2. 數(shù)據(jù)重排

  • Linux 文件 I/O 流程
Linux 文件系統(tǒng)從磁盤讀文件的時候,會以 block 為單位去磁盤讀取,一般 block 
大小是 4KB。也就是說一次磁盤讀寫大小至少是 4KB,然后會把 4KB 數(shù)據(jù)放到頁緩存
 Page Cache 中。如果下次讀取文件數(shù)據(jù)已經(jīng)在頁緩存中,那就不會發(fā)生真實的磁盤 
 I/O,而是直接從頁緩存中讀取,大大提升了讀的速度。

例如讀取文件中1KB數(shù)據(jù),因為Buffer不小心寫成了 1 byte,總共要讀取 1000 次。
那系統(tǒng)是不是真的會讀1000次磁盤呢?事實上1000次讀操作只是我們發(fā)起的次數(shù),
并不是真正的磁盤 I/O 次數(shù),我們雖然讀了 1000 次,但事實上只會發(fā)生一次磁盤 
I/O,其他的數(shù)據(jù)都會在頁緩存中得到。

Dex文件用的到的類和安裝包APK里面各種資源文件一般都比較小,但是讀取非常頻繁。
我們可以利用系統(tǒng)這個機制將它們按照讀取順序重新排列,減少真實的磁盤 I/O 次數(shù);

類重排
// 啟動過程類加載順序可以通過復寫 ClassLoader 得到
class MyClassLoader extends PathClassLoader {
    public Class<?> findClass(String name) {
        //將name記錄到文件
        writeToFile(name,"coldstart_classes.txt");
        return super.findClass(name);
    }
}
//然后通過ReDex的Interdex調(diào)整類在Dex中的排列順序,最后可以利用 010 Editor 查看修改后的效果。
資源文件重排
  • Facebook 在比較早的時候就使用“資源熱圖”來實現(xiàn)資源文件的重排
  • 支付寶在《通過安裝包重排布優(yōu)化 Android 端啟動性能》中詳細講述了資源重排的原理和落地方法;
  • 實現(xiàn)上都是通過修改 Kernel 源碼,單獨編譯了一個特殊的 ROM,為了便于資源文件統(tǒng)計,重排后實現(xiàn)效果的度量,流程自動化
  • 如果僅僅為了統(tǒng)計,也可以用hook方式
    //Hook,利用 Frida 實現(xiàn)獲得 Android 資源加載順序的方法
    resourceImpl.loadXmlResourceParser.implementation=function(a,b,c,d){
       send('file:'+a)
       return this.loadXmlResourceParser(a,b,c,d)
    }
    resourceImpl.loadDrawableForCookie.implementation=function(a,b,c,d,e){
       send("file:"+a)
       return this.loadDrawableForCookie(a,b,c,d,e)
    }
    //Frida相對小眾,后面會替換其他更加成熟的 Hook 框架
    //調(diào)整安裝包文件排列需要修改 7zip 源碼實現(xiàn)支持傳入文件列表順序,同樣最后可以利用 010 Editor 查看修改后的效果;
    
    • 所謂創(chuàng)新,不一定是創(chuàng)造前所未有的東西。我們將已有的方案移植到新的平臺,并且很好地結合該平臺的特性將其落地,就是一個很大的創(chuàng)新

3. 類的加載

  • 在加載類的過程有一個 verify class 的步驟,它需要校驗方法的每一個指令,是一個比較耗時的操作,可以通過 Hook 來去掉 verify 這個步驟
  • 最大的優(yōu)化場景在于首次和覆蓋安裝時
//Dalvik 平臺: 將 classVerifyMode 設為 VERIFY_MODE_NONE
// Dalvik Globals.h
gDvm.classVerifyMode = VERIFY_MODE_NONE;
// Art runtime.cc
verify_ = verifier::VerifyMode::kNone;

//ART 平臺要復雜很多,Hook 需要兼容幾個版本
//在安裝時大部分 Dex 已經(jīng)優(yōu)化好了,去掉 ART 平臺的 verify 只會對動態(tài)加載的 Dex 帶來一些好處
//Atlas 中的dalvik_hack-3.0.0.5.jar可以通過下面的方法去掉 verify
AndroidRuntime runtime = AndroidRuntime.getInstance();
runtime.init(context);
runtime.setVerificationEnabled(false);
//這個黑科技可以大大降低首次啟動的速度,代價是對后續(xù)運行會產(chǎn)生輕微的影響。同時也要考慮兼容性問題,暫時不建議在 ART 平臺使用

4. 黑科技

保活:
  • ?;羁梢詼p少Application創(chuàng)建跟初始化的時間,讓冷啟動變成溫啟動。不過在Target 26之后,?;畹拇_變得越來越難;(大廠一般是廠商合作,例如微信的 Hardcoder 方案和 OPPO 推出的Hyper Boost方案,當應用體量足夠大,就可以倒逼廠商去專門為它們做優(yōu)化)
插件化和熱修復:
  • 事實上大部分的框架在設計上都存在大量的 Hook 和私有 API 調(diào)用,帶來的缺點主要有兩個:
  1. 穩(wěn)定性/兼容性: 廠商的兼容性、安裝失敗、dex2oat 失敗等,Android P推出的non-sdk-interface調(diào)用限制
  2. 性能:Android Runtime 每個版本都有很多的優(yōu)化,黑科技會導致失效
應用加固:
  • 對啟動速度來說簡直是災難,有時候我們需要做一些權衡和選擇
GC 抑制:
  • 參考支付寶客戶端架構解析-Android 客戶端啟動速度優(yōu)化之「垃圾回收」;
  • 允許堆一直增長,直到手動或OOM停止GC抑制

5. MultiDex 優(yōu)化

apk編譯流程/Android Studio 按下編譯按鈕后發(fā)生了什么?
1. 打包資源文件,生成R.java文件(使用工具AAPT)
2. 處理AIDL文件,生成java代碼(沒有AIDL則忽略)
3. 編譯 java 文件,生成對應.class文件(java compiler)
4. .class 文件轉換成dex文件(dex)
5. 打包成沒有簽名的apk(使用工具apkbuilder)
6. 使用簽名工具給apk簽名(使用工具Jarsigner)
7. 對簽名后的.apk文件進行對齊處理,不進行對齊處理不能發(fā)布到Google Market(使用工具zipalign)

其中第4步,單個dex文件中的方法數(shù)不能超過65536,不然編譯會報錯:Unable to execute dex: method ID not in [0, 0xffff]: 65536, 所以我們項目中一般都會用到multidex:

1. gradle中配置
 defaultConfig {
    ...
    multiDexEnabled true
 }
 
 implementation 'androidx.multidex:multidex:2.0.1'
 
2. Application中初始化
@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    MultiDex.install(this);
}

然鵝,這個multidex過程是比較耗時的,那么能否針對這個問題進行優(yōu)化呢?

MultiDex優(yōu)化的兩種方案
1. 子線程install(不推薦):
  • 閃屏頁開一個子線程去執(zhí)行MultiDex.install,然后加載完才跳轉到主頁,
需要注意的是閃屏頁的Activity,包括閃屏頁中引用到的其它類必須在主dex中,
不然在MultiDex.install之前加載這些不在主dex中的類會報錯Class Not Found。
這個可以通過gradle配置,如下:
defaultConfig {
    //分包,指定某個類在main dex
    multiDexEnabled true
    multiDexKeepProguard file('multiDexKeep.pro') // 打包到main dex的這些類的混淆規(guī)制,沒特殊需求就給個空文件
    multiDexKeepFile file('maindexlist.txt') // 指定哪些類要放到main dex
}
2. 今日頭條方案
  1. 在主進程Application 的 attachBaseContext 方法中判斷如果需要使用MultiDex,則創(chuàng)建一個臨時文件,然后開一個進程(LoadDexActivity),顯示Loading,異步執(zhí)行MultiDex.install 邏輯,執(zhí)行完就刪除臨時文件并finish自己。
  2. 主進程Application 的 attachBaseContext 進入while代碼塊,定時輪循臨時文件是否被刪除,如果被刪除,說明MultiDex已經(jīng)執(zhí)行完,則跳出循環(huán),繼續(xù)正常的應用啟動流程。
  3. 注意LoadDexActivity 必須要配置在main dex中。
  4. 具體實現(xiàn)參考項目MultiDexTest

6. 預加載優(yōu)化

1. 類預加載:
  • 在Application中提前異步加載初始化耗時較長的類
2. 頁面數(shù)據(jù)預加載:
  • 在主頁空閑時,將其它頁面的數(shù)據(jù)加載好保存到內(nèi)存或數(shù)據(jù)庫
3. WebView預加載:
  1. WebView第一次創(chuàng)建比較耗時,可以預先創(chuàng)建WebView,提前將其內(nèi)核初始化;
  2. 使用WebView緩存池,用到WebView的地方都從緩存池取,緩存池中沒有緩存再創(chuàng)建,注意內(nèi)存泄漏問題。
  3. 本地預置html和css,WebView創(chuàng)建的時候先預加載本地html,之后通過js腳本填充內(nèi)容部分。
4. Activity預創(chuàng)建: (今日頭條)
  • Activity對象是在子線程預先new出來,例如在閃屏頁等待廣告時調(diào)用下面代碼
DispatcherExecutor.getCPUExecutor().execute(new Runnable() {
    @Override
    public void run() {
            long startTime = System.currentTimeMillis();
            MainActivity mainActivity = new MainActivity();
            LjyLogUtil.d( "preNewActivity 耗時: " + (System.currentTimeMillis() - startTime));
    }
});

對象第一次創(chuàng)建的時候,java虛擬機首先檢查類對應的Class 對象是否已經(jīng)加載。如果沒有加載,jvm會根據(jù)類名查找.class文件,將其Class對象載入。同一個類第二次new的時候就不需要加載類對象,而是直接實例化,創(chuàng)建時間就縮短了。

7. 啟動階段不啟動子進程

  • 子進程會共享CPU資源,導致主進程CPU緊張

8. CPU鎖頻

  • 當下移動設備cpu性能暴增,但一般利用率并不高,我們可以在啟動時暴力拉伸CPU頻率,來增加啟動速度
  • 但是會導致耗電量增加
  • Android系統(tǒng)中,CPU相關的信息存儲在/sys/devices/system/cpu目錄的文件中,通過對該目錄下的特定文件進行寫值,實現(xiàn)對CPU頻率等狀態(tài)信息的更改。
- CPU工作模式
performance:最高性能模式,即使系統(tǒng)負載非常低,cpu也在最高頻率下運行。
powersave:省電模式,與performance模式相反,cpu始終在最低頻率下運行。
ondemand:CPU頻率跟隨系統(tǒng)負載進行變化。
userspace:可以簡單理解為自定義模式,在該模式下可以對頻率進行設定。

啟動監(jiān)控/耗時檢測

logcat

  • Android Studio的logcat中過濾關鍵字Displayed

adb shell

adb shell am start -W com.ljy.publicdemo.lite/com.ljy.publicdemo.activity.MainActivity
執(zhí)行結果:
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.ljy.publicdemo.lite/com.ljy.publicdemo.activity.MainActivity }
Status: ok
LaunchState: COLD 
Activity: com.ljy.publicdemo.lite/com.ljy.publicdemo.activity.MainActivity
TotalTime: 2065
WaitTime: 2069
Complete
//LaunchState表示冷熱溫啟動
//TotalTime:表示所有Activity啟動耗時。(主要數(shù)據(jù),包括 創(chuàng)建進程 + Application初始化 + Activity初始化到界面顯示 的過程)
//WaitTime:表示AMS啟動Activity的總耗時。

實驗室監(jiān)控

  • 通過定期自動錄屏并分析,也適合做競品的對比測試
  • 如何找到啟動結束的點
    • 80%繪制
    • 圖像識別
  • 門檻高,適合大廠

線上監(jiān)控

啟動耗時計算的細節(jié):
  • 啟動結束的統(tǒng)計時機:使用用戶真正可以操作的時間
  • 啟動時間的扣除邏輯:閃屏,廣告,新手引導的時間都應扣除
  • 啟動排除邏輯:Broadcast、Server 拉起,啟動過程進入后臺等都需排除掉
衡量啟動速度快慢的標準
  • 平均啟動時間(體驗差的用戶可能被平均)
  • 快開慢開比,如2秒快開比、5秒慢開比
  • 90%用戶的啟動時間
區(qū)分啟動類型:
  • 首次安裝啟動、覆蓋安裝啟動、冷啟動,溫啟動,熱啟動
  • 熱啟動的占比也可以反映出我們程序的活躍或?;钅芰?/li>
除了指標的監(jiān)控,啟動的線上堆棧監(jiān)控更加困難。Facebook 會利用 Profilo 工具對啟動的
整個流程耗時做監(jiān)控,并且在后臺直接對不同的版本做自動化對比,監(jiān)控新版本是否有新增耗時的函數(shù)。

對于啟動優(yōu)化要警惕 KPI 化,要解決的不是一個數(shù)字,而是用戶真正的體驗問題。

代碼打點(函數(shù)插樁),缺點是代碼有侵入性較強
/**
 * @Author: LiuJinYang
 * @CreateDate: 2020/12/14
 *
 * 在項目中需要統(tǒng)計時間的地方加入打點, 比如
 * 應用程序的生命周期節(jié)點。
 * 啟動時需要初始化的重要方法,例如數(shù)據(jù)庫初始化,讀取本地的一些數(shù)據(jù)。
 * 其他耗時的一些算法。
 */
public class TimeMonitor {
    private int mMonitorId = -1;

    /**
     * 保存一個耗時統(tǒng)計模塊的各種耗時,tag對應某一個階段的時間
     */
    private HashMap<String, Long> mTimeTag = new HashMap<>();
    private long mStartTime = 0;

    public TimeMonitor(int mMonitorId) {
        LjyLogUtil.d("init TimeMonitor id: " + mMonitorId);
        this.mMonitorId = mMonitorId;
    }

    public int getMonitorId() {
        return mMonitorId;
    }

    public void startMonitor() {
        // 每次重新啟動都把前面的數(shù)據(jù)清除,避免統(tǒng)計錯誤的數(shù)據(jù)
        if (mTimeTag.size() > 0) {
            mTimeTag.clear();
        }
        mStartTime = System.currentTimeMillis();
    }

    /**
     * 每打一次點,記錄某個tag的耗時
     */
    public void recordingTimeTag(String tag) {
        // 若保存過相同的tag,先清除
        if (mTimeTag.get(tag) != null) {
            mTimeTag.remove(tag);
        }
        long time = System.currentTimeMillis() - mStartTime;
        LjyLogUtil.d(tag + ": " + time);
        mTimeTag.put(tag, time);
    }

    public void end(String tag, boolean writeLog) {
        recordingTimeTag(tag);
        end(writeLog);
    }

    public void end(boolean writeLog) {
        if (writeLog) {
            //寫入到本地文件
        }
    }

    public HashMap<String, Long> getTimeTags() {
        return mTimeTag;
    }
}
  • 耗時統(tǒng)計可能會在多個模塊和類中需要打點,所以需要一個單例類來管理各個耗時統(tǒng)計的數(shù)據(jù):
/**
 * @Author: LiuJinYang
 * @CreateDate: 2020/12/14
 */
public class TimeMonitorManager {
    private HashMap<Integer, TimeMonitor> mTimeMonitorMap;

    private TimeMonitorManager() {
        this.mTimeMonitorMap = new HashMap<>();
    }

    private static class TimeMonitorManagerHolder {
        private static TimeMonitorManager mTimeMonitorManager = new TimeMonitorManager();
    }

    public static TimeMonitorManager getInstance() {
        return TimeMonitorManagerHolder.mTimeMonitorManager;
    }

    /**
     * 初始化打點模塊
     */
    public void resetTimeMonitor(int id) {
        if (mTimeMonitorMap.get(id) != null) {
            mTimeMonitorMap.remove(id);
        }
        getTimeMonitor(id).startMonitor();
    }

    /**
     * 獲取打點器
     */
    public TimeMonitor getTimeMonitor(int id) {
        TimeMonitor monitor = mTimeMonitorMap.get(id);
        if (monitor == null) {
            monitor = new TimeMonitor(id);
            mTimeMonitorMap.put(id, monitor);
        }
        return monitor;
    }
}
AOP打點,例如統(tǒng)計Application中的所有方法耗
1. 通過AspectJ
//1. 集成aspectj
   //根目錄build.gradle中
  classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'
  //app module的build.gradle中
  apply plugin: 'android-aspectjx'
  //如果遇到報錯Cause: zip file is empty,可添加如下配置
  android{
    aspectjx {
        include 'com.ljy.publicdemo'
    }
  }
//2. 創(chuàng)建注解類  
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GetTime {
    String tag() default "";
}

//3. 使用aspectj解析注解并實現(xiàn)耗時記錄
@Aspect
public class AspectHelper {
    @Around("execution(@GetTime * *(..))")
    public void getTime(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature joinPointObject = (MethodSignature) joinPoint.getSignature();
        Method method = joinPointObject.getMethod();

        boolean flag = method.isAnnotationPresent(GetTime.class);
        LjyLogUtil.d("flag:"+flag);
        String tag = null;
        if (flag) {
            GetTime getTime = method.getAnnotation(GetTime.class);
            tag = getTime.tag();
        }
        if (TextUtils.isEmpty(tag)) {
            Signature signature = joinPoint.getSignature();
            tag = signature.toShortString();
        }
        long time = System.currentTimeMillis();
        try {
            joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        LjyLogUtil.d( tag+" get time: " + (System.currentTimeMillis() - time));
    }
}
2. 通過Epic三方庫
//目前 Epic 支持 Android 5.0 ~ 11 的 Thumb-2/ARM64 指令集,arm32/x86/x86_64/mips/mips64 不支持。
//1. 添加epic依賴
implementation 'me.weishu:epic:0.11.0'
//2. 使用epic
public static class ActivityMethodHook extends XC_MethodHook{

    private long startTime;

    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                super.beforeHookedMethod(param);
        startTime = System.currentTimeMillis();
    }

    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        Activity act = (Activity) param.thisObject;
        String methodName=param.method.getName();
        LjyLogUtil.d( act.getLocalClassName()+"."+methodName+" get time: " + (System.currentTimeMillis() - startTime));
    }
}
private void initEpic() {
    //對所有activity的onCreate執(zhí)行耗時進行打印
     DexposedBridge.hookAllMethods(Activity.class, "onCreate", new ActivityMethodHook());
}
//也可以用于鎖定線程創(chuàng)建者
DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() {
    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        Thread thread = (Thread) param.thisObject;
        LjyLogUtil.i("stack " + Log.getStackTraceString(new Throwable()));
    }
});

課后作業(yè)

參考

我是今陽,如果想要進階和了解更多的干貨,歡迎關注微信公眾號 “今陽說” 接收我的最新文章

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

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

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