APP的性能提升無非就是圍繞穩(wěn)定彬向、流暢之類的指標做文章,在推動性能提升的時候攻冷,什么才是關(guān)鍵娃胆,熱情?能力 等曼?規(guī)范里烦?,個人認為是工具涉兽,用好性能分析工具招驴,性能提升就走完了一大半篙程,就好比:”算數(shù)我比不過小王枷畏,但我找了個電子計算器“。以提升冷啟動速度為例虱饿,看看整體的性能優(yōu)化流程應該是什么樣子拥诡,而在這其中性能工具能帶來什么触趴。
冷啟動的定義與可優(yōu)化的點
如何衡量當前的性能指標,個人感覺渴肉,性能的衡量分三步: 指標制-> 指標采集 -> 性能基線與優(yōu)劣評級, 以上三塊組成性能量化工具冗懦,有了量化工具,就可以說APP性能是好是壞仇祭,以冷啟動為例披蕉,冷啟動指標如何制定?單從技術(shù)上說感覺可以定義如下:
冷啟動耗時 = 從APP進程創(chuàng)建到第一個有效頁面幀[閃屏]
具體到實現(xiàn)上乌奇,涉及哪些環(huán)節(jié)没讲,會怎樣影響冷啟動速度呢?
冷啟動->系統(tǒng)會啟動一個StartWindow占位-> 啟動進程->創(chuàng)建Application-?>Application中初始化全局配置->啟動第一個Activity->Create->Start->Resume->AddWindow->UI測量繪制[performTraversals]->首幀可見
冷啟動的時候礁苗,系統(tǒng)一般會先啟動一個占位Window爬凑,默認是個白屏窗口,復用的是第一個啟動Activity配置试伙,在體感上嘁信,主要下面的Activity配置
<item name="android:windowBackground">@drawable/xxx</item>
它一般是SplashActivity的配置,用品牌圖做個中轉(zhuǎn)疏叨,這個圖最好要限制下尺寸潘靖,否則在解析上影響啟動速度。隨后系統(tǒng)會啟動進程加載SplashActivity蚤蔓,啟動進程主要是Application中可能有些APP全局初始化操作秘豹,盡量輕,或者延后處理昌粤,當然既绕,也會有一些ContentProvider與Receiver影響啟動這些都可以通過工具查看。
public class LabApplication extends Application {
@Override
public void onCreate() {
super.onCreate()
<!--UI中不要處理耗時操作-->
}
之后便是Activity的創(chuàng)建與啟動流程 :
可以看到上圖的Activity啟動流程都是被動消息的處理涮坐,主要是受控AMS指揮凄贩,代碼中設(shè)置View及顯示的流程也就上圖的幾個點,比如onCreate中設(shè)置Layout并inflater袱讹,當然疲扎,這不是必須的,即使不主動setContentView捷雕,在后面的wm.addView中也會創(chuàng)建頂層DecorView椒丧。
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
<!--可能影響耗時-->
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
而setContentView的inflate可能是影響耗時的一個點。之后handleResumeActivity中救巷,會想WMS添加窗口View
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
...
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
...
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
if (!a.mWindowAdded) {
a.mWindowAdded = true;
<!--添加Window-->
wm.addView(decor, l);
} else {
@Override
public final View getDecorView() {
if (mDecor == null || mForceDecorInstall) {
installDecor();
}
return mDecor;
}
可以看到 getDecorView會兜底處理Activity 的頂層窗口創(chuàng)建邏輯壶熏。addView會調(diào)用WindowManagerGlobal的addView,進而創(chuàng)建ViewRootImpl浦译,利用ViewRootImpl進一步添加Window
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
<!--關(guān)鍵-->
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
<!--添加到WMS棒假,處理UI顯示-->
root.setView(view, wparams, panelParentView);
}
}
而ViewRootImpl接管流程之后溯职,所以View相關(guān)的操作都將在ViewRootImpl處理,而最終由其requestLayout觸發(fā)測量帽哑、布局谜酒、繪制的動作
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
<!--申請下個Message繪制-->
requestLayout();
<!--添加窗口-->
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
}
}
}
requestLayout會scheduleTraversals預先占位一個異步消息,用于接收并doScheduleCallback觸發(fā)的VSYNC信號妻枕,這樣可以保證之后插入的消息都被延期處理僻族,從而Window被添加后,UI繪制任務第一時間執(zhí)行屡谐。
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
<!--異步消息-->
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
<!--插入繪制請求鹰贵,觸發(fā)VSYNC-->
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
等待VSYNC消息回來后,撤離異步消息柵欄康嘉,第一時間處理UI繪制:
所以首幀的渲染一定是在Resume之后碉输,那么具體的時機怎么把控?到底在哪亭珍,如下圖所示敷钾,插入的點在哪?
網(wǎng)上有一些其他的實現(xiàn)肄梨,認為可以監(jiān)聽onAttachedToWindow或者OnWindowFocusChange阻荒,onAttachedToWindow的問題是可能太過靠前,還沒有Draw, OnWindowFocusChange的缺點可能是太過滯后众羡,其實可以簡單認為view會的draw以后侨赡,View的繪制就算完成,雖然到展示還可能相差一個VSYNC等待圖層合成粱侣,但是對于性能監(jiān)測的評定羊壹,誤差一個固定值可以接受:
在onResume函數(shù)中插入一條消息可以嗎,理論上來說齐婴,太過靠前油猫,這條消息在執(zhí)行的時候,還沒Draw柠偶,因為請求VSYNC的同步柵欄是在是在Onresume結(jié)束后才插入的情妖,無法攔截之前的Message,但是由于VSYNC可能存在復用诱担,Onresume中插入的消息也有可能會在繪制之后執(zhí)行毡证,這個不是完全一定的,比如點擊MaterialButton啟動一個Activity蔫仙,第二個Activity的setView觸發(fā)的VSYNC就可能復用MaterialButton的波紋觸發(fā)的VSYNC料睛,從而導致第二個Activity的performTraval復用第一個VSYNC執(zhí)行,從而發(fā)生在onResume插入消息之前,如下
柵欄消息
重繪CallBack包含多個Activity的重繪
綜上所述秦效,將指標定義在第一次View的Draw執(zhí)行可能比較靠譜雏蛮。具體可以再DecorView上插入一個透明View涎嚼,監(jiān)聽器onDraw回調(diào)即可阱州,如果覺得不夠優(yōu)雅,就退一步法梯,監(jiān)聽OnWindowFocusChange的回調(diào)苔货,也勉強可以接受, OnWindowFocusChange一定是在Draw之后的。
有了指標立哑,那是否達標夜惭?如何采集?基線呢铛绰?可以參考業(yè)界做法诈茧,采集方式可以無入侵打點,而優(yōu)秀基線可以認為:
優(yōu)秀=秒開
如果發(fā)現(xiàn)不達標捂掰,接下來要做的就是定位+優(yōu)化敢会,這個時候就體現(xiàn)分析工具的重要性,其實上述的原理分析就已經(jīng)借助Studio自帶的Profiler工具这嚣,在理解流程上事半功倍鸥昏。
如何定位當前性能問題
冷啟動每個階段的耗時可以通過多種工具、方式來定位:可以用的有Debug.startMethodTracing跟蹤姐帚,也可以利用perfetto/systrace來查看吏垮,甚至還可以用Studio自身的Profiler跟蹤,每種方式都有自己的優(yōu)勢罐旗,可配合選擇使用膳汪。
Debug.startMethodTracing 適合查看UI線程的耗時函數(shù)
Debug.startMethodTracing是通過應用插樁來生成跟蹤日志,做到對方法的跟蹤九秀。但是啟用剖析功能后旅敷,應用的運行速度會減慢,所以颤霎,不應使用剖析數(shù)據(jù)確定絕對時間媳谁,最大的作用是用在對比上,可以對比之前友酱,或者對比周圍函數(shù)晴音。具體用法:
private void startTrace() {
File file = new File(getApplication().getExternalFilesDir("android"), "methods.trace");
Debug.startMethodTracing(file.getAbsolutePath(), 100 * 1024 * 1024);
}
<!-注意配對使用-->
private void stopTrace() {
Debug.stopMethodTracing();
}
對于冷啟動:進程啟動時開啟監(jiān)聽,在合適節(jié)點配對停止即可缔杉,之后導出.trace文件在Studio中分析锤躁,可以看到關(guān)鍵函數(shù)耗,Studio提供了多種模式或详,F(xiàn)lame Chart系羞、Top Down郭计、Bottom Up、Event椒振,不同的模式側(cè)重點不同昭伸。定向分析的時候,可以分段鎖定范圍澎迎,比如冷啟動可分幾個階段排查庐杨,進程創(chuàng)建、Application初始化夹供、Activity的創(chuàng)建灵份、create、resume哮洽、draw等填渠,先選定Main線程,然后將范圍限制定Application階段鸟辅,如下下:
- Flame Chart:更側(cè)重直觀反映函數(shù)耗時嚴重程度
比如上圖氛什,淺黃色部分其實就是需要重點關(guān)注的部分,耗時最多的函數(shù),會最先展示剔桨,更加方便定位嚴重問題屉更,大致定位問題后,就可以用Top Down 進一步看細節(jié)洒缀。
- Top Down: 更側(cè)重自頂向下詳細排查
利用Top-Down模式可以更精確觀察函數(shù)耗時與調(diào)用堆棧瑰谜,更加清晰,如下在Application初始化階段树绩,可以清醒看到函數(shù)調(diào)用順序萨脑、耗時等,
對于冷啟動饺饭,重點排查耗時函數(shù)渤早,嘗試將非核心邏輯從UI線程中移除。同理對于閃屏Activity的onCreate跟onResume階段所做的處理類似
從圖中就很容下發(fā)現(xiàn)瘫俊,有些Flutterboost鹊杖、埋點Json解析類的耗時操作被不小心關(guān)聯(lián)進了Activit的啟動流程中,拖慢了冷啟動速度扛芽,那就可以放到非UI線程中處理骂蓖,或者延后處理。
- Bottom Up:一種平鋪的模式川尖,
這個模式個人用的不多登下,羅列的函數(shù)太多,沒有層次,可能單獨看排名靠前的幾個有些收益被芳。
依賴profiler基本能定位哪些函數(shù)導致了冷啟動速度慢缰贝,但是這些函數(shù)可能并非自己耗時嚴重,也許是會因為調(diào)度或者鎖的原因?qū)е侣媳簦@個時候perfetto/systrace會提供更多幫助剩晴。
perfetto/systrace:大局與調(diào)度
perfetto/systrace是官方提供另一種性能分析工具,其中perfetto可以看做是systrace的升級版篓冲。相比MethodTracing代碼插樁李破,無法具體到每個方法宠哄,但可以提供全局性能概覽,可以更快定位問題范圍,而且perfetto/systrace在全局任務調(diào)度泳赋、系統(tǒng)調(diào)用上更具優(yōu)勢川无,MethodTracing多少對于性能有些影響,而perfetto/systrace借助系統(tǒng)本身lOG承粤,可以降低自身帶來的影響暴区,用perfetto看一下冷啟動的流程,如下:
如圖辛臊,首先你就能直觀的看到那些階段的耗時比較嚴重仙粱,然后定向分析即可,將時間段收縮彻舰,放大觀察:
可以直觀看出Activity啟動時藍色標記的資源解析耗時過長伐割,定向排查后發(fā)現(xiàn)圖大
Name res/BKC.xml
Category null
Start time 1s 309ms 568us 459ns
Duration 42ms 35us 682ns
Slice ID 2465
適當將圖縮小,降低加載成本刃唤,經(jīng)過優(yōu)化縮短到8ms
Name res/BKC.xml
Category null
Start time 964ms 602us 749ns
Duration 8ms 260us 261ns
Slice ID 11851
type internal_slice
再比如隔心,對于有些階段,UI線程莫名的睡眠尚胞,其實可以比較方便的查看是什么因素導致的硬霍,如下:
如上所示,在當前階段笼裳,UI線程因為沒有獲取鎖進入了睡眠唯卖,之后,被另一個線程喚起了躬柬,這就可以排查到底是哪個地方有問題拜轨,是否可以避免,大概的使用方式就是如此楔脯。
對于整體冷啟動優(yōu)化效果:用perfetto看比較直接
優(yōu)化前:1261ms
優(yōu)化后:439ms
所用的優(yōu)化除了上面的措施還有部分如下措施等:
- 延遲非必要receiver的注冊
- 閃屏廣告Layout布局按需加載
- 鎖優(yōu)化撩轰,進程線程間阻塞優(yōu)化
所用的優(yōu)化除了上面的措施還有部分如下措施等:核心原則 UI線程不做耗時操作
- 延遲非必要receiver的注冊
- 閃屏廣告Layout布局按需加載
- 鎖優(yōu)化,進程線程間阻塞優(yōu)化
總結(jié)
BUG是必然的,優(yōu)化是持久的堪嫂,如何用好工具是關(guān)鍵的偎箫。
作者:看書的小蝸牛
原文鏈接: APP冷啟動優(yōu)化:如何使用好工具【Perfetto\ systrace \MethodTracing】