When building Android App interface, RecyclerView has a high appearance rate. Its loading performance affects user health checks. This article shares a complete RecyclerView performance optimization process: from using tools to locate problems, and then constantly try various optimization schemes, and finally achieve 50% performance optimization.

The interface for this performance tuning is as follows:

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

Pre-optimization, quantization first

The leaderboard is nested in a ViewPager. The performance problem was initially discovered because the pan animation of the ViewPager indicator froze when sliding onto the screen, dropping frames.

Although the lag is visible to the naked eye, it is impossible to quantify the degree of optimization without quantifying the lag.

The first tool that comes to mind is GPU rendering pattern analysis. The path to open it is as follows: Open phone Settings — Developer options — GPU rendering mode analysis — display as a bar chart on screen:

After this function is enabled, the drawing performance will be graphically displayed as follows:

Sure enough, there was a big performance problem, and the pillar was going off the screen.

Although the graphics are very intuitive, but the quantitative is not detailed enough, the best drawing time can be accurate to milliseconds. So switch to another way “in ADB shell Dumpsys gfxinfo”. Adb shell dumpsys gfxinfo < package name >

        Draw    Prepare Process Execute
        50.00   0.23    6.82    1.28
        50.00   0.26    1.49    1.13
        7.01    0.24    1.58    0.76
        6.41    0.52    7.42    1.34
        13.13   0.18    2.01    0.76
        4.38    0.15    1.72    0.39
        4.37    0.15    1.13    0.33
        4.36    0.15    1.23    0.38
        4.34    0.35    1.15    0.31
        4.36    0.15    1.16    8.42
        4.32    0.14    1.11    0.31
        4.32    0.15    1.10    0.32
Copy the code

Each line represents the time spent at each stage of a frame’s rendering.

More accurate data can be obtained with another command: ADB shell dumpsys gfxinfo < package name > framestats, which outputs the time of frames with nanosecond timestamps from the last 120 frames generated by the application:

Flags,IntendedVsync,Vsync,OldestInputEvent,NewestInputEvent,HandleInputStart,AnimationStart,PerformTraversalsStart,DrawS tart,SyncQueued,SyncStart,IssueDrawCommandsStart,SwapBuffers,FrameCompleted,DequeueBufferDuration,QueueBufferDuration, 1299731248990299732582322922372368477807,0,299873227699771,299873227750761,299873228134563,299873242278000, 299873243236959299732343011299732348063299732451375299732550396620 00670 000, 0299732234485299732901151922372368477807,0,299873253133625,299873253191177,299873253443990,299873418812375, 299873433404406299734375313299734402167299734509667299734595448710 00453 000, 0299734831229299734831229922372368477807,0,299873448760344,299873448798573,299873449290656,299873449438469, 299873449500969299734973261299734990979299734077344299734147625650 00264 000, 0299734492749299734492749922372368477807,0,299873465493625,299873465550292,299873466377896,299873466594511, 299873466643417299734693115299734881427299734597792299734685011145 000198 000, 0299734153390299734153390922372368477807,0,299873481932896,299873481972688,299873482590188,299873482741333, 299873482772688299734304375299734326886299734385958299734430886720, 00960, 00, 0299734815975299734815975922372368477807,0,299873498582427,299873498633417,299873499218833,299873499442271, 299873499483781299734985761299735032427299735149886299735217708126 000186 000, 0299735476506299735476506922372368477807,0,299873515260031,299873515314667,299873515966646,299873516205656, 299873516254667299735653052299735689229299735804063299735865990141 000149 000, 0299735137920299735137920922372368477807,0,299873531891646,299873531951906,299873532798261,299873533022792, 299873533072219299735339292299735374886299735481729299735544771118 000160 000, 0299735798162299735798162922372368477807,0,299873548571750,299873548638990,299873549306594,299873549534042, 299873549590969299735991958299735034167299735185063299735261886128 000200 000, 0299735460513299735460513922372368477807,0,299873565070500,299873565126281,299873565700031,299873565924302, 299873565977479299735628073299735663865299735789375299735861386143 000167 000, 0299735121965299735121965922372368477807,0,299873581577948,299873581621073,299873583047271,299873583253833, 299873583293729299735368031299735485875299735237136299735352688222 000308 000,Copy the code

The native output information is not readable, but it follows the CSV format, copy and paste it into the WPS table, select the data – sorted, separated by “comma” :

The data is presented as a table:

Each row represents the time when a frame is drawn. There are 16 columns, and each column represents the timestamp of a key node. For example, PerformTraversalsStart indicates the start time of the traversal and DrawStart indicates the start time of onDraw(). The former minus the latter indicates the time taken by Measure + Layout.

Using the differential function of the table, a row of time taken to characterize performance can be calculated.

We’ve got quantitative data, but it’s a bit of a grind.

After some searching, I finally found the following efficient method:

class TestActivity : BaseActivity() {
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState) window? .addOnFrameMetricsAvailableListener(Window.OnFrameMetricsAvailableListener { window, frameMetrics, dropCountSinceLastInvocation -> Log.v("test"."measure + layout=${frameMetrics.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION)/1000000}," +
                   "    delay=${frameMetrics.getMetric(FrameMetrics.UNKNOWN_DELAY_DURATION)/1000000}," +
                   "    anim=${frameMetrics.getMetric(FrameMetrics.ANIMATION_DURATION)/1000000}," +
                   "    touch=${frameMetrics.getMetric(FrameMetrics.INPUT_HANDLING_DURATION)/1000000}," +
                   "    draw=${frameMetrics.getMetric(FrameMetrics.DRAW_DURATION)/1000000}," +
                   "    total=${frameMetrics.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION)/1000000}")
        }, Handler())
    }
}
Copy the code

Window. AddOnFrameMetricsAvailableListener () method can monitor the recent 120 frames of rendering time consuming, its data source and the adb commands above is the same.

I printed all the time I was interested in, including Measure + layout, delay, animation, touch, drawing and total time.

Then open the leaderboard interface and get the following data:

measure + layout=370,     delay=35,     anim=0,    touch=0,     draw=21,     total=435
measure + layout=0,     delay=451,     anim=3,    touch=0,     draw=0,     total=467
measure + layout=22,     delay=6,     anim=0,    touch=0,     draw=3,     total=34
measure + layout=0,     delay=17,     anim=0,    touch=0,     draw=0,     total=41
Copy the code

One frame takes up to 435 ms to draw, of which measure + Layout takes up 370 ms. (This value varies greatly among different mobile phones)

Then I turned off log filtering and found more information:

measure + layout=370,     delay=35,     anim=0,    touch=0,     draw=21,     total=435
Skipped 23 frames!  The application may be doing too much work on its main thread.
measure + layout=0,     delay=451,     anim=3,    touch=0,     draw=0,     total=467
measure + layout=22,     delay=6,     anim=0,    touch=0,     draw=3,     total=34
measure + layout=0,     delay=17,     anim=0,    touch=0,     draw=0,     total=41
Copy the code

Next to the frame that takes the longest, there is a warning, printed by Choreographer, that frames have dropped at this moment, and for a full 23 frames… (about the Choreographer detailed source code parsing can click to read the source code for long knowledge | Android caton true because “frame”?

Build the layout dynamically and discard XML

One of the first solutions that comes to mind is: “Ditch XML.”

OnCreateViewHolder () is executed on the main thread, and if it takes time to execute, it will affect the drawing performance of the main thread as well.

In Demo, there are two types of leaderboards: table header and table body. The code to build the table header layout is as follows:

// The header item proxy class describes how to build an item and bind data to it
class HeaderProxy:VarietyAdapter.Proxy<Header, HeaderViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        / / build the item
        val itemView = LayoutInflater.from(parent.context).inflate(R.layout.header_layout,parent,false)
        return HeaderProxy(itemView)
    }

    override fun onBindViewHolder(holder: HeaderViewHolder.data: Header, index: Int, action: ((Any?). ->Unit)? {
        // Bind data to itemholder.tvCount? .text =data.count holder.tvName? .text =data.name
        holder.tvRank?.text = data.rank holder.tvTitle? .text =data.title
    }
}

// Table header entity class
data class Header(
    val rank: String,/ / no
    val name: String,/ / the host
    val count: String,/ / number of fans
    val title: String/ / headers
)
// The ViewHolder used to hold the reference to the entry control
class HeaderViewHolder(itemView:View):RecyclerView.ViewHolder(itemView){
    val tvCount = itemView.findViewById<TextView>(R.id.tvCount)
    val tvName = itemView.findViewById<TextView>(R.id.tvName)
    val tvRank = itemView.findViewById<TextView>(R.id.tvRank)
    val tvTitle = itemView.findViewById<TextView>(R.id.tvtitle)
}
Copy the code

This logic should have been written in recyclerView. Adapter and abstracted into a proxy class for decoupling, making it easier to add different types of entries to the list:

private val adapter = VarietyAdapter().apply {
    addProxy(HeaderProxy())
    addProxy(RankProxy())
}
Copy the code

Call addProxy () can dynamically add a new table item type (of proxy mode of practical application can click on the proxy pattern application | every time for the new type RecyclerView is crazy).

OnCreateViewHolder () builds the item by parsing the layout file.

But parsing the layout file requires IO operations to read the layout file into memory, then parsing the XML to generate the corresponding control instance according to the tag new, and finally addView() into the container. This process is time consuming.

If you can build the layout directly using kotlin code, you can speed up the process. However, such build code is very unreadable, and it is difficult to locate a control property that you want to change later.

Use Kotlin’s DSL to improve the readability of build code, even beyond XML:

class HeaderProxy : VarietyAdapter.Proxy<Header, HeaderViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val itemView = parent.context.run {
            LinearLayout { / / build 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 { / / build TextView
                    layout_id = "tvTitle"
                    layout_width = wrap_content
                    layout_height = wrap_content
                    textSize = 16f
                    textColor = "#3F4658"
                    textStyle = bold
                    margin_bottom = 3
                }

                ConstraintLayout { / / build ConstraintLayout
                    layout_width = match_parent
                    layout_height = wrap_content
                    margin_top = 16
                    
                    TextView { / / build TextView
                        layout_id = "tvRank"
                        layout_width = wrap_content
                        layout_height = wrap_content
                        textSize = 11f
                        textColor = "#9DA4AD"
                        start_toStartOf = parent_id
                        center_vertical = true
                    }
                    
                    TextView { / / build 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 { / / build 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
    }
}

// Table header entity class
data class Header(
    val rank: String,/ / no
    val name: String,/ / the host
    val count: String,/ / number of fans
    val title: String/ / headers
)

// The ViewHolder used to record the reference to the entry control
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")}Copy the code

On how to use the DSL simplify building layout can click on the Android performance optimization | to shorten the building layout is 20 times (below)

Table header and table body item are reconstructed using DSL. Run demo to see the data:

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
Copy the code

Measure + Layout time has been shortened from 370 ms to 330 ms

Different ViewGroups require different Measure + layout time

The second optimization that comes to mind is: “Replace the root layout of the entry”

ConstraintLayout is used for both the header and the header item layout. Is measure + Layout too complex to interpret?

Incredulous, I replaced all ConstraintLayout with FrameLayout, and the interface looked like this:

With all the child controls gathered together, take another look at the performance log:

measure + layout=272,     delay=40,     anim=0,    touch=0,     draw=15,     total=312
measure + layout=0,     delay=300,     anim=2,    touch=0,     draw=0,     total=332
measure + layout=9,     delay=7,     anim=0,    touch=0,     draw=2,     total=21
measure + layout=7,     delay=4,     anim=0,    touch=0,     draw=1,     total=55
measure + layout=0,     delay=0,     anim=0,    touch=0,     draw=0,     total=41
Copy the code

Surprisingly, Measure + Layout time was shortened from 330 ms to 272 ms.

It seems that the complexity of the table root layout can affect the loading performance of the list, and the list will magnify the performance gap, because n entries will be measured + layout n times

Use the simplest FrameLayout layout. Position each child control in the entry by leftMargin and topMargin. I read the left and top margins of each child control relative to the parent control against the UI design, and then rewrite the header item with FrameLayout.

However, when I ran the demo on different phones, I found defects in this scheme. Although DP was used instead of pixel value, it still failed to solve the problem of multi-screen adaptation:

“Number of fans” according to the left and top distance to determine the position relative to the parent control, different phone screen width is different, so the adaptation effect is very poor.

Maybe that’s why relative layouts exist, but RelativeLayout isn’t exactly a fuel-efficient lamp. Is there an easier way?

I came up with a percentage layout, again based on the left and top margins, but instead of using the dp value, use the percentage relative to the parent control. Wouldn’t that solve the problem perfectly?

Immediately search PercentFrameLayout sadly deprecated…

Write one yourself:

// Custom percentage layout
class PercentLayout
@JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0) 
: ViewGroup(context, attrs, defStyleAttr, defStyleRes) {

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // Measure all child controls
        measureChildren(widthMeasureSpec, heightMeasureSpec)
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        // The width of the parent control
        val parentWidth = right - left
        // The parent control is high
        val parentHeight = bottom - top
        // Walk through the child controls to locate them one by one
        (0 until childCount).map { getChildAt(it) }.forEach { child ->
            val lp = child.layoutParams as LayoutParam
            // Calculate the child control left value (relative to the parent control width)
            val childLeft = (parentWidth * lp.leftPercent).toInt()
            // Calculate the top value of the child control (relative to the parent control)
            val childTop = (parentHeight * lp.topPercent).toInt()
            // Locate the child control
            child.layout(childLeft, childTop, childLeft + child.measuredWidth, childTop + child.measuredHeight)
        }
    }

    // Custom layout parameters with two new attributes: left percentage and top percentage
    class LayoutParam(source: ViewGroup.LayoutParams?) : ViewGroup.LayoutParams(source) {
        var leftPercent: Float = 0f
        var topPercent: Float = 0f}}Copy the code

The coding of the percentage layout is simple in two steps: measure all child controls, and then position all child controls as needed. Including measuring children using ViewGroup. MeasureChildren () is done. The layout child calculates the width and height of the parent control, multiplies the percentage of the child control to get its position relative to the parent control, and calls View.Layout () to locate the child control.

Run the demo, the effect is ideal ~~

Use the same idea to reconstruct the table body item. One problem was discovered: Not all controls can be laid out relative to the parent control.

Take this scenario:

Table data is returned by the server, the length of the text is variable, “such as misty rain, is not to” behind the picture with its vertical alignment can not be relative to the parent control layout.

So PrecentLayout has to introduce the concept of relative layout, but it doesn’t need to be as complex as ConstraintLayout. A simplified version of the relative layout would look like this:

// Customize the percentage relative layout
class PercentLayout
@JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0) 
: ViewGroup(context, attrs, defStyleAttr, defStyleRes) {
    // Record the map of all child controls and their ids
    private val childMap = SparseArray<View>()

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // Measure all child controls
        measureChildren(widthMeasureSpec, heightMeasureSpec)
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        val parentWidth = right - left
        val parentHeight = bottom - top
        (0 until childCount).map { getChildAt(it) }.forEach { child ->
            val lp = child.layoutParams as LayoutParam
            // Calculates the left value of the child control
            val childLeft = getChildLeft(lp, parentWidth, child)
            // Calculate the top value of the child control
            val childTop = getChildTop(lp, parentHeight, child)
            // Layout child controls
            child.layout(childLeft, childTop, childLeft + child.measuredWidth, childTop + child.measuredHeight)
        }
    }

    // Calculate the distance above the parent control according to different circumstances
    private fun getChildTop(lp: LayoutParam, parentHeight: Int, child: View): Int {
        val parentId = parent_id.toLayoutId()
        return when{ lp.topPercent ! = -1f-> (parentHeight * lp.topPercent).toInt() lp.centerVerticalOf ! = -1- > {if (lp.centerVerticalOf == parentId) {
                    (parentHeight - child.measuredHeight) / 2
                } else {
                    (childMap.get(lp.centerVerticalOf)? .let { it.top + (it.bottom - it.top) /2 } ?: 0) - child.measuredHeight / 2} } lp.topToBottomOf ! = -1- > {val b = if (lp.topToBottomOf == parentId) bottom else childMap.get(lp.topToBottomOf)? .bottom ? :0(b + lp.topMargin) } lp.topToTopOf ! = -1- > {val t = if (lp.topToTopOf == parentId) top else childMap.get(lp.topToTopOf)? .top ? :0(t + lp.topMargin) } lp.bottomToTopOf ! = -1- > {val t = if (lp.bottomToTopOf == parentId) top else childMap.get(lp.bottomToTopOf)? .top ? :0(t - lp.bottomMargin) - child.measuredHeight } lp.bottomToBottomOf ! = -1- > {val b = if (lp.bottomToBottomOf == parentId) bottom else childMap.get(lp.bottomToBottomOf)? .bottom ? :0
                (b - lp.bottomMargin) - child.measuredHeight
            }
            else -> 0}}// Calculate the distance to the left of the parent control depending on the case
    private fun getChildLeft(lp: LayoutParam, parentWidth: Int, child: View): Int {
        val parentId = parent_id.toLayoutId()
        return when{ lp.leftPercent ! = -1f-> (parentWidth * lp.leftPercent).toInt() lp.centerHorizontalOf ! = -1- > {if (lp.centerHorizontalOf == parentId) {
                    (parentWidth - child.measuredWidth) / 2
                } else {
                    (childMap.get(lp.centerHorizontalOf)? .let { it.left + (it.right - it.left) /2 } ?: 0) - child.measuredWidth / 2} } lp.startToEndOf ! = -1- > {val r = if (lp.startToEndOf == parentId) right else childMap.get(lp.startToEndOf)? .right ? :0(r + lp.marginStart) } lp.startToStartOf ! = -1- > {val l = if (lp.startToStartOf == parentId) left else childMap.get(lp.startToStartOf)? .left ? :0(l + lp.marginStart) } lp.endToStartOf ! = -1- > {val l = if (lp.endToStartOf == parentId) left else childMap.get(lp.endToStartOf)? .left ? :0(l - lp.marginEnd) - child.measuredWidth } lp.endToEndOf ! = -1- > {val r = if (lp.endToEndOf == parentId) right else childMap.get(lp.endToEndOf)? .right ? :0
                (r - lp.marginEnd) - child.measuredWidth
            }
            else -> 0}}// When a new child control is added, its ID and reference are stored in the map
    override fun onViewAdded(child: View?). {
        super.onViewAdded(child) child? .let { childMap.put(it.id, it) } }// When a new child control is removed, its ID and reference are removed from the map
    override fun onViewRemoved(child: View?). {
        super.onViewRemoved(child) child? .let { childMap.remove(it.id) } }// Customize layout parameters
    class LayoutParam(source: ViewGroup.LayoutParams?) : MarginLayoutParams(source) {
        // Horizontal attributes
        var leftPercent: Float = -1f
        var startToStartOf: Int = -1
        var startToEndOf: Int = -1
        var endToEndOf: Int = -1
        var endToStartOf: Int = -1
        var centerHorizontalOf: Int = -1
        // Vertical attributes
        var topPercent: Float = -1f
        var topToTopOf: Int = -1
        var topToBottomOf: Int = -1
        var bottomToTopOf: Int = -1
        var bottomToBottomOf: Int = -1
        var centerVerticalOf: Int = -1}}Copy the code
  • PercentLayout uses SparseArray to store relationships between child control ids and child control references. When you get a View, you can get its ID. Why store this information in a map structure? Because you want to trade space for time, otherwise you have to iterate through all the child controls each time. The use of SparseArray instead of HashMap is also for memory saving purposes, as it is relatively more memory efficient. For detailed analysis, click Memory Optimization: SparseArray full of Contradictions.

  • New set of relative layout properties for PercentLayout that have the same semantics as in ConstraintLayout. But there are two special ones: centerHorizontalOf, which means horizontal alignment with respect to a control, and centerVerticalOf, which means vertical alignment with respect to a control.

  • This set of relative layout attributes is mutually exclusive, and they are divided into two groups, one landscape and one vertical (see code comments). A control can have only one landscape property and one vertical property. GetChildLeft () and getChildTop traverse all horizontal and vertical attributes, respectively, taking different calculations based on the relative position to determine the left and top of the child control relative to the parent control.

We can then construct the layout of the table body item like this:

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.08 f // The percentage relative to the left of the parent control
        center_vertical_of_percent = parent_id // center vertically relative to the parent control
    }

    ImageView {
        layout_id = "ivAvatar"
        layout_width = 20
        layout_height = 20
        scaleType = scale_center_crop
        center_vertical_of_percent = parent_id // center vertically relative to the parent control
        left_percent = 0.15 f // The percentage relative to the left of the parent control
    }

    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" // Located to the right of the ivAvatar control
        top_to_top_of_percent = "ivAvatar" // Align with the top of the ivAvatar control
        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" // Align with the left of the tvName control
        top_to_bottom_of_percent = "tvName" // under the tvName control
    }

    ImageView {
        layout_id = "ivLevel"
        layout_width = 10
        layout_height = 10
        scaleType = scale_fit_xy
        center_vertical_of_percent = "tvName" // Align vertically with the tvName control
        start_to_end_of_percent = "tvName" // after the tvName control
        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" // Align vertically with the tvName control
        start_to_end_of_percent = "ivLevel" // After ivLevel control
        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 // Centered relative to the parent control
        end_to_end_of_percent = parent_id // after the parent control
        margin_end = 20}}Copy the code

Measure + Layout takes the following time to run the demo:

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
Copy the code

Measure + Layout uses 288 ms, which is more than a dozen milliseconds more than FrameLayout, but still a significant improvement over ConstraintLayout’s 330 ms.

Measure + Layout time was reduced to 288 ms from the initial 370 ms after two optimations, namely abandoning XML and replacing the root layout of entries, which improved the performance by 22%. But that’s not half the time. Limited to space reasons, the follow-up optimization in the next chapter to continue to explain. Please follow me for blog updates as soon as possible.

Talk is cheap, show me the code

Relative percentage layout + Layout DSL code address proxy mode decouples RecyclerView code address

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?