Android性能優(yōu)化盤點(diǎn) - 布局優(yōu)化

繼上一篇卡頓優(yōu)化后,開始盤點(diǎn)卡頓/丟幀的第一個(gè)小分支:布局優(yōu)化植酥。還是老規(guī)矩俭识,先列大綱:

布局優(yōu)化盤點(diǎn)大綱
一、基礎(chǔ)知識(shí)
1.1 布局加載流程
布局加載流程簡(jiǎn)單示意圖
1.2 布局繪制相關(guān)流程
觸發(fā)addView流程:
performTraversals流程:
measure兼都、layout嫂沉、draw流程:


注:圖片來源于工匠若水

二、優(yōu)化工具

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

2.1 Lint

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

這里簡(jiǎn)單看下布局相關(guān)的兩個(gè)檢查項(xiàng):

點(diǎn)擊Analyze的Inspect Code觸發(fā)Lint檢測(cè)

2.2 show GPU overdraw & GPU rendering

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

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

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

2)adb shell dumpsys gfxinfo

2.3 Layout Inspector

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

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

三确丢、監(jiān)控
3.1 布局整體耗時(shí)監(jiān)控:

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

工程主gradle:

classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0’

項(xiàng)目gradle:

apply plugin: 'android-aspectjx’

implementation 'org.aspectj:aspectjrt:1.8.+’

針對(duì)Activity.setContentView監(jiān)控簡(jiǎn)單示例:

@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));
   }
}
3.2 單個(gè)視圖創(chuàng)建耗時(shí)監(jiān)控:

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

注:Factory2繼承自Factory,F(xiàn)actory2比Factory的onCreateView方法多一個(gè)parent的參數(shù)剃毒,即當(dāng)前創(chuàng)建View的父View病袄。

簡(jiǎn)單示例:

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.單個(gè)View創(chuàng)建耗時(shí)統(tǒng)計(jì)赘阀。
       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;
   }
});

這里有一點(diǎn)要注意:setFactory2必須在super.onCreate(savedInstanceState)之前益缠,不然會(huì)報(bào)如下錯(cuò)誤:

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)

打印結(jié)果:

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

這里用到的還是FPS,就監(jiān)控一個(gè)doFrame基公。

簡(jiǎn)單示例:

private long mStartFrameTime = 0;
private int mFrameCount = 0;
/**
* 單次計(jì)算FPS使用160毫秒
*/
private static final long MONITOR_INTERVAL = 160L;
private static final long MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000L;
/**
* 設(shè)置計(jì)算fps的單位時(shí)間間隔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;
                   }
               }
           });
       }
   });
}

FPS相關(guān)成熟三方庫:

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

fpsviewer 利用Choreographer.FrameCallback來監(jiān)控卡頓和Fps的計(jì)算渗饮,異步線程進(jìn)行周期采樣,當(dāng)前的幀耗時(shí)超過自定義的閾值時(shí)宿刮,將幀進(jìn)行分析保存互站,不影響正常流程的進(jìn)行,待需要的時(shí)候進(jìn)行展示僵缺,定位胡桃。

四、布局加載優(yōu)化

前面簡(jiǎn)單了解了布局加載流程磕潮,

性能瓶頸在于LayoutInflater.inflater過程标捺,主要包括如下兩點(diǎn):

  • xmlPullParser IO操作,布局越復(fù)雜揉抵,IO耗時(shí)越長(zhǎng)。
  • createView 反射嗤疯,View越多冤今,反射調(diào)用次數(shù)越多,耗時(shí)越長(zhǎng)茂缚,但是這必須達(dá)到一定量級(jí)才會(huì)有明顯影響戏罢。Java反射到底慢在哪屋谭?

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

AsyncLayoutInflater

AsyncLayoutInflater是google提供的方案讲岁,讓LayoutInflater.inflater過程通過子線程來做:

     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);
                   }
               });

實(shí)現(xiàn)也很簡(jiǎn)單:handle+thread+queue+inflater我擂。可以理解為具有l(wèi)oop能力的子線程來實(shí)現(xiàn)的耗時(shí)部分異步處理缓艳。

這里有兩點(diǎn)局限性:

  • 不能設(shè)置LayoutInflater.Factory/Factory2
  • 線程安全問題

詳細(xì)源碼分析和自定義AsyncLayoutInflater解決局限性問題可以參考如下文章校摩,我就不重復(fù)造輪子了:
Android AsyncLayoutInflater 源碼解析
Android AsyncLayoutInflater 限制及改進(jìn)

X2C

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

這里個(gè)人理解可能存在的局限性:

  • 失去系統(tǒng)兼容AppCompat
  • 是不是能全面支持所有布局屬性及自定義屬性
  • 如果視圖全部用X2C來處理摹芙,會(huì)造成代碼冗余。
五惜浅、布局繪制優(yōu)化

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

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

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

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

優(yōu)化思路:

  • 減少View樹層級(jí)
  • 布局盡量寬而淺,避免窄而深
  • ConstraintLayout 實(shí)現(xiàn)幾乎完全扁平化布局焦影,同時(shí)具備RelativeLayout和LinearLayout特性车遂,在構(gòu)建復(fù)雜布局時(shí)性能更高封断。
  • 不嵌套使用RelativeLayout
  • 不在嵌套LinearLayout中使用weight
  • merge標(biāo)簽使用:減少一個(gè)根ViewGroup層級(jí)
  • ViewStub 延遲化加載標(biāo)簽,當(dāng)布局整體被inflater舶担,ViewStub也會(huì)被解析但是其內(nèi)存占用非常低坡疼,它在使用前是作為占位符存在,對(duì)ViewStub的inflater操作只能進(jìn)行一次衣陶,也就是只能被替換1次柄瑰。
5.2 避免過度繪制

一個(gè)像素最好只被繪制一次。

優(yōu)化思路:

  • 去掉多余的background祖搓,減少?gòu)?fù)雜shape的使用
  • 避免層級(jí)疊加
  • 自定義View使用clipRect屏蔽被遮蓋View繪制
5.3 視圖與數(shù)據(jù)綁定耗時(shí)

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

六拯欧、Litho介紹

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

主要針對(duì)RecyclerView復(fù)雜滑動(dòng)列表做了以下幾點(diǎn)優(yōu)化:

  • 視圖的細(xì)粒度復(fù)用,可以減少一定程度的內(nèi)存占用镐作。
  • 異步計(jì)算布局藏姐,把測(cè)量和布局放到異步線程進(jìn)行。
  • 扁平化視圖该贾,把復(fù)雜的布局拍成極致的扁平效果羔杨,優(yōu)化復(fù)雜列表滑動(dòng)時(shí)由布局計(jì)算導(dǎo)致的卡頓問題。

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

其他

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

當(dāng)然有更好的文章也可以推薦給我學(xué)習(xí)學(xué)習(xí)寇荧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載举庶,如需轉(zhuǎn)載請(qǐng)通過簡(jiǎn)信或評(píng)論聯(lián)系作者。
  • 序言:七十年代末揩抡,一起剝皮案震驚了整個(gè)濱河市户侥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌峦嗤,老刑警劉巖蕊唐,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異烁设,居然都是意外死亡刃泌,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來耙替,“玉大人,你說我怎么就攤上這事曹体∷咨龋” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵箕别,是天一觀的道長(zhǎng)铜幽。 經(jīng)常有香客問我,道長(zhǎng)串稀,這世上最難降的妖魔是什么除抛? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮母截,結(jié)果婚禮上到忽,老公的妹妹穿的比我還像新娘。我一直安慰自己清寇,他們只是感情好喘漏,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著华烟,像睡著了一般翩迈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盔夜,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天负饲,我揣著相機(jī)與錄音,去河邊找鬼喂链。 笑死返十,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的衩藤。 我是一名探鬼主播吧慢,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼赏表!你這毒婦竟也來了检诗?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤瓢剿,失蹤者是張志新(化名)和其女友劉穎逢慌,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體间狂,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡攻泼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片忙菠。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡何鸡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出牛欢,到底是詐尸還是另有隱情骡男,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布傍睹,位于F島的核電站隔盛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏拾稳。R本人自食惡果不足惜吮炕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望访得。 院中可真熱鬧龙亲,春花似錦、人聲如沸震鹉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽传趾。三九已至迎膜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浆兰,已是汗流浹背磕仅。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留簸呈,地道東北人榕订。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像蜕便,于是被迫代替她去往敵國(guó)和親劫恒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容