Record "Recyclerview Optimization" in a project

Preface

Reading this article can help you understand: In a complex RecyclerView, there are hundreds of Items, each of which contains a large amount of data and images. How can I load and display this data efficiently while maintaining smooth scrolling of the list?

the whole frame

The beginning of the problem:

The problem encountered in an online shopping APP (not a multi-point APP), there are hundreds of items displayed, and each item has a large amount of data and picture display. That is to say, the amount of data is very large, causing the product list loading and display process to be very slow.

Positioning problem:

  • Window.addOnFrameMetricsAvailableListener() is equivalent to adb shell dumpsys gfxinfo to get the rendering time in detail, accurate to seconds.
  • Profiler for Android

Solution to the problem:

  • Efficient updates: DiffUtil
  • Paging preloading (via sliding listening)
  • Set up public addXxListener listening (click, long press, etc.)
  • Other optimizations: things to pay attention to during development
    • Increase cache and exchange space for time: RecycleView.setItemViewCacheSize(size);
    • Strategy for loading images during the sliding process: Don’t simply judge based on the sliding status. It is recommended to judge based on the sliding speed and inertial sliding.
    • Reduce the parsing time of XML. You can create and add views through new view to replace the layout View in XML.
    • Reduce rendering levels: Use custom View (ConstraintLayout is not recommended in particular, it renders very slowly during the first rendering)
    • Items that can be fixed in height can be fixed in height to reduce measurement time.
      • The function of RecyclerView.setHasFixedSize(true) is to tell RecyclerView that the size of the items (Item) will not change on the way.
    • If there is no animation requirement, turn off the default animation.
      • ((SimpleItemAnimator) rv.getItemAnimator()).setSupportsChangeAnimations(false); Turn off the default animation

Other solutions:

  • Dynamically build Kotlin's DSL layout to replace
    the performance loss of xml evaporative IO and reflection, shortening the time it takes to build table item layouts. It's a bit much, and it reduces the readability of the code. After all, you can't preview the interface. There are other personnel doing follow-up maintenance. Reducing the time by tens of milliseconds is not worth the gain.
  • Paging 3
    Both solutions have the same problem: there are learning and maintenance costs. Especially for existing projects, the changes are too large and involve too many businesses, making it difficult to locate problems when they arise.

Efficiently update DiffUtil

What is DiffUtil

DiffUtil is a utility class for calculating the difference between two lists. It compares the elements of two lists, finds the differences between them, and generates a list of update operations so that minimal update operations can be performed.
Of course, this minimized update operation can be completely controlled by strictly using the notify-related API, so I think DiffUtil is a standardized form of minimized update operation. (ps: After all, it is inevitable that notify will be triggered incorrectly, resulting in a waste of resources)

Note: The emphasis is on the update of DiffUtil. If you only want to add it separately, you still want to use notifyItemInserted(). You must know the operation of adding it separately in business.
Principle: Just understand it: DiffUtil uses the Longest Common Subsequence (LCS) algorithm to compare the differences between two data sets. The algorithm first creates a matrix, the rows of which represent the elements of the old data set, and the columns represent the elements of the new data set. The longest common subsequence is then constructed by backtracking, and the differences between the two data sets are determined by comparing elements that do not belong to the longest common subsequence.

Core classDiffUtil.Callback

When we use DiffUtil, we mainly use DiffUtil.Callback, which masters several important abstract methods for monitoring differences.

  • getOldListSize(): Get the size of the old data set
  • getNewListSize()://Get the size of the new data set
  • areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean
    • Get the elements at corresponding positions in the new and old lists respectively, and define under what circumstances the old and new elements are the same object
  • areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean
    • Get the elements at corresponding positions in the new and old lists respectively, and define under what circumstances whether the contents of the same object are the same.
  • getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any?
    • Get the elements at corresponding positions in the new and old lists respectively. If the two elements are the same but the content has changed, you can use this method to get the difference information between them, so that only the parts that need to be changed are updated and unnecessary update operations are reduced.

The specific code and explanation are as follows :

class RvAdapter : RecyclerView.Adapter<RvAdapter.ViewHolder>() {
    
    
    class ViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView)

    var oldList: MutableList<MessageData> = mutableListOf() // 老列表
    var newList: MutableList<MessageData> = mutableListOf() // 新列表

    val diffUtilCallBack = object : DiffUtil.Callback(){
    
    
        override fun getOldListSize(): Int {
    
    
            // 获取旧数据集的大小
            return oldList.size
        }

        override fun getNewListSize(): Int {
    
    
            // 获取新数据集的大小
            return newList.size
        }

        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
    
    
            // 分别获取新老列表中对应位置的元素
            // 定义什么情况下新老元素是同一个对象(通常是业务id)
            val oldItem = oldList[oldItemPosition]
            val newItem = newList[newItemPosition]
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
    
    
            // 定义什么情况下同一对象内容是否相同 (由业务逻辑决定)
            // areItemsTheSame() 返回true时才会被调用
            val oldItem = oldList[oldItemPosition]
            val newItem = newList[newItemPosition]
            return oldItem.content == newItem.content
        }

        override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
    
    
            // 可以通过这个方法获取它们之间的差异信息
            // 具体定义同一对象内容是如何地不同 (返回值会作为payloads传入onBindViewHoder())
            // 当areContentsTheSame()返回false时才会被调用
            val oldItem = oldList[oldItemPosition]
            val newItem = newList[newItemPosition]
            return if (oldItem.content === newItem.content) null else newItem.content
        }
    }

    fun upDataList(oldList: MutableList<MessageData>, newList: MutableList<MessageData>){
    
    
        this.oldList = oldList
        this.newList = newList
        // 利用DiffUtil比对结果
        val diffResult = DiffUtil.calculateDiff(diffUtilCallBack)
        // 将比对结果应用到 adapter
        diffResult.dispatchUpdatesTo(this)
    }
    
    // 其他常规的函数
    .....
    .....
}

Just take a look at the source code

Just take a look at what diffResult.dispatchUpdatesTo(this) does. The differential algorithm was just mentioned above

// 将比对结果应用到Adapter
public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
    
    
    dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));
}

// 将比对结果应用到ListUpdateCallback
public void dispatchUpdatesTo(@NonNull ListUpdateCallback updateCallback) {
    
    ...}

// 基于 RecyclerView.Adapter 实现的列表更新回调
public final class AdapterListUpdateCallback implements ListUpdateCallback {
    
    
    private final RecyclerView.Adapter mAdapter;
    public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
    
    
        mAdapter = adapter;
    }
    @Override
    public void onInserted(int position, int count) {
    
    
            // 区间插入
        mAdapter.notifyItemRangeInserted(position, count);
    }
    @Override
    public void onRemoved(int position, int count) {
    
    
            // 区间移除
        mAdapter.notifyItemRangeRemoved(position, count);
    }
    @Override
    public void onMoved(int fromPosition, int toPosition) {
    
    
            // 移动
        mAdapter.notifyItemMoved(fromPosition, toPosition);
    }
    @Override
    public void onChanged(int position, int count, Object payload) {
    
    
            // 区间更新
        mAdapter.notifyItemRangeChanged(position, count, payload);
    }
}

So I said it above: I think DiffUtil is a canonical form of minimizing update operations.

Asynchronous Diff calculation process

As mentioned above, we calculate our differences through algorithms. When encountering big data, you will inevitably encounter time-consuming problems caused by calculations.
Therefore, it is necessary to process this Diff process asynchronously. (ps: Use coroutine directly)

suspend fun upDataList(oldList: MutableList<MessageData>, newList: MutableList<MessageData>) =
    withContext(Dispatchers.Default) {
    
    
        this@RvAdapter.oldList = oldList
        this@RvAdapter.newList = newList

        // 利用DiffUtil比对结果
        val diffResult = DiffUtil.calculateDiff(diffUtilCallBack)
        withContext(Dispatchers.Main) {
    
    
            // 将比对结果应用到 adapter
            diffResult.dispatchUpdatesTo(this@RvAdapter)
        }
    }

pit

  • Pay attention to thread safety issues during asynchronous operations: you can use Mutex to protect the access and modification of oldList and newList.

Modified code

private val updateListMutex = Mutex()

suspend fun upDataList(oldList: MutableList<MessageData>, newList: MutableList<MessageData>) = withContext(Dispatchers.Default) {
    
    
    // 加锁,保护数据的访问和修改
    updateListMutex.withLock {
    
    
        this@RvAdapter.oldList = oldList
        this@RvAdapter.newList = newList
    }

    // 利用DiffUtil比对结果
    val diffResult = DiffUtil.calculateDiff(diffUtilCallBack)

    withContext(Dispatchers.Main) {
    
    
        // 加锁,保护数据的访问和修改
        updateListMutex.withLock {
    
    
            // 将比对结果应用到 adapter
            diffResult.dispatchUpdatesTo(this@RvAdapter)
        }
    }
}
  • DiffUtil determines whether two data objects are the same by comparing their references.

If you are using immutable objects that are modified by Final, then there will be no problem.
If it is a mutable object, then you need to override the equals and hashCode methods so that DiffUtil can correctly compare data items. The specific code depends on the actual business.

Paginated preloading

Optimization ideas

Of course, loading data in pages is a must: the content of the list requires paged data returned by the server. This avoids request delays caused by loading excessive data at one time. It also reduces the pressure on the server.
So how do we optimize this paging?
Since preloading is an important idea for us to optimize loading speed. So can this idea also be added to paging?
That is to say: request the next page of data before the page of data is finished. Then we can do it through two ideas:

  • Request the next page of data before the page of data is finished.
  • The first time you request 2 pages of content, when you slide through all the items on the current page, you will request the content of subsequent pages (of course, the number of preloaded pages can also be 3 or more)

Realization (first type)

Both methods load the data on the next page in advance to optimize user perception.
We will only talk about the first method here, the second method is similar.

  • Step 1: Rewrite the Adapter of RecyclerView to listen to the position of the bound Item in the list, and request data when the threshold is reached.
class PreloadAdapter : RecyclerView.Adapter<ViewHolder>() {
    
    

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    
    
        checkPreload(position)    // 判断是否达到阈值
    }

Note: Those who know RecyclerView here may ask, then onBindViewHolder will be called back when RecyclerView is preloaded. Not when the current Item is displayed on the page.
Answer: Of course, but the first point is that you can set the number of RecyclerView preloads. The second point is that if it will be called back during preloading, the request will be advanced. What's wrong with that?

  • Step 2: Monitor the sliding status and load again when it is determined to be triggered by sliding.
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
    
    
    recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    
    
        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
    
    
            // 更新滚动状态
            scrollState = newState
            super.onScrollStateChanged(recyclerView, newState)
        }
    })
}
  • Step 3: Prevent the list from scrolling back to the bottom after triggering a preload. Rolling it down again risks triggering it again when preloading is not completed.
// 增加预加载状态标记位
var isPreloading = false
  • Complete code
class PreloadAdapter : RecyclerView.Adapter<ViewHolder>() {
    
    
    // 增加预加载状态标记位
    var isPreloading = false
    // 预加载回调
    var onPreload: (() -> Unit)? = null
    // 预加载偏移量
    var preloadItemCount = 0
    // 列表滚动状态
    private var scrollState = SCROLL_STATE_IDLE

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    
    
        checkPreload(position)
    }

    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
    
    
        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    
    
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
    
    
                // 更新滚动状态
                scrollState = newState
                super.onScrollStateChanged(recyclerView, newState)
            }
        })
    }

    // 判断是否进行预加载
    private fun checkPreload(position: Int) {
    
    
        if (onPreload != null
            && position == max(itemCount - 1 - preloadItemCount, 0)// 索引值等于阈值
            && scrollState != SCROLL_STATE_IDLE // 列表正在滚动
            && !isPreloading // 预加载不在进行中
        ) {
    
    
            isPreloading = true // 表示正在执行预加载
            onPreload?.invoke()
        }
    }
}
  • Step 4: Call
val preloadAdapter = PreloadAdapter().apply {
    
    
    // 在距离列表尾部还有2个表项的时候预加载
    preloadItemCount = 2
    onPreload = {
    
    
        // 预加载业务逻辑
    }
}

Use public listener

We can use custom public listeners to reduce the creation time of the listener object, improve performance, and use the holder.getAdapterPosition() method to obtain the accurate ID or Tag for judgment.

Wrong approach

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    
    
    holder.itemText.text = mItemList[position]
    holder.itemText.setOnClickListener({
    
     
        // 具体点击业务
    })
}

This will set the listening object for itemText every time the View is bound, that is, when onBindViewHolder is executed, a large number of objects will be created frequently. What are you doing! ! ! !

Recommended practices

  • Step one: First, define an interface in the adapter of RecyclerView as a public listener:
interface RecyclerViewListener {
    
    
    fun onItemClick(position: Int)
    fun onItemLongClick(position: Int)
}
  • Step 2: Then, set the listener in the ViewHolder of RecyclerView:
class RecyclerViewHolder(itemView: View, private val listener: RecyclerViewListener) : RecyclerView.ViewHolder(itemView),
    View.OnClickListener, View.OnLongClickListener {
    
    
    private val textView: TextView = itemView.findViewById(R.id.textView)

    init {
    
    
        itemView.setOnClickListener(this)
        itemView.setOnLongClickListener(this)
    }

    override fun onClick(v: View) {
    
    
        val position = adapterPosition
        listener.onItemClick(position)
    }

    override fun onLongClick(v: View): Boolean {
    
    
        val position = adapterPosition
        listener.onItemLongClick(position)
        return true
    }

    fun bindData(data: String) {
    
    
        textView.text = data
    }
}
  • Step 3: Next, set up the listener in the adapter:
class RecyclerViewAdapter(private val dataList: List<String>, private val listener: RecyclerViewListener) :
    RecyclerView.Adapter<RecyclerViewHolder>() {
    
    

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder {
    
    
        val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
        return RecyclerViewHolder(itemView, listener)
    }

    override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
    
    
        val data = dataList[position]
        holder.bindData(data)
    }

    override fun getItemCount(): Int {
    
    
        return dataList.size
    }
}
  • Step 4: Finally, where RecyclerView is used, set a public listener and create an adapter:
val listener = object : RecyclerViewListener {
    
    
    override fun onItemClick(position: Int) {
    
    
        // 处理点击事件
    }

    override fun onItemLongClick(position: Int) {
    
    
        // 处理长按事件
    }
}

val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
val adapter = RecyclerViewAdapter(dataList, listener)
recyclerView.adapter = adapter

In this way, the creation time of the listening object can be reduced, performance can be improved, and the holder.adapterPosition property can be used to obtain the accurate ID or Tag for judgment.

Summarize

This article records my optimization research on RecyclerView in the project and the actual optimization methods.
Everyone saves it for later use! ! ! ! ! !

Guess you like

Origin blog.csdn.net/weixin_45112340/article/details/132766481