Recyclerview performance optimization | halving the time spent loading table entries (Part 2)

Time:2022-1-7

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:

  1. Replacing XML with dynamic layout can reduce the performance loss of IO and reflection and shorten the time-consuming of building table item layout.
  2. Replace the table item root layout by a simplerPercentLayoutreplaceConstraintLayoutTo 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 + layoutThe 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 calledGlideExecutorThread 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.”

byImageViewAdd 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 themviewScopeIt’s aCoroutineScopeObject, I declare it asViewExtended 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

viewScopeThe 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 referenceViewModelScopeFor 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 + layoutThe 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 + layoutIt 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 largerRankBeanIn 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 withLinearLayoutMedium:

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 + layoutThe 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:

  1. Replacing XML with dynamic layout can reduce the performance loss of IO and reflection and shorten the time-consuming of building table item layout.
  2. Replace the table item root layout by a simplerPercentLayoutreplaceConstraintLayoutTo shorten the measure + layout time.
  3. Use the collaborative process + glide synchronous loading method to reduce the time-consuming of loading pictures.
  4. 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

Recommended Today

Proper memory alignment in go language

problem type Part1 struct { a bool b int32 c int8 d int64 e byte } Before we start, I want you to calculatePart1What is the total occupancy size? func main() { fmt.Printf(“bool size: %d\n”, unsafe.Sizeof(bool(true))) fmt.Printf(“int32 size: %d\n”, unsafe.Sizeof(int32(0))) fmt.Printf(“int8 size: %d\n”, unsafe.Sizeof(int8(0))) fmt.Printf(“int64 size: %d\n”, unsafe.Sizeof(int64(0))) fmt.Printf(“byte size: %d\n”, unsafe.Sizeof(byte(0))) fmt.Printf(“string size: %d\n”, […]