- 啟動, 打開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)的三個任務:
- 加載并啟動應用
- 顯示應用的空白啟動窗口
- 創(chuàng)建應用進程
- 應用進程負責后續(xù)階段:
- 創(chuàng)建應用對象(Application)
- 啟動主線程
- 創(chuàng)建主Activity
- 擴充視圖/加載布局
- 布局屏幕
- 執(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對于完成此任務有一定助益)
- 下面說到的啟動一般指冷啟動
啟動過程
- (桌面) 點擊響應,應用解析
- (系統(tǒng)) 預覽窗口顯示(根據(jù)Theme屬性創(chuàng)建,如果Theme中指定為透明,看到的仍然是桌面)
- (應用) Application創(chuàng)建, 閃屏頁/啟動頁 Activity創(chuàng)建(一系列的inflateView、onMeasure、onLayout)
- (系統(tǒng)) 閃屏顯示
- (應用) MainActivity創(chuàng)建界面準備
- (系統(tǒng)) 主頁/首頁 顯示
- (應用) 其他工作(數(shù)據(jù)的加載,預加載,業(yè)務組件初始化)
- 窗口可操作
啟動問題分析
- 由啟動過程可以推測出用戶可能遇到的三個問題
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;
}
- 合并閃屏和主頁面的Activity(微信),不過違法單一職責原則,業(yè)務邏輯比較復雜;
2. 業(yè)務梳理
- 理清啟動過程中的模塊,哪些需要,哪些可以砍掉,哪些可以懶加載(懶加載要防止集中化,避免首頁可見但用戶無法操作的情況);
- 根據(jù)業(yè)務場景決定不同的啟動模式;
- 對低端機降級,做功能取舍;
- 啟動優(yōu)化帶來整體留存、轉化的正向價值,是大于某個業(yè)務取消預加載帶來的負面影響的;
3. 業(yè)務優(yōu)化
- 抓大放小,解決主要耗時問題,如優(yōu)化解密算法;
- 異步線程預加載,但過度使用會讓代碼邏輯更加復雜;
- 償還技術債,如有必要,擇時對老代碼進行重構;
4. 線程優(yōu)化
- 減少CPU調(diào)度帶來的波動,讓應用的啟動時間更加穩(wěn)定
- 控制線程的數(shù)量,避免線程太多互爭CPU資源,用統(tǒng)一線程池,根據(jù)機器性能來控制數(shù)量;
- 檢查線程間的鎖,特別是防止主線程出現(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)用,帶來的缺點主要有兩個:
- 穩(wěn)定性/兼容性: 廠商的兼容性、安裝失敗、dex2oat 失敗等,Android P推出的non-sdk-interface調(diào)用限制
- 性能: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. 今日頭條方案
- 在主進程Application 的 attachBaseContext 方法中判斷如果需要使用MultiDex,則創(chuàng)建一個臨時文件,然后開一個進程(LoadDexActivity),顯示Loading,異步執(zhí)行MultiDex.install 邏輯,執(zhí)行完就刪除臨時文件并finish自己。
- 主進程Application 的 attachBaseContext 進入while代碼塊,定時輪循臨時文件是否被刪除,如果被刪除,說明MultiDex已經(jīng)執(zhí)行完,則跳出循環(huán),繼續(xù)正常的應用啟動流程。
- 注意LoadDexActivity 必須要配置在main dex中。
- 具體實現(xiàn)參考項目MultiDexTest
6. 預加載優(yōu)化
1. 類預加載:
- 在Application中提前異步加載初始化耗時較長的類
2. 頁面數(shù)據(jù)預加載:
- 在主頁空閑時,將其它頁面的數(shù)據(jù)加載好保存到內(nèi)存或數(shù)據(jù)庫
3. WebView預加載:
- WebView第一次創(chuàng)建比較耗時,可以預先創(chuàng)建WebView,提前將其內(nèi)核初始化;
- 使用WebView緩存池,用到WebView的地方都從緩存池取,緩存池中沒有緩存再創(chuàng)建,注意內(nèi)存泄漏問題。
- 本地預置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è)
參考
- Android開發(fā)高手課-啟動優(yōu)化(上):從啟動過程看啟動速度優(yōu)化
- 初識pipeline
- 微信Android模塊化架構重構實踐(mmkernel)
- Alpha啟動框架
- Android開發(fā)高手課-啟動優(yōu)化(下):優(yōu)化啟動速度的進階方法
- ReDex: An Android Bytecode Optimizer
- 010Editor的Template安裝與使用
- 支付寶 App 構建優(yōu)化解析:通過安裝包重排布優(yōu)化 Android 端啟動性能
- 微信Android熱補丁實踐演進之路(verify class)
- Atlas動態(tài)組件化(Dynamic Bundle)框架
- 歷時三年研發(fā),OPPO 的 Hyper Boost 引擎如何對系統(tǒng)、游戲和應用實現(xiàn)加速
- 支付寶客戶端架構解析:Android 客戶端啟動速度優(yōu)化之「垃圾回收」
- Android Vitals
- 深入探索Android啟動速度優(yōu)化(上)
- Epic:一個在虛擬機層面、以 Java Method 為粒度的 運行時 AOP Hook 框架
- 面試官:今日頭條啟動很快,你覺得可能是做了哪些優(yōu)化?
- 開源 | BoostMultiDex:挽救 Android Dalvik 機型APP升級安裝體驗
- 今日頭條App 頁面秒開方案詳解
- 深入探索Android啟動速度優(yōu)化(下)