2019-04-10(Android 布局優(yōu)化)

/ 開始 /
轉(zhuǎn)載于郭霖公眾號:
Stan_Z的博客地址:
http://www.itdecent.cn/u/7f26e9b13731

繼上一篇卡頓優(yōu)化后(見作者原文),開始盤點卡頓/丟幀的第一個小分支:布局優(yōu)化。還是老規(guī)矩,先列大綱:

image

/ 基礎(chǔ)知識 /

1.1 布局加載流程

image

1.2 布局繪制相關(guān)流程

觸發(fā)addView流程:

image

performTraversals流程:

image

measure、layout、draw流程:

image

注:圖片來源于工匠若水

https://blog.csdn.net/yanbober

/ 優(yōu)化工具 /

首先簡單介紹下繪制優(yōu)化相關(guān)的工具,這里systrace和traceView依然好使,按繪制流程階段發(fā)現(xiàn)繪制耗時函數(shù)。這部分同卡頓篇原理一致就不贅述了。

2.1 Lint

靜態(tài)代碼檢測工具,通過對代碼進行靜態(tài)分析,可以幫助開發(fā)者發(fā)現(xiàn)代碼質(zhì)量問題和提出一些改進建議。AS中目前大概有200個左右的lint檢查,當然有特殊需求的可以自定義:【我的Android進階之旅】Android自定義Lint實踐

https://blog.csdn.net/ouyang_peng/article/details/80374867

這里簡單看下布局相關(guān)的兩個檢查項:

image

點擊Analyze的Inspect Code觸發(fā)Lint檢測

image

2.2 show GPU overdraw & GPU rendering

http://www.itdecent.cn/p/a0e8575e9846

Settings/開發(fā)者選項/調(diào)試GPU過度繪制

image

Settings/開發(fā)者選項/HWUI呈現(xiàn)模式分析

1)在屏幕上顯示為條形圖:

image

2)adb shell dumpsys gfxinfo

https://developer.android.com/training/testing/performance

2.3 Layout Inspector

http://www.itdecent.cn/p/1b64024f2d08

AS:Tools > Android > Layout Inspector 選擇對應(yīng)進程

image

左側(cè)看視圖層級結(jié)構(gòu),右側(cè)看具體屬性和賦值內(nèi)容。

/ 監(jiān)控 /

3.1 布局整體耗時監(jiān)控:

可以使用AspectJ做面向aop的非侵入性的監(jiān)控。

工程主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監(jiān)控簡單示例:

<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 單個視圖創(chuàng)建耗時監(jiān)控:

Factory2、Factory本質(zhì)上他倆就是創(chuàng)建View的一個hook,可以通過這個回調(diào)來監(jiān)控單個View創(chuàng)建耗時情況。

注:Factory2繼承自Factory,F(xiàn)actory2比Factory的onCreateView方法多一個parent的參數(shù),即當前創(chuàng)建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創(chuàng)建耗時統(tǒng)計。
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>

打印結(jié)果:

<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 布局繪制監(jiān)控

這里用到的還是FPS,就監(jiān)控一個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;
    /
    *

  • 設(shè)置計算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相關(guān)成熟三方庫:

matrix 微信的卡頓檢測方案,采用的ASM插樁的方式,支持fps和堆棧獲取的定位,但是需要自己根據(jù)asm插樁的方法id來自己分析堆棧,定位精確度高,性能消耗小,比較可惜的是目前沒有界面展示,對代碼有一定的侵入性。如果線上使用可以考慮。

fpsviewer 利用Choreographer.FrameCallback來監(jiān)控卡頓和Fps的計算,異步線程進行周期采樣,當前的幀耗時超過自定義的閾值時,將幀進行分析保存,不影響正常流程的進行,待需要的時候進行展示,定位。

/ 布局加載優(yōu)化 /

前面簡單了解了布局加載流程,

性能瓶頸在于LayoutInflater.inflater過程,主要包括如下兩點:

  • xmlPullParser IO操作,布局越復(fù)雜,IO耗時越長。

  • createView 反射,View越多,反射調(diào)用次數(shù)越多,耗時越長,但是這必須達到一定量級才會有明顯影響。Java反射到底慢在哪?

那么很容易想到兩個解決辦法:要么把IO和反射交由子線程來處理,要么通過動態(tài)加載視圖把IO和反射規(guī)避掉。那么市面上有沒有相關(guān)的成熟方案呢?當然是有的,下面來簡單看一看:

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>

實現(xiàn)也很簡單:handle+thread+queue+inflater??梢岳斫鉃榫哂衛(wèi)oop能力的子線程來實現(xiàn)的耗時部分異步處理。

這里有兩點局限性:

  • 不能設(shè)置LayoutInflater.Factory/Factory2

  • 線程安全問題

詳細源碼分析和自定義AsyncLayoutInflater解決局限性問題可以參考如下文章,我就不重復(fù)造輪子了:

Android AsyncLayoutInflater 源碼解析

http://www.itdecent.cn/p/a3a3bd314c45

Android AsyncLayoutInflater 限制及改進

http://www.itdecent.cn/p/f0c0eda06ae4

X2C

https://github.com/iReaderAndroid/X2C/blob/master/README_CN.md

動態(tài)加載視圖,這樣能避免IO和反射,但是這樣缺點是可讀性差、可維護性差,因此掌閱團隊開發(fā)的X2C做了魚和熊掌都兼得的方案:X2C,它原理是采用APT(Annotation Processor Tool)+ JavaPoet技術(shù)來完成編譯期間視圖xml布局生成java代碼,這樣布局依然是用xml來寫,編譯期X2C會將xml轉(zhuǎn)化為動態(tài)加載視圖的java代碼。

這里個人理解可能存在的局限性:

  • 失去系統(tǒng)兼容AppCompat

  • 是不是能全面支持所有布局屬性及自定義屬性

  • 如果視圖全部用X2C來處理,會造成代碼冗余。

/ 布局繪制優(yōu)化 /

這部分是由ViewRootImpl觸發(fā)的performTraversals,它主要包含:measure(確定ViewGroup以及View的大?。?layout(ViewGroup決定View的擺放位置) draw(繪制視圖)三個部分。另外,繪制好的DisplayListOp tree最終需要經(jīng)過OpenGL命令轉(zhuǎn)換交由GPU渲染,如果同一個像素點被多次重復(fù)繪制,勢必也是造成浪費以及GPU任務(wù)變重。

因此布局繪制最終優(yōu)化方向就是如下兩個:

5.1 優(yōu)化布局層級及其復(fù)雜度

measure、layout、draw這三個過程都包含的自頂向下的view tree遍歷耗時,它是由視圖層級太深會造成耗時,另外也要避免類似RealtiveLayout嵌套造成的多次觸發(fā)measure、layout的問題。最后onDraw在頻繁刷新時可能多次被觸發(fā),因此onDraw不能做耗時操作,同時不能有內(nèi)存抖動隱患等。

優(yōu)化思路:

  • 減少View樹層級

  • 布局盡量寬而淺,避免窄而深

  • ConstraintLayout 實現(xiàn)幾乎完全扁平化布局,同時具備RelativeLayout和LinearLayout特性,在構(gòu)建復(fù)雜布局時性能更高。

  • 不嵌套使用RelativeLayout

  • 不在嵌套LinearLayout中使用weight

  • merge標簽使用:減少一個根ViewGroup層級

  • ViewStub 延遲化加載標簽,當布局整體被inflater,ViewStub也會被解析但是其內(nèi)存占用非常低,它在使用前是作為占位符存在,對ViewStub的inflater操作只能進行一次,也就是只能被替換1次。

5.2 避免過度繪制

一個像素最好只被繪制一次。

優(yōu)化思路:

  • 去掉多余的background,減少復(fù)雜shape的使用

  • 避免層級疊加

  • 自定義View使用clipRect屏蔽被遮蓋View繪制

5.3 視圖與數(shù)據(jù)綁定耗時

由于網(wǎng)絡(luò)請求或者復(fù)雜數(shù)據(jù)處理邏輯耗時導(dǎo)致與視圖綁定不及時。這里可以從優(yōu)化數(shù)據(jù)處理的維度來解決。

/ Litho介紹 /

Litho

https://fblitho.com/docs/intro

是 FaceBook 2017年上半年開源的聲明式UI渲染框架。

主要針對RecyclerView復(fù)雜滑動列表做了以下幾點優(yōu)化:

視圖的細粒度復(fù)用,可以減少一定程度的內(nèi)存占用。

異步計算布局,把測量和布局放到異步線程進行。

扁平化視圖,把復(fù)雜的布局拍成極致的扁平效果,優(yōu)化復(fù)雜列表滑動時由布局計算導(dǎo)致的卡頓問題。

這里具體實戰(zhàn)可以了解下Litho在美團動態(tài)化方案MTFlexbox中的實踐

https://tech.meituan.com/2019/09/19/litho-practice-in-dynamic-program-mtflexbox.html

/ 其他 /

本篇文章對布局優(yōu)化做了一個全局的簡單梳理,也提供一些常規(guī)的優(yōu)化思路以及目前市面上比較成熟的三方庫。最終所有的優(yōu)化點都需要落地到具體的技術(shù)點上,因此這里再簡單例舉一些個人認為值得去研究和學習的若干技術(shù)點:

AspectJ使用和原理 參考:AOP之AspectJ 技術(shù)原理詳解及實戰(zhàn)總結(jié)

https://blog.csdn.net/zlmrche/article/details/79643801

ConstraintLayout的使用 參考:約束布局ConstraintLayout看這一篇就夠了

http://www.itdecent.cn/p/17ec9bd6ca8a

如何異步改造AsyncLayoutInflater,讓它能設(shè)置LayoutInflater.Factory/Factory2以及保證線程安全 參考:Android AsyncLayoutInflater 限制及改進)

http://www.itdecent.cn/p/f0c0eda06ae4

X2C用到的APT(Annotation Processor Tool)+ JavaPoet技術(shù),這里著重需要了解:運行時注解(借助反射機制實現(xiàn))VS 編譯時注解(APT)具體運用場景。參考:注解(反射+APT)整理(附帶腦圖)

https://blog.csdn.net/qq_31391977/article/details/83784319

Litho的實現(xiàn)原理 參考:Litho的使用及原理剖析

https://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651750430&idx=2&sn=89c8c1212f4b6a24694028ec3188aa09&from=timeline

當然有更好的文章也可以推薦給我學習學習。

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

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