例说 Constraint Layout(三)—— 性能测评

0 引言

去年写完《例说 Constraint Layout(一)—— 概论》后过去一年多了,怎么《例说》的(二)、(三)就“难产”了?究其根本的原因,一是尝试实测 Constraint Layout(CL) 性能时,用 DDMS(Dalvik Debug Monitor Service)查看后发现性能没有明显提升;二则官方也说,如果项目中原有的布局没有性能问题的话,不必切换成 CL,又正好面临安装包过大的问题,就没有引入 CL;三是测试一次性能特别麻烦,忙起来就拖到新版的 AS 甚至不集成 DDMS 了(用命令行去 Platform 下也打不开了)。幸好这时候遇到了 《Understanding the performance benefits of ConstraintLayout》[1]这篇文章(作者为 Google 工作,也是可伸缩布局 FlexboxLayout 的作者),本文参照了其测试方式,全面地对 CL 的性能进行了评测。本文结论浓缩成一句话的话,就是:在各种页面设计下,提升有多有少,但 CL 的性能确实是最佳的! 正文详细讨论了测试方法并分析了不同情况的测量结果,有时间的读者且听我细细道来,没有的记住上一句总结即可:)

1 概述

1.1 评判 Layout 性能好坏的标准

先简单介绍一下本文用于评判 Layout 性能好坏的依据。在 Android 中,加载布局并最终将其绘制到屏幕上的过程主要包括 3 步:

  • 测量(Measure)
  • 布局(Layout)
  • 绘制(Draw)

这三个步骤都是从布局的根节点开始,自顶向下遍历视图树完成的。

就像《例说 Constraint Layout(一)》中提到的,RelativeLayout(RL)需要至少调用两次子 View 的onMeasure()方法才能完全确定布局中所有 View 的尺寸和位置,使用了 android:layout_weight 属性的 LinearLayout、使用了android:stretchColumns/android:shrinkColumns的 TableLayout,也都需要遍历两次子 View 的onMeasure()方法。随着布局层级的叠加,Measure 的耗时也呈指数型地增加。可以预期到更加扁平化的 CL 布局,其最主要的性能提升在于 Measure 阶段的速度提高,本文的测量也主要专注于测量 onMeasure 阶段的耗时。

又因为 CL 的灵活性,比起传统布局它可以省略中间不必要的一些 View 或 ViewGroup,所以其 Layout 和 Draw 速度也会有一定小幅提升[2]。结合考虑到测量的方便性,本文将 Layout 的耗时也纳入了考量。

1.2 采用的性能比较方法

下面简单介绍一下本文用到的性能测量方式。 我最开始尝试使用的是 DDMS,使用其统计得到布局耗费的 onMeasure & onLayout 时间,后来也尝试过使用 AS 新功能:Android Profiler 的 CPU Profiler 来测量,这两种方式测得的效果都不太理想,无法看出 CL 比其它布局更快速。推测下来由三个原因造成:

  1. 像 Android Profiler 这种测量工具,本来就极其消耗计算机的资源,相信小伙伴们使用的时候也发现了,打开工具后AS 界面会明显出现卡顿。所以其测量本身会对测量结果有影响,CL 同其他布局间性能上几毫秒的微妙差异,相比起使用 Android Profiler 对测量环境的人明显可感知到的影响,完全可以忽略不计;
  2. 万年非酋作者天赋技能触发,虽然根据理论选择了理应性能比较差的布局同 CL 比较,但是最后证明这种情况下 CL 的性能提升最少(平均仅比十分之一略多,下文会具体分析),看到的细微差别也就容易被我当成误差忽略了;
  3. 打开一个 Activity,统计其 Measure 和 Layout 的时间实在不是一个好方法,即使可以测量十次求平均,但这种方式统计繁琐,单词测量数据量小,效率太低了。
  • OnFrameMetricsAvailableListener

所以我最终放弃了前述的两种方式,而参照《Understanding the performance benefits of ConstraintLayout》[1]中的测量方法,使用 Android API Level 24 引入的OnFrameMetricsAvailableListener接口来比较各布局 Measure 和 Layout 的耗时之和。在代码中使用OnFrameMetricsAvailableListener的代码如下:

private val frameMetricsHandler = Handler()
private val frameMetricsAvailableListener = Window.OnFrameMetricsAvailableListener {
    _, frameMetrics, _ ->    val frameMetricsCopy = FrameMetrics(frameMetrics)    // Layout measure duration in Nano seconds
    val layoutMeasureDurationNs = frameMetricsCopy.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION)
    Log.d(TAG, "layout_Measure(ns): " + layoutMeasureDurationNs)
}
……
override fun onResume() {    
    super.onResume()
    window.addOnFrameMetricsAvailableListener(frameMetricsAvailableListener, frameMetricsHandler)
}
override fun onPause() {    
    super.onPause()
    window.removeOnFrameMetricsAvailableListener(frameMetricsAvailableListener)
}

我们的关注点在于测量/布局的性能,因此使用了FrameMetrics.LAYOUT_MEASURE_DURATION,FrameMetrics的其他可测量时长详见官方文档[4]。

加入了前述代码后,在获取到时间信息之后,就会触发frameMetricsAvailableListener()回调。

为了使结果更精确,每一个布局的一次测量都是绘制 100 次取平均的结果。即每 100 ms,切换一下根节点的 MeasureSpec(match_parent 和固定值间切换,以确保整个布局被重新测量和布局),切换 100 次后,计算平均耗时。代码如下:

private fun measureAndLayoutWrapLength(round: Int?, container: ViewGroup, w: Int, h: Int) {    val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(w, View.MeasureSpec.AT_MOST)    val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(h, View.MeasureSpec.AT_MOST)
    container.measure(widthMeasureSpec, heightMeasureSpec)
    container.layout(0, 0, container.measuredWidth, container.measuredHeight)
}
private fun measureAndLayoutExactLength(round: Int?, container: ViewGroup, w: Int, h: Int) {    val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(w, View.MeasureSpec.EXACTLY)    val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(h, View.MeasureSpec.EXACTLY)
    container.measure(widthMeasureSpec, heightMeasureSpec)
    container.layout(0, 0, container.measuredWidth, container.measuredHeight)
}
  • 环境
  1. 除了最后部分比较 CL 不同版本用到了 ConstraintLayout 1.1.3 版本,其余测量均使用 CL 1.0.2 版本;
  2. 测试机用的是小米 5;
  3. 不同的 UI 界面可能不是同一天测量的,但是同一个 UI 界面用于比较的、分别用 CL 和传统布局实现的界面必然是一起测量的,以确保机器处于相同状态;
  4. 重点重申:每一个结果都是间隔 100 ms 、测一百次的结果的平均。

2 实测

2.1 官方 Demo 页面

先来看看官方 Demo 中的页面,其 CL 和传统布局耗时的对比。布局真实的展示效果见 Fig. 1,左边为传统布局,右边为约束布局。为了和《Understanding the performance benefits of ConstraintLayout》[1]文中的结果对比,即使文中的 CL 和传统 RL 的展示效果略有不同,也没有改写布局文件使两者保持一致。而后文对比的我自己创建的布局,会尽我所能使其展示效果保持相同。

Fig. 1 性能测试用 Demo 中的传统 Layout 和 CL

上面两个布局 100 次 Measure + Layout 平均耗时对比如下:

Fig.2 Demo 中 CL 和传统 Layout 耗时对比

第一次安装完跑下来发觉 CL 性能的提升只有 21% 左右,只有《Understanding the performance benefits of ConstraintLayout》[1]文中结果 40% 的一半,因为效果不够好,又连续多测了几次,并尝试采用不同的根节点的 MeasureSpec 固定值时的尺寸(全屏和 1080*1920)。可以看到之后几次测量,传统布局和约束布局的时间都有提升,且约束布局的提升特别明显,其性能比传统布局提高了 65% 以上。所以,即使采用了 100 次取平均,手机当时的状态对测量结果还是有很大影响的,后文也尝试在不同天进行测试,结果也确实不同,我们可以做的就是用于比较的两个布局的数据,必须在同一段短时间内测量(本文所有比较数据都在一分钟以内测量完成)。Demo 的例子中,虽然手机状态有所变化,但可以肯定的是,CL 要比传统布局更快。

另,此节中页面根节点的 MeasureSpec 固定值时的尺寸不同,对结果并没有影响;而 2.4 节中,此值对结果则有一定影响。

2.2 磁贴风 LL(weight)和 CL

既然验证了《Understanding the performance benefits of ConstraintLayout》[1]一文的结果,我们回过头来看看我最开始使用 DDMS 和 Android Profiler 的 CPU Profiler 来比较,并没有得到明显性能差异的页面。这个页面是仿造 Windows 磁贴风写的,手机上显示效果如下,左边是 LL,右边是 CL:

Fig. 3 性能测试用磁贴风的传统 LL (weight) 和 CL

当初选择这个样式其实是经过思考的,根据 Android 源代码,使用了android:layout_weight属性的线性布局的子节点必须遍历两遍 Measure,理应性能比较差,且层级越深,性能越差。Fig. 4 左边展示了 LL 布局前半部分的层级结构,可以看到仅仅是前半部分,层级就很复杂了,每一层都是通过使用了android:layout_weight属性的 LL 实现的,多层嵌套,最多可以达到 7 层,即其叶节点会跑 2^6 次 OnMeasure 方法。

而 CL 则是扁平的单层结构(见 Fig. 4 右半边),使用了 Guideline 方式来实现磁贴风效果,同 LL 相比大体结构一致,仅细微处(黑边粗细)略有不同。

Fig. 4 磁贴风的传统 LL (weight) 和 CL 的层级结构

然而和预期的不太一样,CL 的性能提升并没有想象的多,平均只有 10% 左右,见下图 Fig. 5。如此小的差距,使用对手机环境影响比较大的 DDMS 或 CPU Profiler 确实会无法对性能提高和误差加以区分辨别。

Fig. 5 磁贴风 CL 和 LL (weight) 耗时对比

2.3 传统 LL (weight) 和不同写法 CL

为什么比起使用了android:layout_weight属性的、性能理应比较差的 LinearLayout(LL), CL 并没有明显的性能优势呢?不禁怀疑是不是约束布局的 Guideline 属性其实也属于比较耗时的属性,所以决定要比较一下:使用了不同 CL 属性实现的相同显示效果的 UI 界面的性能(Fig. 6),左边是使用了android:layout_weight属性的传统线性布局,右边从上之下分别是使用了:layout_constraintXXXXXX_biaslayout_constraintXXXXXX_chainStylelayout_constraintGuide_XXXXXX属性写成的效果完全一致的约束布局(由于页面的下半部分无内容,裁剪掉以节约展示空间)。

Fig. 6 性能测试用传统 LL (weight) 和不同写法 CL

Fig. 7 是测量结果,可以看到应用不同 CL 属性,其性能差别并不大:比起 LL,提升都在 40% 左右,使用 Guideline 方式甚至性能会更优秀一点。在这个比较简单的布局中,CL 的性能提升就比较明显,比 2.2 中的磁贴风要明显很多,猜测当布局明显变复杂,每一个元素的上下左右边都同其它元素相关时,CL 的性能会有一定程度的下降。

Fig. 7 ActionBar 中不同 CL 写法和 LL (weight) 耗时对比

2.4 网格风 CL 和 RL

除了混合布局(2.1 节)、线性布局(2.2 节、2.3 节),当然也想将约束布局同我们最常用、也是灵活性很高的相对布局比较一下。下图 Fig. 8 就是分别使用 RL(左)和 CL(右)实现的一个价目表页面,两者的显示效果已经完全对齐。

Fig. 8 性能测试用网格风 RL 和 CL

Fig. 9 分别比较了在不同的日子测量、根节点的 MeasureSpec 固定值使用全屏和 1080*1920 的性能,可以看到结果不尽相同,所以说两者对布局的性能确实是有影响的,但是总体说来还是那句话:CL 还是比 RL 性能要有所提升。

Fig. 9 网格风 CL 和 RL 耗时对比

2.5 不同版本 ConstraintLayout 依赖库

由于在我写《例说 Constraint Layout》系列文章以来,Google 仍然在不断优化更新 ConstraintLayout 依赖库,最后也希望简单测试一下最新版本的 CL 是否在速度方面有进一步提高,所以升级到了 1.1.3 版本的 CL 库,比较了 Chains 写法的 CL 和 Weight 写法的 LL 的性能。界面样式见 Fig. 6,测量结果见 Fig. 10。

Fig. 10 CL 1.1.3 版本 CL (chains) 和 LL (weight) 耗时对比

结果略有点出人意料:

  1. 首先,头两次测量下来,CL 的性能并不比 LL 好。(图表中只记录了第二次,第一次的数据因为我以为是自己搞错了,没有记录下来。)可见手机的运行状态对布局性能的影响还是挺大的;
  2. 排除开始的异常数据,1.1.3 版本的 CL 平均性能提升(~24%)比起 1.0.2 版本的(39%)要低(不过和 2.3 节比较,不同 CL 版本对应的 LL 的绝对时长也不相同,此处只能比较相对提升了,一定程度上会受机器当时状态影响)。

当然,上述的测量只是试水性质的,针对一个非常简单的单方向的布局的,也许在其它的界面设计下,新版的 CL 会有更优异的表现,完整的结论需要更多详细的测量才能得出。

小结

先来归纳几个点:

  1. 布局性能的测量受测试机器当时的状态、布局的设计两个因素的影响比较大,但仍旧可以很肯定地说,约束布局 CL 的性能要比传统布局(混合、相对、线性等)有提升;
  2. CL 的性能同用到的不同属性关系不大,一般说来会比传统布局提升 10% ~ 45%,常见在 30% 左右;
  3. 对于特别复杂、或某些特殊的界面,CL 的性能提升可能不那么显著,10% 左右;
  4. 不同的 ConstraintLayout 依赖版本,并不保证版本越新性能越好,至少在某个简单的界面下,1.1.3 的版本性能并不比 1.0.2 的优秀。

所以大家对安装包大小没有特别限制的话,写新的布局可以多尝试尝试约束布局 CL,毕竟在大部分情况下它都是性能最好的那一款,灵活性也足够。当然,原有的许多没有性能问题的界面,也没有必要强求改变。

参考资料

[1] Takeshi Hagikura. Understanding the performance benefits of ConstraintLayout,Aug. 2017

[2] Takeshi Hagikura. Exploring New Android Layouts,Apr. 2017

[3] Window.OnFrameMetricsAvailableListener()

[4] FrameMetrics

作者简介:opalli,天天P图 Android 工程师

文章后记:
天天P图是由腾讯公司开发的业内领先的图像处理,相机美拍的APP。欢迎扫码或搜索关注我们的微信公众号:“天天P图攻城狮”,那上面将陆续公开分享我们的技术实践,期待一起交流学习!
加入我们:
天天P图技术团队长期招聘:(1) 图像处理算法工程师,(2) Android / iOS 开发工程师,期待对我们感兴趣或者有推荐的技术牛人加入我们(base 上海)!联系方式:ttpic_dev@tencent.com

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
例说 Constraint Layout(三)—— 性能测评
在各种页面设计下,提升有多有少,但 CL 的性能确实是最佳的!
<<上一篇
下一篇>>