前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >RecyclerView性能优化的最后一公里

RecyclerView性能优化的最后一公里

作者头像
音视频开发进阶
发布2021-09-03 14:54:42
1.3K0
发布2021-09-03 14:54:42
举报

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"。与测量机制有关。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-08-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 音视频开发进阶 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 前言
  • 2. ViewCacheExtension能为性能优化做什么?
  • 3. 从一个案例说起
  • 4. 优化方案
  • 5.遇到的坑
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档