应用UI卡顿常见原因主要在以下几个方面:
人为在UI线程中做轻微耗时操作,导致UI线程卡顿;
布局Layout过于复杂,无法在16ms内完成渲染;
同一时间动画执行的次数过多,导致CPU或GPU负载过重;
View过度绘制,导致某些像素在同一帧时间内被绘制多次,从而使CPU或GPU负载过重;
View频繁的触发measure、layout,导致measure、layout累计耗时过多及整个View频繁的重新渲染;
内存频繁触发GC过多(同一帧中频繁创建内存),导致暂时阻塞渲染操作;
冗余资源及逻辑等导致加载和执行缓慢;
发现和定位问题,及解决方案
打开GPU绘制,手指在卡片上来回滑来滑去,通过观察高线的位置判断卡顿的时机,我们发现滑动停止后再一次滑动时出现高峰,如图,按照经验ViewPager的卡顿问题在于滑动事件的回调,重点排查onPageScrolled
,onPageSelected
及Adapter
的instantiateItem
方法。
为了更精确的定位耗时方法,可以使用TraceView。打开DDMS,进入下面界面
点击红点位置开始录制,滑动手机屏幕,一段时间后再点击红点停止录制。分析下面的图
可以看到耗时最多的两个方法,RoundAngleImageView的draw方法和CardAdapter的instantiateItem方法。我们通过打log也应证了TraceView的判断,ViewPager滑动过程中RoundAngleImageView会不断绘制,在滑动停顿后又开始滑动得时候出现耗时峰值。优化的方案就是用RoundedImageView替换掉自定义的RoundAngleImageView,RoundedImageView使用BitmapShader方式优于RoundAngleImageView使用的PorterDuffXfermode圆角方案。特别在不断绘制的情况下,效果较明显。
接下来分析CardAdapter的instantiateItem方法,同样先看log,定位更细的方法
通过日志看出val view = LayoutInflater.from(container.context).inflate(R.layout.``item_lesson_card``, container, false) as LessonCardView
和initLessonCardView(position, view, cardList)
两个方法。
inflate方法可做的优化就是减少View层级,把最外层的RelativeLayout替换成LinearLayout。另外一个优化就是把 initLessonCardView
方法加入延时,对于当前显示的item延时为0,不可见的item延时时间与当前View的位置步距依次延长。通过分步延时避免UI线程一直被占用。
下面来跟踪initLessonCardView
方法
通过日志发现耗时最大的在于View的构造函数。
val exerciseItemView: ExerciseItemView = ExerciseItemView(context)
尝试把View的创建方法改成inflate
方式,事实证明然并卵。
val exerciseItemView: ExerciseItemView = View.inflate(context, R.layout.view_exercise_item, null) as ExerciseItemView
优化后的图
左边是优化后的图,右边是某竞品的图,比较下很明显看出渲染的差异来
总结
优化到此为止,总结一下View的优化套路。首先发现问题,通过GPU柱状图判断卡顿程度。然后通过TraceView定位卡顿的方法,打log方式找到更具体的耗时细节,然后逐个优化。
本次ViewPager的优化包括:
RoundedImageView
控件的选择,选择更高效的圆角方案优化view的层次结构,尽量减少层级,同样层级情况使用
LinearLayout
优于RelativeLayout
adapter instantiateItem
分布延迟渲染
Tips
ViewPager里包含ListView时,layout_height
属性为wrap_content
会导致adapter getView
不断被调用,建议改成match_content
<ListView android:layout_width="match_parent" android:layout_height="wrap_content" />