一、App啓動優化介紹
1、背景介紹
- 第一體驗
- 八秒定律
2、啓動分類
- 冷啓動
- 耗時最多,衡量標準
ClickEvent
->IPC
->Process.start
->ActivityThread
(單獨app進程入口類) ->bindApplication
(通過反射創建Application以及調用與Application相關的生命週期) ->LifeCycle
(Activity生命週期) ->ViewRootImpl
(開始真正的界面繪製)
- 熱啓動,最快
後臺
->前臺
- 溫啓動,較快
LifeCycle
(只會重走activity生命週期,不會重新進程創建)
#####3、相關任務
- 冷啓動之前:啓動App,加載空白Window,創建進程
- 隨後任務:創建Application,啓動主線程,創建MainActivity,加載佈局,佈置屏幕,首幀繪製
4、優化方向
- Application和Activity生命週期
二、啓動時間測量方式
1、adc命令
adb shell am start -W packagename/首屏Activity
- ThisTime:最後一個Activity啓動耗時
- TotalTime:所有Activity啓動耗時
- WaitTime:AMS啓動Activity的總耗時
問題:線下使用方便,不能帶到線上,非嚴謹精確時間
2、手動打點
啓動時埋點,啓動結束時埋點,差值
public class LaunchTimer {
private static long sTime;
public static void startRecord() {
sTime = System.currentTimeMillis();
}
public static void endRecord() {
long cost = System.currentTimeMillis() - sTime;
Log.i("cost", cost + "");
}
}
這個回調是應用程序能接收到的最早的回調,在這裏開始計時
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
LaunchTimer.startRecord();
MultiDex.install(this);
}
誤區:onWindowFocusChanged只是首幀時間,並不能代表界面已經展示出來了,要在真實用戶展示時間埋點,比如在列表第一條數據展示時候
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (position == 0 && !mHasRecorded) {
mHasRecorded = true;
holder.itemView.getViewTreeObserver()
.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
holder.itemView.getViewTreeObserver().removeOnPreDrawListener(this);
LaunchTimer.endRecord();
return false;
}
});
}
}
三、 啓動優化工具選擇
1、traceview
- 圖形化界面形式展示代碼執行時間,調用棧等
- 信息全面,包含所有線程
- 使用方式
//在開始時調用
Debug.startMethodTracing("文件名");
//結束時調用
Debug.stopMethodTracing();
生成的文件存放在sd卡:Android/data/packagename/files,運行app後開路徑下找到文件雙擊打開文件
最上面的是時間範圍,可移動鼠標拖動
中間的THREADS是線程,可以看到有17個線程,選中一個線程就可以看到下面線程做的事情,當前看的是mian線程
- Call Chart
每一行都是一個方法的調用時間消耗,他的垂直方向是被調用者
系統api是橙色,應用自身的方法是綠色,第三方api是藍色(包括java語言的api) - TopDown可以看到方法執行的總時間Total,Self時間和Children時間
可以選擇看Wall Colock Time (線程真正執行時間),Thread Time (CPU執行時間) - Flame Chart
收集相同的調用順序 - Bottom Up
一個函數的調用列表,誰調用了我 - 問題:加入了traceview,運行開銷嚴重,整體開銷都會變慢,可能會帶偏優化方向
2、systrace
- 結合Android內核的數據,生成Html報告
- 使用方式
python systrace.py t 10 [other-options] [categories]
//開始時
TraceCompat.beginSection("systrace");
//結束時
TraceCompat.endSection();
到systrace目錄下運行腳本,我的是E:\SDK\platform-tools\systrace
注意python要2.7版本的
python systrace.py --time=10 -o mytrace.html sched gfx view wm
也可以用Android Device Monitor來生成
打開生成的html
該報告列出了呈現UI幀並指示沿時間線的每個渲染幀的每個進程。用綠色框架圓圈表示在16.6毫秒內渲染以保持每秒60幀穩定所需的幀。渲染時間超過16.6毫秒的幀用黃色或紅色框架圓圈表示。
可以選中一個線程查看他的時間消耗
- 官方文檔:https://source.android.com/devices/tech/debug/systrace
- 優點:輕量級,開銷小,直觀反饋CPU利用率
- cputime和walltime的區別:cputime是代碼消耗cpu的時間(重點指標),walltime是代碼執行時間。
舉例:鎖衝突,可能某個線程在等待鎖,導致walltime時間看起來很長,但是對CPU沒有佔用
3、優雅獲取方法耗時
1.常規方式:
背景:需要知道啓動階段所有方法耗時
實現:手動埋點
入侵性強,工作量大
2.AOP
Aspect Oriented Programming,面向切面編程,針對同一類問題的統一處理,無侵入添加代碼
https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx
使用AspectJ,在根build.gradle下配置:
buildscript {
...
dependencies {
...
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
}
}
app項目的build.gradle及新建的module的build.gradle裏都應用插件
apply plugin: 'android-aspectjx'
dependencies {
implementation 'org.aspectj:aspectjrt:1.8.9'
}
- Join Points
程序運行時的執行點,可以作爲切面的地方。(函數調用、執行,獲取、設置變量,類的初始化) - PointCut
帶條件的JoinPoints - Advice
一種Hook,要插入代碼的位置。before:PointCut之前執行。after:PointCut之後執行。around:之前之後分別執行 - 語法介紹
@Before("execution(*android.app.Activity.on**(...))
public void onActivityCalled(JoinPoint joinPoint) throws Throwable{
...
}
Before:Advice,具體插入位置
execution:處理Join Point的類型,call(插入在函數體裏面)、execution(插入在函數體外面)
(android.app.Activity.on*(…)):匹配規則
onActivityCalled:要插入的代碼
使用AOP實現啓動時間監聽
@Aspect
public class PerformanceAop {
//Around在每個方法執行之前和之後分別插入代碼,只切了BaseApplication裏面的函數**(..)任何方法參數
@Around("call(* com.test.common.BaseApplication.**(..))")
public void getTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name=signature.getName();
long time = System.currentTimeMillis();
Log.i("cost", System.currentTimeMillis() - time + "");
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Log.i("cost", name + (System.currentTimeMillis() - time));
}
}
有點:無侵入性,修改方便
四、異步優化
1、優化小技巧
- Theme切換:感覺上的快,先進入一個閃屏頁
先使用這個theme,在MainActivity的super.onCreate()之前切換回來
2、異步優化
- 核心思想:子線程分擔主線程任務,並行減少時間
異步優化注意 - 不符合異步要求的,一種是修改代碼使其滿足,一種是放棄異步的優化
- 需要在某個階段完成,可以使用CountDownLatch
- 區分CPU密集型和IO密集型任務
public class AppApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
//線程池數量,可以參照AsyncTask,
int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// We want at least 2 threads and at most 4 threads in the core pool,
// preferring to have 1 less than the CPU count to avoid saturating
// the CPU with background work
int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
ExecutorService service=Executors.newFixedThreadPool(CORE_POOL_SIZE);
service.submit(new Runnable() {
@Override
public void run() {
initBugly();
}
});
service.submit(new Runnable() {
@Override
public void run() {
initMap();
}
});
......
}
}
當然並不是所有代碼都可以滿足異步優化,比如在子線程中
Handler handler = new Handler();
就會崩潰,因爲在子線程中他找不到looper,解決方案
Handler handler = new Handler(Looper.getMainLooper());
但是項目中,總是會有一些代碼必須要在主線程中執行,這種情況就要放棄異步優化。
比如初始化的代碼必須在application的onCreate()中結束,在activity中調用不到就會崩潰,針對這種情況可以使用CountDownLatch
//條件被滿足1次
private CountDownLatch countDownLatch=new CountDownLatch(1);
@Override
public void onCreate() {
super.onCreate();
service.submit(new Runnable() {
@Override
public void run() {
initMap();
//條件被滿足
countDownLatch.countDown();
}
});
//如果條件沒滿足就等待
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//條件被滿足了,onCreate()纔會執行結束
}
以上常規的異步方案,是有一些問題的:代碼不夠優雅維護成本高,有些操作有依賴關係,需要有先後順序執行的,不方便統計
3、異步優化升級
啓動器介紹
核心思想:充分利用CPU多核,自動梳理任務順序
啓動器流程
-代碼Task化,啓動邏輯抽象爲Task
- 根據所有任務依賴關係排序生成一個有向無環圖
- 多線程按照排序後的優先級依次執行
4、更優秀的延遲初始化方案
常規方案
- new Handler().postDelayed
- 痛點:時機不受控制,可能導致卡頓
更優方案 - 核心思想:對延遲任務進行分批初始化
利用IdleHandler特性空閒執行
public class DelayInitDispatcher {
private Queue<Task> mDelayTasks = new LinkedList<>();
private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
if(mDelayTasks.size()>0){
Task task = mDelayTasks.poll();
new DispatchRunnable(task).run();
}
return !mDelayTasks.isEmpty();
}
};
public DelayInitDispatcher addTask(Task task){
mDelayTasks.add(task);
return this;
}
public void start(){
Looper.myQueue().addIdleHandler(mIdleHandler);
}
}
5、優化總方針
- 異步,延遲,懶加載
- 技術、、業務相結合
注意事項 - wall time和cpu time
cpu time纔是優化方向,按照systrace及cpu time跑滿cpu - 監控的完善:線上監控多階段時間(App,Application,生命週期間隔時間),處理聚合看趨勢
- 收斂啓動代碼修改權限,結合ci修改啓動代碼需要Review或通知
其他方案 - 提前加載SharePreference
如果不加限制,可能有幾十個類在使用幾十個文件,調用get方法會異步加載配置文件,加載到內存當中,在get或者put一個屬性時候如果load到內存中的操作沒有執行完,就會阻塞進行等待
Multidex之前加載,利用此階段的CPU
覆寫getApplicationContext()返回this - 啓動階段不啓動子進程
很多App會有多個進程,子進程會影響主進程的啓動時間,因爲子進程會共享CPU資源,導致主進程CPU緊張。
注意啓動順序:App onCreate之前是ContentProvider, - 類加載優化:提前異步類加載
每隻用一個類都是通過classloader,如果啓動太多類,會延遲啓動時間。
通過Class.forName()只加載類本身及其靜態變量的引用類,如果new類實例可以額外加載類成員變量的引用類,主要需要根據業務情況判斷。
哪些類需要提前異步類加載,可以通過替換系統的ClassLoader,在自定義的ClassLoader打印log - 啓動階段抑制GC
- CPU鎖頻
6、模擬問題
- 你啓動優化怎麼做的?要講整個過程
分析現狀,確認問題:比如,在某個版本發現啓動速度變得特別卡頓,用戶反饋變多,所以進行優化,對啓動代碼進行梳理,發現啓動流程非常複雜了,通過一系列工具來確認出來在主線程中執行了太多代碼。
針對性優化:比如進行了異步初始化,比如有些代碼優先級不是很高,可以延遲執行
長期保持優化效果: - 是怎麼異步的,異步過程中遇到了哪些問題
體現演進過程:最初採用普通異步方案,之後發現不方便,尋找新的解決方案,介紹啓動器 - 做了啓動優化,覺得有哪些容易忽略的點
cpu time,wall time
注意延遲初始化是優化
介紹下黑科技,比如類加載,cup拉高頻率 - 版本迭代導致啓動變慢有什麼好的解決方案
啓動器,結合CI,監控完善