/ 開始 /
轉載於郭霖公衆號:
Stan_Z的博客地址:
https://www.jianshu.com/u/7f26e9b13731
繼上一篇卡頓優化後(見作者原文),開始盤點卡頓/丟幀的第一個小分支:佈局優化。還是老規矩,先列大綱:
/ 基礎知識 /
1.1 佈局加載流程
1.2 佈局繪製相關流程
觸發addView流程:
performTraversals流程:
measure、layout、draw流程:
注:圖片來源於工匠若水
/ 優化工具 /
首先簡單介紹下繪製優化相關的工具,這裏systrace和traceView依然好使,按繪製流程階段發現繪製耗時函數。這部分同卡頓篇原理一致就不贅述了。
2.1 Lint
靜態代碼檢測工具,通過對代碼進行靜態分析,可以幫助開發者發現代碼質量問題和提出一些改進建議。AS中目前大概有200個左右的lint檢查,當然有特殊需求的可以自定義:【我的Android進階之旅】Android自定義Lint實踐
這裏簡單看下佈局相關的兩個檢查項:
點擊Analyze的Inspect Code觸發Lint檢測
2.2 show GPU overdraw & GPU rendering
Settings/開發者選項/調試GPU過度繪製
Settings/開發者選項/HWUI呈現模式分析
1)在屏幕上顯示爲條形圖:
2)adb shell dumpsys gfxinfo
2.3 Layout Inspector
AS:Tools > Android > Layout Inspector 選擇對應進程
左側看視圖層級結構,右側看具體屬性和賦值內容。
/ 監控 /
3.1 佈局整體耗時監控:
可以使用AspectJ做面向aop的非侵入性的監控。
工程主gradle:
<pre style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; font-size: inherit; color: inherit; line-height: inherit;">
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0’
</pre>
項目gradle:
<pre style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; font-size: inherit; color: inherit; line-height: inherit;">
apply plugin: 'android-aspectjx’
implementation 'org.aspectj:aspectjrt:1.8.+’
</pre>
針對Activity.setContentView監控簡單示例:
<pre style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; font-size: inherit; color: inherit; line-height: inherit;">
@Aspect
public class PerformanceAop {
public static final String TAG = "aop";
@Around("execution(* android.app.Activity.setContentView(..))")
public void getSetContentViewTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Log.i(TAG, name + " cost " + (System.currentTimeMillis() - time));
}
}
</pre>
3.2 單個視圖創建耗時監控:
Factory2、Factory本質上他倆就是創建View的一個hook,可以通過這個回調來監控單個View創建耗時情況。
注:Factory2繼承自Factory,Factory2比Factory的onCreateView方法多一個parent的參數,即當前創建View的父View。
簡單示例:
<pre style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; font-size: inherit; color: inherit; line-height: inherit;">
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
@Nullable
@Override
public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
//1.配合getDelegate().createView來做高版本控件的兼容適配。
//2.單個View創建耗時統計。
long time = System.currentTimeMillis();
View view = getDelegate().createView(parent, name, context, attrs);
Log.i("TAG", name + " cost: " + (System.currentTimeMillis() - time));
return view;
}
@Nullable
@Override
public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
return null;
}
});
</pre>
這裏有一點要注意:setFactory2必須在super.onCreate(savedInstanceState)之前,不然會報如下錯誤:
<pre style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; font-size: inherit; color: inherit; line-height: inherit;">
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.stan.topnews/com.stan.topnews.app.MainActivity}: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3314)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3453)
</pre>
打印結果:
<pre style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; font-size: inherit; color: inherit; line-height: inherit;">
2020-03-11 16:43:07.389 17078-17078/com.stan.topnews I/Perf: Connecting to perf service.
2020-03-11 16:43:07.567 17078-17078/com.stan.topnews I/perf: LinearLayout cost: 13
2020-03-11 16:43:07.569 17078-17078/com.stan.topnews I/perf: ViewStub cost: 0
2020-03-11 16:43:07.634 17078-17078/com.stan.topnews I/perf: TextView cost: 16
2020-03-11 16:43:07.637 17078-17078/com.stan.topnews I/perf: TextView cost: 3
...
</pre>
3.3 佈局繪製監控
這裏用到的還是FPS,就監控一個doFrame。
簡單示例:
<pre style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; font-size: inherit; color: inherit; line-height: inherit;">
private long mStartFrameTime = 0;
private int mFrameCount = 0;
/**
單次計算FPS使用160毫秒
/
private static final long MONITOR_INTERVAL = 160L;
private static final long MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000L;
/*-
設置計算fps的單位時間間隔1000ms,即fps/s
*/
private static final long MAX_INTERVAL = 1000L;
private void getFPS() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
return;
}getWindow().getDecorView().getViewTreeObserver().addOnDrawListener(new ViewTreeObserver.OnDrawListener() {
@Override
public void onDraw() {
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
if (mStartFrameTime == 0) {
mStartFrameTime = frameTimeNanos;
}
long interval = frameTimeNanos - mStartFrameTime;
if (interval > MONITOR_INTERVAL_NANOS) {
double fps = (((double) (mFrameCount * 1000L * 1000L)) / interval) * MAX_INTERVAL;
Log.i(TAG, "fps:" + fps);
mFrameCount = 0;
mStartFrameTime = 0;
} else {
++mFrameCount;
}
}
});
}
});
}
</pre>
FPS相關成熟三方庫:
matrix 微信的卡頓檢測方案,採用的ASM插樁的方式,支持fps和堆棧獲取的定位,但是需要自己根據asm插樁的方法id來自己分析堆棧,定位精確度高,性能消耗小,比較可惜的是目前沒有界面展示,對代碼有一定的侵入性。如果線上使用可以考慮。
fpsviewer 利用Choreographer.FrameCallback來監控卡頓和Fps的計算,異步線程進行週期採樣,當前的幀耗時超過自定義的閾值時,將幀進行分析保存,不影響正常流程的進行,待需要的時候進行展示,定位。
/ 佈局加載優化 /
前面簡單瞭解了佈局加載流程,
性能瓶頸在於LayoutInflater.inflater過程,主要包括如下兩點:
xmlPullParser IO操作,佈局越複雜,IO耗時越長。
createView 反射,View越多,反射調用次數越多,耗時越長,但是這必須達到一定量級纔會有明顯影響。Java反射到底慢在哪?
那麼很容易想到兩個解決辦法:要麼把IO和反射交由子線程來處理,要麼通過動態加載視圖把IO和反射規避掉。那麼市面上有沒有相關的成熟方案呢?當然是有的,下面來簡單看一看:
AsyncLayoutInflater
https://developer.android.com/reference/android/support/v4/view/AsyncLayoutInflater
AsyncLayoutInflater是google提供的方案,讓LayoutInflater.inflater過程通過子線程來做:
<pre style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; font-size: inherit; color: inherit; line-height: inherit;">
new AsyncLayoutInflater(AsyncLayoutActivity.this)
.inflate(R.layout.async_layout, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(View view, int resid, ViewGroup parent) {
setContentView(view);
}
});
</pre>
實現也很簡單:handle+thread+queue+inflater。可以理解爲具有loop能力的子線程來實現的耗時部分異步處理。
這裏有兩點侷限性:
不能設置LayoutInflater.Factory/Factory2
線程安全問題
詳細源碼分析和自定義AsyncLayoutInflater解決侷限性問題可以參考如下文章,我就不重複造輪子了:
Android AsyncLayoutInflater 源碼解析
Android AsyncLayoutInflater 限制及改進
X2C
https://github.com/iReaderAndroid/X2C/blob/master/README_CN.md
動態加載視圖,這樣能避免IO和反射,但是這樣缺點是可讀性差、可維護性差,因此掌閱團隊開發的X2C做了魚和熊掌都兼得的方案:X2C,它原理是採用APT(Annotation Processor Tool)+ JavaPoet技術來完成編譯期間視圖xml佈局生成java代碼,這樣佈局依然是用xml來寫,編譯期X2C會將xml轉化爲動態加載視圖的java代碼。
這裏個人理解可能存在的侷限性:
失去系統兼容AppCompat
是不是能全面支持所有佈局屬性及自定義屬性
如果視圖全部用X2C來處理,會造成代碼冗餘。
/ 佈局繪製優化 /
這部分是由ViewRootImpl觸發的performTraversals,它主要包含:measure(確定ViewGroup以及View的大小) layout(ViewGroup決定View的擺放位置) draw(繪製視圖)三個部分。另外,繪製好的DisplayListOp tree最終需要經過OpenGL命令轉換交由GPU渲染,如果同一個像素點被多次重複繪製,勢必也是造成浪費以及GPU任務變重。
因此佈局繪製最終優化方向就是如下兩個:
5.1 優化佈局層級及其複雜度
measure、layout、draw這三個過程都包含的自頂向下的view tree遍歷耗時,它是由視圖層級太深會造成耗時,另外也要避免類似RealtiveLayout嵌套造成的多次觸發measure、layout的問題。最後onDraw在頻繁刷新時可能多次被觸發,因此onDraw不能做耗時操作,同時不能有內存抖動隱患等。
優化思路:
減少View樹層級
佈局儘量寬而淺,避免窄而深
ConstraintLayout 實現幾乎完全扁平化佈局,同時具備RelativeLayout和LinearLayout特性,在構建複雜佈局時性能更高。
不嵌套使用RelativeLayout
不在嵌套LinearLayout中使用weight
merge標籤使用:減少一個根ViewGroup層級
ViewStub 延遲化加載標籤,當佈局整體被inflater,ViewStub也會被解析但是其內存佔用非常低,它在使用前是作爲佔位符存在,對ViewStub的inflater操作只能進行一次,也就是隻能被替換1次。
5.2 避免過度繪製
一個像素最好只被繪製一次。
優化思路:
去掉多餘的background,減少複雜shape的使用
避免層級疊加
自定義View使用clipRect屏蔽被遮蓋View繪製
5.3 視圖與數據綁定耗時
由於網絡請求或者複雜數據處理邏輯耗時導致與視圖綁定不及時。這裏可以從優化數據處理的維度來解決。
/ Litho介紹 /
Litho
是 FaceBook 2017年上半年開源的聲明式UI渲染框架。
主要針對RecyclerView複雜滑動列表做了以下幾點優化:
視圖的細粒度複用,可以減少一定程度的內存佔用。
異步計算佈局,把測量和佈局放到異步線程進行。
扁平化視圖,把複雜的佈局拍成極致的扁平效果,優化複雜列表滑動時由佈局計算導致的卡頓問題。
這裏具體實戰可以瞭解下Litho在美團動態化方案MTFlexbox中的實踐
https://tech.meituan.com/2019/09/19/litho-practice-in-dynamic-program-mtflexbox.html
/ 其他 /
本篇文章對佈局優化做了一個全局的簡單梳理,也提供一些常規的優化思路以及目前市面上比較成熟的三方庫。最終所有的優化點都需要落地到具體的技術點上,因此這裏再簡單例舉一些個人認爲值得去研究和學習的若干技術點:
AspectJ使用和原理 參考:AOP之AspectJ 技術原理詳解及實戰總結
ConstraintLayout的使用 參考:約束佈局ConstraintLayout看這一篇就夠了
如何異步改造AsyncLayoutInflater,讓它能設置LayoutInflater.Factory/Factory2以及保證線程安全 參考:Android AsyncLayoutInflater 限制及改進)
X2C用到的APT(Annotation Processor Tool)+ JavaPoet技術,這裏着重需要了解:運行時註解(藉助反射機制實現)VS 編譯時註解(APT)具體運用場景。參考:註解(反射+APT)整理(附帶腦圖)
Litho的實現原理 參考:Litho的使用及原理剖析
當然有更好的文章也可以推薦給我學習學習。