Szhangbiao's blog

记录一些让自己可以回忆的东西

0%

解决使用ConstraintLayout不当而引起的卡顿

优化页面渲染速度的一个方向是简化布局减少嵌套ConstraintLayout的特性很适合处理这方面的优化,一直以来我也是尽可能的在各种场景下使用ConstraintLayout,保证Fragment页面或者 RecyclerViewViewHolder里的itemView中只有一个布局,但是最近在适配新的开发板设备过程中,个别页面打开速度出现比较严重的卡顿并在logcat里打印了ANR Warning日志,从而让我定位到了卡顿的元凶是ConstraintLayoutonLayoutonMeasure方法,这篇文章就是记录该问题出现和解决的过程。

背景

我目前所开发的项目在某种程度上来说也算得上是大屏TV类的Android开发,特点就是一些特殊页面控件元素众多,为了防止这些View元素出现布局嵌套整个页面就一个ConstraintLayout,开发完后的感受就是打开页面时会在视觉上有滞后感,也就是UI卡顿。不过这个问题当时并没有成为严重到APP不能使用的地步,前段时间也专门抽出一些时间做专项优化治理,优化手段包括代码代替xml创建布局、优化列表数据加载、View缓存复用和异步加载等等,直接体现就是Fragment的各个生命周期执行时间得到明显的缩短。

1
2
3
4
5
6

Fragment XXXFragment Total duration from onFragmentPreCreated to onFragmentCreated: 4ms
Fragment XXXFragment Total duration from onFragmentCreated to onFragmentViewCreated: 78ms
Fragment XXXFragment Total duration from onFragmentViewCreated to onFragmentStarted: 4ms
Fragment XXXFragment Total duration from onFragmentStarted to onFragmentResumed: 46ms

卡顿问题有所缓解却没有得到根治,直到最近在适配Android 5.1系统的新设备,卡顿问题在该设备上被放大且伴有logcatANR Warning日志,这让我发现了新的优化方向,让我比较意外的是ConstraintLayout这一我在任何开发场景都比较推崇使用的布局在一些场景下也有局限性,物极必反的道理也在其中有很好的体现。

问题分析及解决

目前出现ANR Warning日志的页面主要有两例,下面就来介绍详细情况跟相关的日志。

1. onMeasure方法卡顿

该场景下发生卡顿的原因是ConstraintLayout下的直接子View众多,减少嵌套这种优化在我看来最好是没有嵌套,页面中所有的元素都在一个Layout中,当页面展示动态数据时就会有以下日志打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[ANR Warning]onMeasure time too long, this =androidx.constraintlayout.widget.ConstraintLayout{5e6add4 V.E..... ......I. 0,0-0,0}time =1437 ms
[ANR Warning]onMeasure time too long, this =androidx.fragment.app.FragmentContainerView{eaa1c88 V.E..... ......ID 0,0-1280,800 #7f090217 app:id/nav_host_fragment}time =1438 ms
[ANR Warning]onMeasure time too long, this =androidx.fragment.app.FragmentContainerView{252346b2 V.E..... ......ID 0,0-1280,800 #7f090217 app:id/nav_host_fragment}time =1438 ms
[ANR Warning]onMeasure time too long, this =androidx.appcompat.widget.ContentFrameLayout{384e6e26 V.E..... ......ID 0,0-1280,800 #1020002 android:id/content}time =1438 ms
[ANR Warning]onMeasure time too long, this =androidx.appcompat.widget.FitWindowsLinearLayout{21c3908b V.E..... ......ID 0,0-1280,800 #7f090038 app:id/action_bar_root}time =1438 ms
[ANR Warning]onMeasure time too long, this =android.widget.FrameLayout{1311434e V.E..... ......ID 0,0-1280,800}time =1439 ms
[ANR Warning]onMeasure time too long, this =android.widget.LinearLayout{2ee80c50 V.E..... ......ID 0,0-1280,800}time =1439 ms
[ANR Warning]onMeasure time too long, this =com.android.internal.policy.impl.PhoneWindow$DecorView{363775e4 V.E..... R.....ID 0,0-1280,800}time =1440 ms
[ANR Warning]onMeasure time too long, this =androidx.constraintlayout.widget.ConstraintLayout{5e6add4 V.E..... ......I. 0,0-0,0}time =1333 ms
[ANR Warning]onMeasure time too long, this =androidx.fragment.app.FragmentContainerView{eaa1c88 V.E..... ......ID 0,0-1280,800 #7f090217 app:id/nav_host_fragment}time =1334 ms
[ANR Warning]onMeasure time too long, this =androidx.fragment.app.FragmentContainerView{252346b2 V.E..... ......ID 0,0-1280,800 #7f090217 app:id/nav_host_fragment}time =1334 ms
[ANR Warning]onMeasure time too long, this =androidx.appcompat.widget.ContentFrameLayout{384e6e26 V.E..... ......ID 0,0-1280,800 #1020002 android:id/content}time =1334 ms
[ANR Warning]onMeasure time too long, this =androidx.appcompat.widget.FitWindowsLinearLayout{21c3908b V.E..... ......ID 0,0-1280,800 #7f090038 app:id/action_bar_root}time =1334 ms
[ANR Warning]onMeasure time too long, this =android.widget.FrameLayout{1311434e V.E..... ......ID 0,0-1280,800}time =1334 ms
[ANR Warning]onMeasure time too long, this =android.widget.LinearLayout{2ee80c50 V.E..... ......ID 0,0-1280,800}time =1334 ms
[ANR Warning]onMeasure time too long, this =com.android.internal.policy.impl.PhoneWindow$DecorView{363775e4 V.E..... R.....ID 0,0-1280,800}time =1334 ms

原理大概是ConstraintLayout下的TextViewtext变动或者一些Viewvisibility属性变动会导致ConstraintLayout重新测量,导致onMeasure方法方法多次执行从而导致卡顿,解决方法也比较简单,就是数据变动涉及到的控件使用一个父布局做一层嵌套,这样个别控件的宽高变动不会导致整个布局重新测量,从而避免onMeasure方法的卡顿。ConstraintLayout的出现是为了减少嵌套而不是避免嵌套,在适当的场景下增加一层嵌套反而能提高页面加载的效率。

当然这种增加嵌套的方式比较适合当前我这种childView众多的卡顿的场景,也有通过自定义ConstraintLayout中重写onMeasure方法来减少不必要的测量优化方式,还是要具体问题具体分析。

2. onLayout方法卡顿

这个页面的场景是应用的主页,页面采用TabLayout+ViewPager+Fragments的结构,是一种比较典型的TV类布局结构,原型图大致如下:

image

Fragment里使用RecyclerView展示页面数据,部分页面加载中状态时使用Fake数据填充RecyclerView来达到预览的效果,由于ViewPager预加载的处理会导致页面打开时会有三个Fragment同时加载,这样的场景下ViewPageronLayout方法就耗时比较严重,从而导致onLayout方法卡顿。当然也有一部分原因是我在ViewHolder里的itemView里使用了ConstraintLayout来做布局。

1
2
3
4
5
6
7
8
9
[ANR Warning]onLayout time too long, this =androidx.viewpager.widget.ViewPager{30c079ac VFED.... ......ID 0,100-1024,600 #7f090364 app:id/vp_container}time =441 ms
[ANR Warning]onLayout time too long, this =androidx.constraintlayout.widget.ConstraintLayout{10fd8623 V.E..... ......ID 0,0-1024,600}time =442 ms
[ANR Warning]onLayout time too long, this =androidx.fragment.app.FragmentContainerView{2aeadbb3 V.E..... ......ID 0,0-1024,600 #7f09021a app:id/nav_host_fragment}time =442 ms
[ANR Warning]onLayout time too long, this =androidx.fragment.app.FragmentContainerView{1a980a6f V.E..... ......ID 0,0-1024,600 #7f09021a app:id/nav_host_fragment}time =443 ms
[ANR Warning]onLayout time too long, this =androidx.appcompat.widget.ContentFrameLayout{53f8013 V.E..... ......ID 0,0-1024,600 #1020002 android:id/content}time =443 ms
[ANR Warning]onLayout time too long, this =androidx.appcompat.widget.FitWindowsLinearLayout{1c458be4 V.E..... ......ID 0,0-1024,600 #7f090038 app:id/action_bar_root}time =443 ms
[ANR Warning]onLayout time too long, this =android.widget.FrameLayout{1b26438 V.E..... ......ID 0,0-1024,600}time =443 ms
[ANR Warning]onLayout time too long, this =android.widget.LinearLayout{385d24aa V.E..... ......ID 0,0-1024,600}time =443 ms
[ANR Warning]onLayout time too long, this =com.android.internal.policy.impl.PhoneWindow$DecorView{e93dc9e V.E..... R.....ID 0,0-1024,600}time =443 ms

这个场景下的优化思路就比较明显了,主因是ViewPager下三个Fragment同时加载造成的同一时间需要onLayoutView太多,次因则实在ViewHolder中使用了ConstraintLayoutFragment层面则是采用非当前显示的Fragment采用延迟加载的策略,ViewHolder层面则是替换掉ConstraintLayout的使用,使用FrameLayoutLinearLayoutRelativeLayout代替。

之所以确定在ViewHolder中使用ConstraintLayout会造成onLayout耗时严重的问题,是因为在切换到仅使用一个图片来表示加载中的状态的页面时也会打印上述ANR Warning日志,替换成其它普通布局则没有问题。

总结

这次卡顿优化的过程再次验证了我在Android高手开发课中看到的关于性能优化的总结的一句话的含金量,就是性能优化的终点就是监控,性能的瓶颈在于如何被发现,就像我们日常解决Bug一样,只要有Bug的重现路径,解决Bug反而是其中比较简单的一环,对于我而言,在性能优化的道路上还有很长的一段路要走。