The last article introduced how to efficiently quantify the drawing performance, and optimized the loading speed of RecyclerView for two times, which reduced the loading time of entries from 370 ms to 288 ms. This paper continues to introduce the following two optimization methods. Add these optimizations together and you can cut the load time in half.

The interface for this performance tuning is as follows:

Interface in the form of a list, showing a ranking of anchors.

Let’s review the two optimizations made in the last article:

  1. Replace XML with a dynamically built layout, which evaporates IO and reflection performance losses and reduces the time it takes to build an entry layout.
  2. Replace the entry root layout with a simplerPercentLayoutreplaceConstraintLayoutTo shorten measure + layout time.

In detail about these two points can click RecyclerView performance optimization | to halve load time table item (on)

Time consuming Glide loads asynchronously for the first time

As shown in the figure above, each entry has two images of content from the network, which is loaded asynchronously using Glide.

I used the same idea of replacing the entry root layout for image loading: is onBindViewHolder() taking too long because Glide is too complex?

To do an experiment, load the commented image 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 the code

To my surprise, Measure + Layout time was reduced from 288 ms to 160 ms. Loading images has a huge impact on list loading performance!

I typed the log before and after onBindViewHolder() to more intuitively detect the impact of Glide loading image on performance:

class RankProxy : VarietyAdapter.Proxy<Rank, RankViewHolder>() {
    // Build the entry
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {... }// Bind entry data
    override fun onBindViewHolder(holder: RankViewHolder.data: Rank, index: Int, action: ((Any?). ->Unit)? {
        // Start the timer
        valstart = System.currentTimeMillis() holder.tvCount? .text =data.count.formatNums()
        // Glide loads the first imageholder.ivAvatar? .let { Glide.with(holder.ivAvatar.context).load(data.avatarUrl).into(it)
        }
        // Glide loads the second imageholder.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 the timer
        Log.w("test"."bind view duration = ${System.currentTimeMillis() - start}")}}Copy the code

Run demo log 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 the code

Binding the first entry in the list takes a lot of time! And it’s an exaggerated 41 ms, which makes me wonder what the Glide did when it first started up.

After doing some digging around the Glide source code, I found that Glide starts a thread pool called GlideExecutor to load images asynchronously.

Thread pools are expensive and time consuming to build.

Is there any way to make Glide load using a common thread pool for the entire App instead of using its own thread pool?

The solution I came up with was: “Use Glide’s synchronous method to load images in coroutines.”

Add an extension method for ImageView:

fun ImageView.load(url: String) {
    viewScope.launch {
        val bitmap = Glide.with(context).asBitmap().load(url).submit().get()
        withContext(Dispatchers.Main) { setImageBitmap(bitmap) }
    }
}
Copy the code

The extension method starts a coroutine and loads the image using Glide’s Submit (), which returns a FutureTarget and calls its GET () to get the Bitmap object synchronously. And then switch to the main thread and set it to the ImageView.

The viewScope is a CoroutineScope object that I declare as an extended property of the View.

val View.viewScope: CoroutineScope
    get() {
        // Get the existing viewScope object
        val key = "ViewScope".hashCode()
        var scope = getTag(key) as? CoroutineScope
        // Create a viewScope object if it does not exist
        if (scope == null) {
            scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
            // Cache the viewScope object as the View's tag
            setTag(key,scope)
            val listener = object : View.OnAttachStateChangeListener {
                override fun onViewAttachedToWindow(v: View?).{}override fun onViewDetachedFromWindow(v: View?). {
                    // Detach the task of removing the coroutine while view detach
                    scope.cancel()
                }

            }
            addOnAttachStateChangeListener(listener)
        }
        return scope
    }
Copy the code

The semantics of the viewScope extension property are: “Each View has a CoroutinScope bound to its lifecycle to start the coroutine.” This dynamic extension class and bind the life cycle of writing is the reference ViewModelScope, detailed knowledge can click to read the source code long | dynamic extension class and bind the new way of the life cycle.

Rewrite onBindViewHolder() with the new extension function:

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 coroutines to load imagesholder.ivLevel? .load(data.levelUrl)// Use coroutines to load imagesholder.tvRank? .text =data.rank.toString() holder.tvName? .text =data.name
        holder.tvLevel?.text = data.level.toString() holder.tvTag? .text =data.tag
    }
}
Copy the 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 the code

Measure + Layout time was reduced from 288 ms to 251 ms, another step towards halving.

The number of entries affects the drawing performance

In the previous series of RecyclerView source code reading process, draw a lot of conclusions, one of which is related to loading performance:

Populating entries is a while loop that loops as many times as the entries need to be filled.

The source code is as follows:

public class LinearLayoutManager {
    // Fill the entry according to the remaining space
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
        ...
        // Calculate the remaining space
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        // Continue to fill more entries when the remaining space is greater than 0
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            ...
            // Populate a single entrylayoutChunk(recycler, state, layoutState, layoutChunkResult) ... }}// Populate a single entry
    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
        // 1. Get the next populated entry view, onCreateViewHolder(), where onBindViewHoder() is called
        View view = layoutState.next(recycler);
        // 2. Make the entry a child of RecyclerViewaddView(view); . }}Copy the code

Both onCreateViewHolder() and onBindViewHoder() are called in this loop. Therefore, the more entries, the more time it takes to draw.

First, let the entire screen display only two entries:

Increased the top distance of RecyclerView, so that the whole screen only shows 2 entries, 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 the code

Measure + layout only took 120 ms (about how to obtain performance log can click RecyclerView performance optimization | to halve load time table item (on))

To optimize the performance of the first load list, can you merge all the entries on the first screen into one entry?

List data is returned by the server in a variable amount. If you build a layout statically using XML, you can’t merge the table entries dynamically, so you can only build the table entries dynamically using Kotlin DSL:

class RankProxy : VarietyAdapter.Proxy<RankBean, RankViewHolder>() {
    // Build the 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.23 f 
                        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.06 f 
                        top_percent = 0.78 f 
                    }

                    TextView { / / build TextView
                        layout_id = "tvName"
                        layout_width = wrap_content
                        layout_height = wrap_content
                        textSize = 11f
                        textColor = "#9DA4AD"
                        left_percent = 0.18 f 
                        top_percent = 0.78 f 
                    }

                    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.78 f}}}}return RankViewHolder(itemView)
    }
}

// Entry entity class
data class RankBean(
    val title: String,
    val rankColumn: String,
    val nameColumn: String,
    val countColumn: String,
    val ranks: 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 the code

The table header is dynamically built in onCreateViewHolder() using DSL:

The header is the static part of the list that is not returned by the server. The entire item is verticalLinearLayout, which provides convenience for dynamically adding entries vertically.

The data structure also needs to be refactored to pack the List

structure returned by the server into a larger RankBean structure. To get all the anchor ranking information in a single onBindViewHolder(), then iterate through the List

to build the entry view one by one and fill it into the LinearLayout:

class RankProxy : VarietyAdapter.Proxy<RankBean, RankViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        // Build the header and container
    }
    
    // Dynamically build entries and bind data simultaneously
    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 {// Walk through all anchors
            data.ranks.forEachIndexed { index, rank ->
                // Build a PercentLayout for each anchor data
                PercentLayout {
                    layout_width = match_parent
                    layout_height = 35
                    background_color = "#ffffff"

                    TextView { // Build the ranking control
                        layout_id = "tvRank"
                        layout_width = 18
                        layout_height = wrap_content
                        textSize = 14f
                        textColor = "#9DA4AD"
                        left_percent = 0.08 f
                        center_vertical_of_percent = parent_id
                        text = rank.rank.toString()
                    }

                    ImageView { // Build the avatar control
                        layout_id = "ivAvatar"
                        layout_width = 20
                        layout_height = 20
                        scaleType = scale_center_crop
                        center_vertical_of_percent = parent_id
                        left_percent = 0.15 f
                        Glide.with(this.context).load(rank.avatarUrl).into(this)
                    }

                    TextView { // Build the 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 the 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 the rank 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 the rank 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 the fan
                        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 the code

Run demo to see the 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 the code

The time-consuming of Measure + layout has been reduced from 251 ms to 170 ms, which is a huge improvement.

It can be seen that the number of entries displayed on the screen has a great impact on the drawing performance of the list. The more entries there are, the slower the drawing will be.

Although this approach, let the first load RecyclerView speed up a lot, but there are also disadvantages. It adds a new entry type to the list, and the ViewHolder of this entry holds too many views to stress memory. And it cannot be reused by subsequent entries.

This method is also a way to optimize the loading speed for scenarios such as Demo. That is, all the entries that may be displayed on the first screen are combined with a new entry type. In the drop-down refresh, the original entries are normally loaded one by one.

conclusion

After 4 times of optimization, the first loading time of the list is shortened from 370 ms to 170 ms, which is 54% improvement. Review these four optimizations:

  1. Replace XML with a dynamically built layout, which evaporates IO and reflection performance losses and reduces the time it takes to build an entry layout.
  2. Replace the entry root layout with a simplerPercentLayoutreplaceConstraintLayoutTo shorten measure + layout time.
  3. Use coroutine + Glide synchronous loading method to reduce the loading time of pictures.
  4. Merge the entries displayed on the first screen of the list into a new entry type to shorten the entry filling time.

In fact, I have some more bold ideas, which will be introduced in the next blog. Please follow me and get updated on the blog

Talk is cheap, show me the code

Recommended reading

RecyclerView series article directory is as follows:

  1. RecyclerView caching mechanism | how to reuse table?

  2. What RecyclerView caching mechanism | recycling?

  3. RecyclerView caching mechanism | recycling where?

  4. RecyclerView caching mechanism | scrap the view of life cycle

  5. Read the source code long knowledge better RecyclerView | click listener

  6. Proxy mode application | every time for the new type RecyclerView is crazy

  7. Better RecyclerView table sub control click listener

  8. More efficient refresh RecyclerView | DiffUtil secondary packaging

  9. Change an idea, super simple RecyclerView preloading

  10. RecyclerView animation principle | change the posture to see the source code (pre – layout)

  11. RecyclerView animation principle | pre – layout, post – the relationship between the layout and scrap the cache

  12. RecyclerView animation principle | how to store and use animation attribute values?

  13. RecyclerView list of interview questions | scroll, how the list items are filled or recycled?

  14. RecyclerView interview question | what item in the table below is recycled to the cache pool?

  15. RecyclerView performance optimization | to halve load time table item (a)

  16. RecyclerView performance optimization | to halve load time table item (2)

  17. RecyclerView performance optimization | to halve load time table item (3)

  18. How does RecyclerView roll? (a) | unlock reading source new posture

  19. RecyclerView how to achieve the scrolling? (2) | Fling

  20. RecyclerView Refresh list data notifyDataSetChanged() why is it expensive?