RecyclerView性能优化的最后一公里

2021-09-03 14:54:42 浏览数 (1)

1. 前言

时至今日相信大部分的Android开发者对RecyclerView的缓存机制如数家珍。相关教程也是数不胜数。如果你想详细了解这些不同缓存的作用以及实现原理。可以参考我之前写过的两篇文章。聊聊RecyclerView缓存机制和详细聊聊RecyclerView缓存机制,前者主要是介绍各个层级缓存的作用以及它们之间的区别,后者主要是从源码的角度讲解缓存是怎么实现的。缓存架构图如下:

「今天我们重点来讲解一下ViewCacheExtension缓存」
代码语言:javascript复制
public abstract static class ViewCacheExtension {

    public abstract View getViewForPositionAndType(
        Recycler recycler,
        int position,
        int type
    );
}

ViewCacheExtension是RecyclerView框架预留给开发者实现自己的缓存逻辑的一个接口。很诡异的是,就算是到2021年的秋天,无论你怎么搜索,还是很难找到正确使用ViewCacheExtension的方法。网上的教程,对它的定性都很一致,由于ViewCacheExtension只提供了getView而没有提供putView方法,所以它的用处不大「当然这是错误的,本文就是为ViewCacheExtension翻案的。」 当我们穷尽所有方法,把RecyclerView调优方案都用尽了的时候,用好ViewCacheExtension就成了将RecyclerView性能优化到极致的最后一公里。

曾经我也是Too young too simple,说ViewCacheExtension没什么软用。下图引用自我写的聊聊RecyclerView缓存机制

2. ViewCacheExtension能为性能优化做什么?

"减少ItemView的嵌套层级,让布局尽量轻量级"或者减少ItemView的inflate时长会是RecyclerView性能优化的众多Tips中的其二。这样的方案当然没问题。但是现实有可能是,ItemView本身就是很复杂,将它的布局优化之后inflate还是很耗时 或者ItemView是前辈写的,太复杂了,后继的开发者无能为力或者不愿意去修改它。 这种情况下如何进一步优化到极致。当然你可能会说,我用ConstraintLayout将布局优化到极致,我能力强而且能吃苦耐劳,前辈写的复杂且低效的布局我有信心有能力优化好。退一步讲,这些你都做的很好了。RecyclerView刚初始化的时候ItemView inflate终归要耗时,而且是会阻塞线程。假设有个10个ItemView,每个耗时20ms,那也会阻塞主线程200ms,有没有办法优化呢?

❝答案当然是有。用ViewCacheExtension来优化。用它来优化RecyclerView初始化时创建View对主线程阻塞的时长。 ❞

3. 从一个案例说起

首先模拟复杂View的场景。TextView的构造方法中休眠100ms。

代码语言:javascript复制
class HeavyTextView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : androidx.appcompat.widget.AppCompatTextView(context, attrs, defStyleAttr) {
    init {
        println("heavy view init")
        Thread.sleep(100L)
    }
}

RecyclerView的界面很简单,就是几个TextView。itemView布局文件代码如下:

代码语言:javascript复制
<androidx.cardview.widget.CardView 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="5dp"
    android:layout_marginTop="5dp"
    android:layout_marginRight="5dp"
    android:layout_marginBottom="5dp">
    <com.peter.viewgrouptutorial.recyclerview.HeavyTextView
        android:id="@ id/heavy.text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/white_touch"
        android:clickable="true"
        android:orientation="horizontal"
        android:padding="@dimen/small"
        android:textSize="14sp" />
</androidx.cardview.widget.CardView>

程序运行结果如下:

我们通过Systrace来看下RecyclerView性能表现

通过上图我们可以看到。初始化HeavyTextView总共花费了639ms。我们知道Android每帧的耗时超过16ms就要掉帧了。所以相对来说比较卡顿。实际运行程序,也会发现跳转到该Activity明显不流畅。

对比下优化后的效果。前提是不修改HeavyTextView,仍然休眠100ms

对比RV OnLayout事件,优化后的效果只需要76ms。将近10倍的优化空间。实际效果是,跳转Activity很顺滑很流畅。

4. 优化方案

程序UI模型图如下,从AActivity跳转到BActivity,它有一个RecyclerView列表。

AActivity代码如下:

图片版本代码:

Kotlin版本代码 方便复制

代码语言:javascript复制
class AActivity : AppCompatActivity() {
    companion object {
        //静态变量,ArrayList保存开发者缓存View
        var sCustomViewCaches: ArrayList<View> = arrayListOf()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //当AActivity MessageQueue有空闲的时候,创建10个HeavyText布局ItemView
        Looper.myQueue().addIdleHandler {
            thread {
                repeat(10) {
                    val linearLayout = LinearLayout(this@AActivity).apply {
                        orientation = LinearLayout.VERTICAL
                    }

                    //将itemView add到linearLayout上,后有remove掉,为了正确的将item布局中padding显示出来
                    val itemView = LayoutInflater.from(this@AActivity)
                        .inflate(R.layout.custom_cache_view_item, linearLayout)
                    linearLayout.removeView(itemView)

                    //背景设置成红色为了更好的测试是否用到了正确缓存中的View
                    itemView.setBackgroundColor(Color.RED)

                    itemView.layoutParams = RecyclerView.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT
                    )

                    // 反射设置RecyclerView.LayoutParams的mViewHolder属性
                    val viewHolderField =
                        RecyclerView.LayoutParams::class.java.getDeclaredField("mViewHolder")
                            .apply {
                                isAccessible = true
                            }
                    
                     //等效于Adapter中的onCreateViewHolder方法,创建ViewHolder
                    val viewHolder = object : RecyclerView.ViewHolder(itemView) {}
                    
                    //将ViewHolder的mItemViewType设置成0。具体业务具体实现。主要是为了复用
                    with(
                        RecyclerView.ViewHolder::class.java.getDeclaredField("mItemViewType")
                            .apply {
                                isAccessible = true
                            }) {
                        set(viewHolder, 0)
                    }
                    
                    viewHolderField.set(itemView.layoutParams, viewHolder)
                    
                    //将ItemView保存到缓存中
                    sCustomViewCaches.add(itemView)
                }

                println("custom  view cache ok")

            }
            false
        }
    }
}

BActivity实现如下

图片版本代码:

Kotlin版本代码 方便复制

代码语言:javascript复制
class BActivity : AppCompatActivity() {
    private lateinit var mRecyclerView: RecyclerView
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_recycler_view_custom_cache)
        mRecyclerView = findViewById(R.id.recyclerview)
        //省略很多RecyclerView的常规操作比如setAdapter和LayoutManager
        mRecyclerView.setViewCacheExtension(object : RecyclerView.ViewCacheExtension() {
            override fun getViewForPositionAndType(
                recycler: RecyclerView.Recycler,
                position: Int,
                type: Int
            ): View? {
                //从AActivity的缓存中拿View,Demo实例,实际业务可以写的更优雅
                if (AActivity.sCustomViewCaches.size != 0) {
                    val view = DashboardActivity.sCustomViewCaches.removeFirst()
                    println("custom cache view remove $position $view")
                    if (position == 0) {
                        println("attention $position $view")
                    }
                    return view
                }
                return null
            }
        })
    }
}

5.遇到的坑

  1. 空指针异常。解决方案:为itemView设置RecyclerView.LayoutParems。
  1. ViewHolder不能为空。解决方案:反射设置ViewHolder。
  1. 布局间距不正确。解决方案:先将itemView add到临时viewGroup上,然后remove掉。
  2. 缓存复用不正确。解决方案:反射设置ViewHolder的itemViewType。
  3. 缓存不够用。原因RecyclerView的layout_height="wrap_content",解决方案:"设置成match_parent"。与测量机制有关。

0 人点赞