rv viewholder复用流畅度问题
RV 基础理解
viewholder,为什么需要viewholder
非常显然一个rv,他滑动一定是需要限制展示的view和复用的
比如一屏显示6个view,这个时候大概就需要维护一个7~8个显示的view的队列就好了
非常直观,然后这个view,inflate的开销是很大的,要去找xml渲染,所以就直接这个存七八个view,就这个队列的view,然后挨个复用,就修改他的内容就行了。
然后呢,这里view内部会有一堆东西,比如textview这种东西,你如果不持有这部分引用的话,你挨个bind的时候还得findviewbyId,这部分查找也很麻烦
所以就有viewhoder这个概念,就持有view和他内部的资源的引用比如textview一大堆,然后还有一堆本身这块儿复用以及生命周期相关的一些值(比如fragment destroy了,这个viewholder就没有用了,这个就要销毁)
场景问题1
点击进入一个有rv的fragment,此时rv的pool是空的,因为都还没开始滑动(都没创建),这个时候需要从头创建viewholder,那如果这里view要是多了,这个要创建的viewholder就多了,那这个view inflate的任务一重,就很容易导致卡顿掉帧,有流畅度问题
解题思路
view的inflate
既然view的inflate是最耗时的,而且viewholder只需要持有view的引用以及他内部的引用状态,那我只需要提前创建这个裸view,然后给viewholder不就好了?就在这个进入这个页面的过程中,比如可能有动画,那就在启动/动画这个间隔开一个子线程尝试给加载这部分view给viewholder复用
Q:疑问,如果动画结束/启动完成了,这个子线程还在加载怎么办?
T: 思考,是不是有类似于message的这种格式,obtain呢?有一个发一个,然后复用obtain就行了?然后正常就不走那个流程了,走我这个?最坏的情况下也就是退化到线上原始的那种情况
A:实际上确实是类似Message这种的思路,就是有一个发一个,然后主线程里面就直接拿,如果没有就走原来的这个createViewHolderByViewType这种
场景问题2
如果反复进入进出fragment呢?这个viewholder会频繁消费/创建
解题思路
fragment间复用
RV 原生就支持 setRecycledViewPool(pool) 来指定外部 Pool
RecyclerView.setRecycledViewPool() 是 Google 专门为跨 RV 共享而设计的 API。ViewPager2 里的多个 Fragment 共享 Pool 就是典型用法。这个方案在这个 API 基础上加了一层 PoolManager 来管理「谁和谁共享」。
Pool这里提升到activity了,然后这里的poolGroup设计也是类似rv本身的byViewType这种,根据type来分类的,然后还有一些多线程问题,子线程失败标记降级,多线程读写并发控制CAS,还有接口解耦,就你要用到的这个组件fragment就自己声明一下就行了,思想就是组件不应该依赖于具体的类,就依赖倒转吧。还有kotlin的DSL builder
DSL
DSL不是一个语法糖,它是一个设计目标:让代码读起来像自然语言/配置文件,而不像单纯的调 API
为什么能做到是因为:1. lambda 是最后一个参数时可以提到括号外面 2. 带接收者的 lambda
public inline fun
block() // 实际上是 this.block()
return this
}
大概就是接收了一个lambda函数,这个函数的this是T,也就是本身的类型
实例
class Person {
var name = “”
var age = 0
}
// 没有 apply
val p = Person()
p.name = “张三”
p.age = 25
// 用 apply
val p = Person().apply {
name = “张三” // this.name = “张三”,this 是 Person 实例
age = 25 // this.age = 25
}
然后这里结合builder的话就是
fun
return Builder
}
buildMyBuilder.apply {
buildMethod1()
buildMethod2()
}
// 等价于
val builder = Builder()
builder.apply {
buildMethod
}
builder.build()
非常的优雅,自然
然后你看,这个apply是不是和build.gradle很像,感觉就一摸一样
CAS
compareAndSet(expected, new) 在 CPU 层面是一条指令(x86 上是 CMPXCHG)
实际上就是原子的执行一条指令,就一堆线程来尝试改这个值,然后只有一个能成功
if (内存中的值 == expected) {
内存中的值 = new
return true
} else {
return false
}
那这个就只适用于只改一次的情况,因为你确实是没锁了,但是false就失败了,就不会等待了。所以一般来说这个东西会结合自旋while(true),但这个又会导致其他的问题,下面有说。
而且这个东西只能改一个变量的值。
而且还有一个问题,你怎么知道这个东西不是被修了之后再给改回来了呢?
ABA 问题
线程 A: 读到值 = 0
线程 B: 把 0 改成 1
线程 C: 把 1 改回 0
线程 A: compareAndSet(0, new_value) → 成功!
但 A 不知道中间被改过又改回来了
值从 A → B → A,CAS 看不出来中间变过。对于 boolean(false → true,不会改回来)这不是问题。但如果你用 AtomicInteger 做计数器,ABA 可能导致逻辑错误。Java 提供了 AtomicStampedReference 带版本号来解决这个
自旋浪费 CPU 如果竞争激烈,CAS 经常失败,通常的做法是循环重试(自旋)
// 典型的 CAS 重试循环
while (true) {
val current = counter.get()
val next = current + 1
if (counter.compareAndSet(current, next)) break // 成功就退出
// 失败 → 重试
}
如果很多线程同时争一个变量,大部分线程每次都失败、反复重试,CPU 空转浪费。竞争越激烈,CAS 性能越差。这种情况下锁反而更好——让线程排队等着,不浪费 CPU
总结:
改一个 boolean/int,竞争不激烈 → AtomicBoolean / AtomicInteger(CAS)
改一个 boolean/int,竞争激烈 → 还是 CAS,但考虑 LongAdder 等分段方案
改多个变量,必须同时生效 → synchronized / ReentrantLock
保护一段复杂的读-改-写逻辑 → synchronized / ReentrantLock
调度问题
这个pool有很多的工程细节,其中一部分是调度
独立线程池,优先级任务排序 不同阶段的任务紧迫程度不同,比如首屏马上要用到,和可能会用到这些优先级就是不一样,还有被降级的部分
这个调度比较复杂,就不同页面,他的优先级不一样,但是有的数据,他马上要用到了,那这个时候已经提交的数据就需要降级了。




