The previous article introduced how to efficiently quantify rendering performance, and optimized the recyclerview loading speed twice, reducing the table entry loading time from 370 MS to 288 Ms. This article continues to introduce the following two optimization methods. Adding these optimizations together can achieve the effect of halving the loading time.
The performance tuning interface is as follows:
The interface displays an anchor ranking list in the form of a list.
First review the two optimizations in the previous article:
- Replacing XML with dynamic layout can reduce the performance loss of IO and reflection and shorten the time-consuming of building table item layout.
- Replace the table item root layout by a simpler
PercentLayout
replaceConstraintLayout
To shorten the measure + layout time.
For detailed explanations on these two points, clickRecyclerview performance optimization | halving the time spent loading table entries (Part 1)
Time consuming glide first asynchronous load
As shown in the above figure, the contents of two pictures of each table item come from the network, and glide is used for asynchronous loading.
I use the idea of replacing the table item root layout to the image loading: is it because glide is too complexonBindViewHolder()
Too long?
Do an experiment, load the annotated pictures, and run the demo again:
measure + layout=160, unknown delay=19, anim=0, touch=0, draw=12, total=161
measure + layout=0, unknown delay=134, anim=2, touch=0, draw=0, total=138
measure + layout=0, unknown delay=0, anim=0, touch=0, draw=0, total=3
Copy code
To my surprise,measure + layout
The time is reduced from 288 MS to 160 ms. It turns out that loading pictures has such a great impact on the list loading performance!
I am hereonBindViewHolder()
Log before and after to more intuitively detect the impact of glide loading pictures on Performance:
class RankProxy : VarietyAdapter.Proxy<Rank, RankViewHolder>() {
//Build table item
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {...}
//Binding table item data
override fun onBindViewHolder(holder: RankViewHolder, data: Rank, index: Int, action: ((Any?) -> Unit)?) {
//Start timing
val start = System.currentTimeMillis()
holder.tvCount?.text = data.count.formatNums()
//Glide loads the first picture
holder.ivAvatar?.let {
Glide.with(holder.ivAvatar.context).load(data.avatarUrl).into(it)
}
//Glide loads the second picture
holder.ivLevel?.let {
Glide.with(holder.ivLevel.context).load(data.levelUrl).into(it)
}
holder.tvRank?.text = data.rank.toString()
holder.tvName?.text = data.name
holder.tvLevel?.text = data.level.toString()
holder.tvTag?.text = data.tag
//End timing
Log.w("test", "bind view duration = ${System.currentTimeMillis() - start}")
}
}
Copy code
Run the demo, and the log is as follows:
03-20 18:22:04.243 17994 17994 W ttaylor : rank bind view duration = 41
03-20 18:22:04.252 17994 17994 W ttaylor : rank bind view duration = 2
03-20 18:22:04.261 17994 17994 W ttaylor : rank bind view duration = 2
03-20 18:22:04.270 17994 17994 W ttaylor : rank bind view duration = 1
03-20 18:22:04.279 17994 17994 W ttaylor : rank bind view duration = 1
...
Copy code
Binding the first table item in the list is particularly time-consuming! And it’s a very exaggerated 41 MS, which makes me wonder what glide did when it first started?
After a walk through the glide source code, I found thatGlide will start a program calledGlideExecutor
Thread pool to load pictures asynchronously.
The construction of thread pool is expensive and time-consuming.
Is there any way to make glide not use its own thread pool, but use the thread pool common to the whole app for loading?
The solution I think of is: “in the collaboration process, use glide’s synchronization method to load pictures.”
byImageView
Add an extension method:
fun ImageView.load(url: String) {
viewScope.launch {
val bitmap = Glide.with(context).asBitmap().load(url).submit().get()
withContext(Dispatchers.Main) { setImageBitmap(bitmap) }
}
}
Copy code
The extension method starts a coroutine and uses glide’ssubmit()
Load the picture, this method will return aFutureTarget
, call itsget()
You can get the bitmap object synchronously. Then switch to the main thread and set it to ImageView.
Among themviewScope
It’s aCoroutineScope
Object, I declare it asView
Extended properties for.
val View.viewScope: CoroutineScope
get() {
//Get existing viewscope object
val key = "ViewScope".hashCode()
var scope = getTag(key) as? CoroutineScope
//If it does not exist, create a new viewscope object
if (scope == null) {
scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
//Cache the viewscope object as the tag of the view
setTag(key,scope)
val listener = object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
}
override fun onViewDetachedFromWindow(v: View?) {
//When view detach, cancel the task of the collaboration
scope.cancel()
}
}
addOnAttachStateChangeListener(listener)
}
return scope
}
Copy code
viewScope
The semantics of this extended attribute is: “each view has a coroutinscope bound to its life cycle to start the collaboration process”. This method of dynamically extending classes and binding life cycles is a referenceViewModelScope
For detailed explanation, clickRead source code long knowledge | a new way to dynamically extend classes and bind life cycles。
Override with new extension functiononBindViewHolder()
:
class RankProxy : VarietyAdapter.Proxy<Rank, RankViewHolder>() {
override fun onBindViewHolder(holder: RankViewHolder, data: Rank, index: Int, action: ((Any?) -> Unit)?) {
holder.tvCount?.text = data.count.formatNums()
holder. ivAvatar?. Load (data. Avatarurl) // use the collaboration to load pictures
holder. ivLevel?. Load (data. Levelurl) // use the collaboration to load pictures
holder.tvRank?.text = data.rank.toString()
holder.tvName?.text = data.name
holder.tvLevel?.text = data.level.toString()
holder.tvTag?.text = data.tag
}
}
Copy code
Run the demo to see the data:
measure + layout=251, unknown delay=19, anim=0, touch=0, draw=12, total=300
measure + layout=0, unknown delay=290, anim=2, touch=0, draw=0, total=321
measure + layout=0, unknown delay=0, anim=0, touch=0, draw=0, total=3
Copy code
measure + layout
The time is reduced from 288 MS to 251 MS, which is another step towards halving.
The number of table items affects the rendering performance
BeforeA seriesIn the process of reading the recyclerview source code, many conclusions are drawn, one of which is related to the loading performance:
Filling table items is a while loop. The number of table items to be filled will be cycled as many times as possible.
The source code is as follows:
public class LinearLayoutManager {
//Populate table entries based on remaining space
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
...
//Calculate remaining space
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
//Loop, and continue to fill more table entries when the remaining space is > 0
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
...
//Populate a single table entry
layoutChunk(recycler, state, layoutState, layoutChunkResult)
...
}
}
//Populate a single table entry
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
// 1. Get the next table item view to be filled oncreateviewholder(), where onbindviewholder() is called
View view = layoutState.next(recycler);
// 2. Make the table entry a child view of recyclerview
addView(view);
...
}
}
Copy code
onCreateViewHolder()
andonBindViewHoder()
Will be called in this loop. So,The more table entries, the more time-consuming the drawing.
The experiment was carried out immediately. First, only two table items were displayed on the whole screen:
The top margin of recyclerview is increased, so that only two table items are displayed in the whole screen. Take a look at the performance log:
measure + layout=120, anim=0, touch=0, draw=1, first draw = false total=126
measure + layout=0, anim=0, touch=0, draw=0, first draw = false total=124
measure + layout=12, anim=0, touch=0, draw=0, first draw = true total=15
Copy code
measure + layout
It only took 120 ms (for how to get the performance log, click recyclerview performance optimization to halve the time spent loading table entries (Part 1))
In order to optimize the performance of loading the list for the first time, can you merge all table items on the first screen into one table item?
The list data is returned by the server, and the number is variable. If the layout is built statically with XML, the table items cannot be merged dynamically, so the table items can only be built dynamically through kotlin DSL:
class RankProxy : VarietyAdapter.Proxy<RankBean, RankViewHolder>() {
//Build header view
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val itemView = parent.context.run {
LinearLayout {// build LinearLayout
layout_id = "container"
layout_width = match_parent
layout_height = wrap_content
orientation = vertical
margin_start = 20
margin_end = 20
padding_bottom = 16
shape = shape {
corner_radius = 20
solid_color = "#ffffff"
}
Percentlayout {// build percentlayout
layout_width = match_parent
layout_height = 60
shape = shape {
corner_radii = intArrayOf(20, 20, 20, 20, 0, 0, 0, 0)
solid_color = "#ffffff"
}
Textview {// build textview
layout_id = "tvTitle"
layout_width = wrap_content
layout_height = wrap_content
textSize = 16f
textColor = "#3F4658"
textStyle = bold
top_percent = 0.23f
start_to_start_of_percent = parent_id
margin_start = 20
}
Textview {// build textview
layout_id = "tvRank"
layout_width = wrap_content
layout_height = wrap_content
textSize = 11f
textColor = "#9DA4AD"
left_percent = 0.06f
top_percent = 0.78f
}
Textview {// build textview
layout_id = "tvName"
layout_width = wrap_content
layout_height = wrap_content
textSize = 11f
textColor = "#9DA4AD"
left_percent = 0.18f
top_percent = 0.78f
}
Textview {// build textview
layout_id = "tvCount"
layout_width = wrap_content
layout_height = wrap_content
textSize = 11f
textColor = "#9DA4AD"
margin_end = 20
end_to_end_of_percent = parent_id
top_percent = 0.78f
}
}
}
}
return RankViewHolder(itemView)
}
}
//Table entry entity class
data class RankBean(
val title: String,
val rankColumn: String,
val nameColumn: String,
val countColumn: String,
Val ranges: List < rank > // all anchor information
)
//Anchor information entity class
data class Rank(
val rank: Int,
val name: String,
val count: Int,
val avatarUrl: String,
val levelUrl: String,
val level: Int ,
val tag: String
)
class RankViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val tvTitle = itemView.find<TextView>("tvTitle")
val tvRankColumn = itemView.find<TextView>("tvRank")
val tvAnchormanColumn = itemView.find<TextView>("tvName")
val tvSumColumn = itemView.find<TextView>("tvCount")
val container = itemView.find<LinearLayout>("container")
}
Copy code
Using DSL inonCreateViewHolder()
The header is dynamically built in:
The header is the static part of the list, and this part of the data does not depend on the server to return. The entire item is a verticalLinearLayout
, which makes it easy to add table entries dynamically.
The data structure must also be reconstructed to take the data returned by the serverList<Rank>
Structure package in a largerRankBean
In the structure. So that at one timeonBindViewHolder()
Get all the anchor ranking information, and then traverseList<Rank>
, build the table item view one by one and populate it withLinearLayout
Medium:
class RankProxy : VarietyAdapter.Proxy<RankBean, RankViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
//Build header and container
}
//Dynamically build table items and bind data at the same time
override fun onBindViewHolder(holder: RankViewHolder, data: RankBean, index: Int, action: ((Any?) -> Unit)?) {
holder.tvAnchormanColumn?.text = data.nameColumn
holder.tvRankColumn?.text = data.rankColumn
holder.tvSumColumn?.text = data.countColumn
holder.tvTitle?.text = data.title
holder.container?.apply {
//Traverse all anchors
data.ranks.forEachIndexed { index, rank ->
//Construct a percentlayout for each anchor data
PercentLayout {
layout_width = match_parent
layout_height = 35
background_color = "#ffffff"
Textview {// build ranking control
layout_id = "tvRank"
layout_width = 18
layout_height = wrap_content
textSize = 14f
textColor = "#9DA4AD"
left_percent = 0.08f
center_vertical_of_percent = parent_id
text = rank.rank.toString()
}
ImageView {// build avatar control
layout_id = "ivAvatar"
layout_width = 20
layout_height = 20
scaleType = scale_center_crop
center_vertical_of_percent = parent_id
left_percent = 0.15f
Glide.with(this.context).load(rank.avatarUrl).into(this)
}
Textview {// build name control
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"
top_to_top_of_percent = "ivAvatar"
margin_start = 5
ellipsize = TextUtils.TruncateAt.END
text = rank.name
}
Textview {// build label control
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"
top_to_bottom_of_percent = "tvName"
}
ImageView {// build level icon control
layout_id = "ivLevel"
layout_width = 10
layout_height = 10
scaleType = scale_fit_xy
center_vertical_of_percent = "tvName"
start_to_end_of_percent = "tvName"
margin_start = 5
Glide.with(this.context).load(rank.levelUrl).submit()
}
Textview {// build level label control
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"
start_to_end_of_percent = "ivLevel"
margin_start = 5
text = rank.level.toString()
}
Textview {// build fan count control
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
text = rank.count.formatNums()
}
}
}
}
}
}
Copy code
Run the demo to see the following data:
measure + layout=170, unknown delay=41, anim=0, touch=0, draw=18, total= 200
measure + layout=0, unknown delay=250, anim=1, touch=0, draw=0, total=289
measure + layout=4, unknown delay=4, anim=0, touch=0, draw=2, total=13
measure + layout=4, unknown delay=0, anim=0, touch=0, draw=1, total=13
Copy code
measure + layout
The time consumption is reduced from 251 MS to 170 ms, which is a huge improvement.
It can be seen that the number of table items displayed on the screen has a great impact on the list drawing performance. The more the number, the slower the drawing.
Although this method speeds up the loading of recyclerview for the first time, it also has disadvantages. It adds a new table item type to the list, and the viewholder of this table item holds too many views, which inevitably increases memory pressure. And it cannot be reused by subsequent table entries.
This method is also a method to optimize the loading speed for the demo scenario, that is, the table items that may be displayed on the first screen are merged into a new table item type. When the drop-down refresh is performed, the original table items are loaded normally one by one.
summary
After four optimizations, the first loading time of the list was shortened from 370 MS to 170 ms, with a 54% improvement. Review these four optimizations:
- Replacing XML with dynamic layout can reduce the performance loss of IO and reflection and shorten the time-consuming of building table item layout.
- Replace the table item root layout by a simpler
PercentLayout
replaceConstraintLayout
To shorten the measure + layout time. - Use the collaborative process + glide synchronous loading method to reduce the time-consuming of loading pictures.
- Merge the table items displayed on the first screen of the list into a new table item type to shorten the time of filling in table items.
Author: Tang Zixuan
Link:https://juejin.cn/post/6942276625090215943