2019-04-10(Android 布局优化)

/ 开始 /
转载于郭霖公众号:
Stan_Z的博客地址:
https://www.jianshu.com/u/7f26e9b13731

继上一篇卡顿优化后(见作者原文),开始盘点卡顿/丢帧的第一个小分支:布局优化。还是老规矩,先列大纲:

image

/ 基础知识 /

1.1 布局加载流程

image

1.2 布局绘制相关流程

触发addView流程:

image

performTraversals流程:

image

measure、layout、draw流程:

image

注:图片来源于工匠若水

https://blog.csdn.net/yanbober

/ 优化工具 /

首先简单介绍下绘制优化相关的工具,这里systrace和traceView依然好使,按绘制流程阶段发现绘制耗时函数。这部分同卡顿篇原理一致就不赘述了。

2.1 Lint

静态代码检测工具,通过对代码进行静态分析,可以帮助开发者发现代码质量问题和提出一些改进建议。AS中目前大概有200个左右的lint检查,当然有特殊需求的可以自定义:【我的Android进阶之旅】Android自定义Lint实践

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

这里简单看下布局相关的两个检查项:

image

点击Analyze的Inspect Code触发Lint检测

image

2.2 show GPU overdraw & GPU rendering

https://www.jianshu.com/p/a0e8575e9846

Settings/开发者选项/调试GPU过度绘制

image

Settings/开发者选项/HWUI呈现模式分析

1)在屏幕上显示为条形图:

image

2)adb shell dumpsys gfxinfo

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

2.3 Layout Inspector

https://www.jianshu.com/p/1b64024f2d08

AS:Tools > Android > Layout Inspector 选择对应进程

image

左侧看视图层级结构,右侧看具体属性和赋值内容。

/ 监控 /

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 源码解析

https://www.jianshu.com/p/a3a3bd314c45

Android AsyncLayoutInflater 限制及改进

https://www.jianshu.com/p/f0c0eda06ae4

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

https://fblitho.com/docs/intro

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

主要针对RecyclerView复杂滑动列表做了以下几点优化:

视图的细粒度复用,可以减少一定程度的内存占用。

异步计算布局,把测量和布局放到异步线程进行。

扁平化视图,把复杂的布局拍成极致的扁平效果,优化复杂列表滑动时由布局计算导致的卡顿问题。

这里具体实战可以了解下Litho在美团动态化方案MTFlexbox中的实践

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

/ 其他 /

本篇文章对布局优化做了一个全局的简单梳理,也提供一些常规的优化思路以及目前市面上比较成熟的三方库。最终所有的优化点都需要落地到具体的技术点上,因此这里再简单例举一些个人认为值得去研究和学习的若干技术点:

AspectJ使用和原理 参考:AOP之AspectJ 技术原理详解及实战总结

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

ConstraintLayout的使用 参考:约束布局ConstraintLayout看这一篇就够了

https://www.jianshu.com/p/17ec9bd6ca8a

如何异步改造AsyncLayoutInflater,让它能设置LayoutInflater.Factory/Factory2以及保证线程安全 参考:Android AsyncLayoutInflater 限制及改进)

https://www.jianshu.com/p/f0c0eda06ae4

X2C用到的APT(Annotation Processor Tool)+ JavaPoet技术,这里着重需要了解:运行时注解(借助反射机制实现)VS 编译时注解(APT)具体运用场景。参考:注解(反射+APT)整理(附带脑图)

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

Litho的实现原理 参考:Litho的使用及原理剖析

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

当然有更好的文章也可以推荐给我学习学习。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349