RecyclerView性能优化的最后一公里

简介: RecyclerView性能优化的最后一公里

1. 前言


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

640.png

「今天我们重点来讲解一下ViewCacheExtension缓存」
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缓存机制

640.jpg


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。

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布局文件代码如下:

<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>


程序运行结果如下:

640.jpg


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

640.jpg


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


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


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


4. 优化方案


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

640.png


AActivity代码如下:

640.png

Kotlin版本代码 方便复制


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实现如下


图片版本代码:

640.png

Kotlin版本代码 方便复制

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。

640.jpg

ViewHolder不能为空。解决方案:反射设置ViewHolder。

640.jpg


  1. 布局间距不正确。解决方案:先将itemView add到临时viewGroup上,然后remove掉。


  1. 缓存复用不正确。解决方案:反射设置ViewHolder的itemViewType。


  1. 缓存不够用。原因RecyclerView的layout_height="wrap_content",解决方案:"设置成match_parent"。与测量机制有关。
相关文章
|
资源调度 监控 JavaScript
3倍+提升,高德地图极致性能优化之路
伴随着高德地图APP近几年的高速发展,也面临到这些问题,从2019年开始,我们开启了一系列性能优化专项,对高德地图APP进行了深入性能分析和极致优化,取得比较显著的效果。在这个过程中总结了一系列优化思路和技术方案,希望对同样面临超级应用性能问题的你有所帮助。
|
5月前
|
设计模式 缓存 Android开发
深入理解Android应用性能优化
【2月更文挑战第18天】在移动开发领域,应用性能是用户体验的关键因素之一。特别是对于安卓设备而言,由于硬件配置的多样性,确保应用在不同设备上都能流畅运行是一项挑战。本文将探讨Android应用的性能优化策略,包括内存管理、UI渲染、多线程处理以及电池效率等方面。通过实例和最佳实践,我们将展示如何诊断性能瓶颈,并提供解决方案来改善应用响应速度和稳定性。
|
缓存 Java 测试技术
RecyclerView 优化—滑动性能提升
RecyclerView 优化—滑动性能提升
289 0
|
XML 存储 缓存
RecyclerView 性能优化 | 把加载表项耗时减半 (一)
RecyclerView 性能优化 | 把加载表项耗时减半 (一)
10977 0
|
XML 前端开发 数据格式
RecyclerView 性能优化 | 把加载表项耗时减半 (三)
RecyclerView 性能优化 | 把加载表项耗时减半 (三)
145 0
|
XML 存储 缓存
RecyclerView 性能优化 | 把加载表项耗时减半 (三)(下)
RecyclerView 性能优化 | 把加载表项耗时减半 (三)
125 0
|
XML 存储 缓存
RecyclerView 性能优化 | 把加载表项耗时减半 (二)
RecyclerView 性能优化 | 把加载表项耗时减半 (二)
221 0
|
缓存 算法 API
使用优化 | RecyclerView中可优化的点
使用优化 | RecyclerView中可优化的点
使用优化 | RecyclerView中可优化的点
|
监控 API Android开发
Android卡顿优化 | 卡顿及其优化工具概述及StrictMode实践案例
Android卡顿优化 | 卡顿及其优化工具概述及StrictMode实践案例
|
XML Java Android开发
Android性能优化 | 把构建布局耗时缩短 20 倍(上)
xml 布局文件是如何变成 View 并填入 View 树的?带着这个问题,阅读源码,居然发现了一个优化布局构建时间的方案。
395 0