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月前
|
C# UED 开发者
WPF与性能优化:掌握这些核心技巧,让你的应用从卡顿到丝滑,彻底告别延迟,实现响应速度质的飞跃——从布局到动画全面剖析与实例演示
【8月更文挑战第31天】本文通过对比优化前后的方法,详细探讨了提升WPF应用响应速度的策略。文章首先分析了常见的性能瓶颈,如复杂的XAML布局、耗时的事件处理、不当的数据绑定及繁重的动画效果。接着,通过具体示例展示了如何简化XAML结构、使用后台线程处理事件、调整数据绑定设置以及利用DirectX优化动画,从而有效提升应用性能。通过这些优化措施,WPF应用将更加流畅,用户体验也将得到显著改善。
355 1
|
6月前
|
监控 Java 图形学
【性能优化篇】U3D游戏卡顿大作战:内存与渲染效率的极致提升
【7月更文第12天】在Unity3D游戏开发领域,性能优化是决定玩家体验好坏的关键一环。游戏频繁卡顿,不仅破坏了沉浸式体验,还可能造成玩家流失。本文将深入探讨如何有效解决U3D游戏卡顿问题,特别聚焦于内存管理和渲染效率两大核心领域,助力开发者打造流畅丝滑的游戏世界。
480 0
|
缓存 Java 测试技术
RecyclerView 优化—滑动性能提升
RecyclerView 优化—滑动性能提升
342 0
|
XML 存储 Android开发
ListView的“终极优化”,打造你的万能适配器
传统的代码逻辑我们是怎么使用的呢?每写一个ListView,都要去写一个Adapter类,一个ViewHolder类,这几乎是我们必须要操作的,以致于有太多太多的冗余代码,让我们感到真的不厌其烦,一个两个还可以,十个八个,就真的有点太崩溃了,不仅代码繁琐,还会占用内存,为了解决这样的一个问题,下面就要开始对其抽取优化。
|
缓存 算法 API
使用优化 | RecyclerView中可优化的点
使用优化 | RecyclerView中可优化的点
使用优化 | RecyclerView中可优化的点
|
XML SQL 算法
Android性能优化 | 把构建布局用时缩短 20 倍(下)
上一篇讲述了 Activity 构建布局的过程,及测量其耗时的方法。这一篇在此基础上给出优化构建布局的方案。
303 0
|
XML Java Android开发
Android性能优化 | 把构建布局耗时缩短 20 倍(上)
xml 布局文件是如何变成 View 并填入 View 树的?带着这个问题,阅读源码,居然发现了一个优化布局构建时间的方案。
416 0
|
设计模式 消息中间件 Java
Android性能优化方案
Android性能优化方案
1028 2
|
存储 测试技术
为什么优化性能,相亲源码怎样进行性能优化
完成性能优化后进行性能测试,与相亲源码之前的性能指标进行对比,分析现在的系统性能是否符合需求,如果不符合的话,再次进行优化。

热门文章

最新文章