构建 Android App 界面时,RecyclerView 出场率很高。它的加载性能影响着用户体检。本篇分享一次完整的 RecyclerView 性能优化过程:从用工具定位问题,再不断尝试各种优化方案,最终达成 50% 的性能优化。
这次性能调优的界面如下:
界面用列表的形式,展示了一个主播排行榜。
在onCreateViewHolder()
中通过解析布局文件的方式来构建表项 item。
但解析布局文件需要进行 IO 操作将布局文件读到内存中,再解析 xml 根据标签 new 出对应的控件实例,最后 addView() 到容器中。这个过程是耗时的。
如果能使用 kotlin 代码直接完成布局的构建,则可以加速这个过程。但这样的构建代码可读性很差,后期想要更改控件的某个属性很难定位。
利用 kotlin 的DSL
来改善构建代码的可读性,甚至超越 xml:
class HeaderProxy : VarietyAdapter.Proxy<Header, HeaderViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val itemView = parent.context.run {
LinearLayout { // 构建 LinearLayout
layout_width = match_parent
layout_height = wrap_content
orientation = vertical
padding_top = 10
padding_horizontal = 10
shape = shape {
corner_radii = intArrayOf(20, 20, 20, 20, 0, 0, 0, 0)
solid_color = "#ffffff"
}
TextView { // 构建 TextView
layout_id = "tvTitle"
layout_width = wrap_content
layout_height = wrap_content
textSize = 16f
textColor = "#3F4658"
textStyle = bold
margin_bottom = 3
}
ConstraintLayout { // 构建 ConstraintLayout
layout_width = match_parent
layout_height = wrap_content
margin_top = 16
TextView { // 构建 TextView
layout_id = "tvRank"
layout_width = wrap_content
layout_height = wrap_content
textSize = 11f
textColor = "#9DA4AD"
start_toStartOf = parent_id
center_vertical = true
}
TextView { // 构建 TextView
layout_id = "tvName"
layout_width = wrap_content
layout_height = wrap_content
textSize = 11f
textColor = "#9DA4AD"
align_vertical_to = "tvRank"
start_toEndOf = "tvRank"
margin_start = 19
}
TextView { // 构建 TextView
layout_id = "tvCount"
layout_width = wrap_content
layout_height = wrap_content
textSize = 11f
textColor = "#9DA4AD"
align_vertical_to = "tvRank"
end_toEndOf = parent_id
}
}
}
}
return HeaderViewHolder(itemView)
}
override fun onBindViewHolder(holder: HeaderViewHolder, data: Header, index: Int, action: ((Any?) -> Unit)?) {
holder.tvCount?.text = data.count
holder.tvName?.text = data.name
holder.tvRank?.text = data.rank
holder.tvTitle?.text = data.title
}
}
// 表头实体类
data class Header(
val rank: String,// 排名
val name: String,// 主播
val count: String,// 粉丝数
val title: String// 表头
)
// 用于记录表项控件引用的 ViewHolder
class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val tvRank = itemView.find<TextView>("tvRank")
val tvName = itemView.find<TextView>("tvName")
val tvCount = itemView.find<TextView>("tvCount")
val tvTitle = itemView.find<TextView>("tvTitle")
}
复制代码
关于如何使用 DSL 简化布局构建可以点击Android性能优化 | 把构建布局用时缩短 20 倍(下)
将表头和表体 item 都用DSL
重构了一番,运行 demo 看看数据:
measure + layout=330, delay=47, anim=0, touch=0, draw=21, total=402
measure + layout=0, delay=357, anim=2, touch=0, draw=0, total=362
measure + layout=19, delay=4, anim=0, touch=0, draw=3, total=39
measure + layout=0, delay=16, anim=0, touch=0, draw=0, total=40
复制代码
measure + layout
时间从 370 ms 缩短到 330 ms,可喜可贺~~
内存优化:充满矛盾的SparseArray。
为PercentLayout
新增了一系列相对布局属性,这些属性的语义和ConstraintLayout
中的一样。但有两个比较特殊的:centerHorizontalOf
表示相对于某个控件水平对齐,centerVerticalOf
表示相对于某个控件垂直对齐。
这一系列相对布局属性存在互斥关系,他们分为两组,一组横向,一组纵向(详见代码注释)。一个控件只能拥有一个横向属性和一个纵向属性。getChildLeft()
和getChildTop
分别遍历所有的横向和纵向属性,根据不同的相对位置采取不同的计算方法,以确定子控件相对于父控件的 left 和 top。
然后就可以像这样构建表体 item 的布局:
PercentLayout {
layout_width = match_parent
layout_height = 35
background_color = "#ffffff"
TextView {
layout_id = "tvRank"
layout_width = 18
layout_height = wrap_content
textSize = 14f
textColor = "#9DA4AD"
left_percent = 0.08f // 相对于父控件左边的百分比
center_vertical_of_percent = parent_id //相对于父控件垂直居中
}
ImageView {
layout_id = "ivAvatar"
layout_width = 20
layout_height = 20
scaleType = scale_center_crop
center_vertical_of_percent = parent_id // 相对于父控件垂直居中
left_percent = 0.15f // 相对于父控件左边的百分比
}
TextView {
layout_id = "tvName"
layout_width = wrap_content
layout_height = wrap_content
textSize = 11f
textColor = "#3F4658"
gravity = gravity_center
maxLines = 1
includeFontPadding = false
start_to_end_of_percent = "ivAvatar" // 位于 ivAvatar 控件的右边
top_to_top_of_percent = "ivAvatar" // 与 ivAvatar 控件上边对齐
margin_start = 5
}
TextView {
layout_id = "tvTag"
layout_width = wrap_content
layout_height = wrap_content
textSize = 8f
textColor = "#ffffff"
text = "save"
gravity = gravity_center
padding_vertical = 1
includeFontPadding = false
padding_horizontal = 2
shape = shape {
corner_radius = 4
solid_color = "#8cc8c8c8"
}
start_to_start_of_percent = "tvName" // 与 tvName 控件左边对齐
top_to_bottom_of_percent = "tvName" // 在 tvName 控件的下面
}
ImageView {
layout_id = "ivLevel"
layout_width = 10
layout_height = 10
scaleType = scale_fit_xy
center_vertical_of_percent = "tvName" // 与 tvName 控件垂直对齐
start_to_end_of_percent = "tvName" // 在 tvName 控件的后面
margin_start = 5
}
TextView {
layout_id = "tvLevel"
layout_width = wrap_content
layout_height = wrap_content
textSize = 7f
textColor = "#ffffff"
gravity = gravity_center
padding_horizontal = 2
shape = shape {
gradient_colors = listOf("#FFC39E", "#FFC39E")
orientation = gradient_left_right
corner_radius = 20
}
center_vertical_of_percent = "tvName" // 与 tvName 控件垂直对齐
start_to_end_of_percent = "ivLevel" // 在 ivLevel 控件后面
margin_start = 5
}
TextView {
layout_id = "tvCount"
layout_width = wrap_content
layout_height = wrap_content
textSize = 14f
textColor = "#3F4658"
gravity = gravity_center
center_vertical_of_percent = parent_id // 相对于父控件居中
end_to_end_of_percent = parent_id // 在父控件尾部
margin_end = 20
}
}
复制代码
把 demo 跑起来,measure + layout 的耗时如下:
measure + layout=288, delay=39, anim=0, touch=0, draw=20, total=350
measure + layout=0, delay=307, anim=4, touch=0, draw=0, total=314
measure + layout=15, delay=9, anim=0, touch=0, draw=4, total=31
measure + layout=0, delay=14, anim=0, touch=0, draw=0, total=27
复制代码
measure + layout
用了 288 ms,虽然相对于FrameLayout
多了十几毫秒,但是和ConstraintLayout
的 330 ms 相比还是有不小的提升。
measure + layout
耗时从最开始的 370 ms 经过两次优化,分别是弃用 xml和替换表项根布局,缩减到 288 ms,有了 22% 的性能提升。但是离“耗时减半”还有点距离。限于篇幅原因,后续的优化详解放到下一篇继续讲解。欢迎关注我,以及时获取博客更新。
地址 代理模式解耦RecyclerView
代码地址