现象
常在路边走,哪有不湿鞋。
这不就有一个需求,让做一个列表的单条目刷新,多简单的一个需求啊,简简单单的CV之后,代码就敲完了,然后手贱了一小下,做了个自测,然后,悲剧就这么出现了。
具体需求我已经记不住了,这里模拟一下类似的情况,自定义一个ViewHolder,其中有一个参数count,用于记录当前ViewHolder刷新了多少次,然后通过TextView展示出来即可。
布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:viewBindingIgnore="true">
<EditText
android:id="@+id/index"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="onlyNotify"
android:text="无Payload刷新" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="notifyAll"
android:text="全局刷新" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="notifyWithPayloads"
android:text="有Payload刷新" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/loop_rv"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
tools:context=".ui.loop.LoopActivity">
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>
自定义ViewHolder,其中为了区分各个条目,给TextView添加了一个背景色:
class PayloadTestHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var count = -1
fun setText(
holder: PayloadTestHolder,
position: Int,
payloads: MutableList<Any>?
) {
val tv = holder.itemView.findViewWithTag<TextView>(-123456)
count++
tv.text = "count = $count"
Logger.i("$position --- ${holder.hashCode()}")
tv.setBackgroundColor(getBgColor(position))
}
fun getBgColor(position: Int): Int {
return when (position % 4) {
0 -> Color.GREEN
1 -> Color.BLUE
2 -> Color.RED
3 -> Color.YELLOW
else -> Color.GRAY
}
}
}
自定义adapter,itemCount为8是为了模拟超出一页:
private val mAdapter = object : RecyclerView.Adapter<PayloadTestHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): PayloadTestHolder {
val lp = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 300)
val tv = TestTextView(parent.context)
tv.layoutParams = lp
tv.gravity = Gravity.CENTER
tv.tag = -123456
return PayloadTestHolder(tv)
}
override fun onBindViewHolder(holder: PayloadTestHolder, position: Int) {
holder.setText(holder, position, null)
}
override fun onBindViewHolder(
holder: PayloadTestHolder,
position: Int,
payloads: MutableList<Any>
) {
holder.setText(holder, position, payloads)
}
override fun getItemCount(): Int = 8
}
Activity:
class RecyclerTestActivity : AppCompatActivity() {
private val random = Random()
private val mAdapter = object : RecyclerView.Adapter<PayloadTestHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): PayloadTestHolder {
val lp = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 300)
val tv = TestTextView(parent.context)
tv.layoutParams = lp
tv.gravity = Gravity.CENTER
tv.tag = -123456
return PayloadTestHolder(tv)
}
override fun onBindViewHolder(holder: PayloadTestHolder, position: Int) {
holder.setText(holder, position, null)
}
override fun onBindViewHolder(
holder: PayloadTestHolder,
position: Int,
payloads: MutableList<Any>
) {
holder.setText(holder, position, payloads)
}
override fun getItemCount(): Int = 8
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_loop)
loop_rv.layoutManager = LinearLayoutManager(this)
loop_rv.adapter = mAdapter
index.setText("0")
}
class Holder(itemView: View) : RecyclerView.ViewHolder(itemView)
fun randomLoop(view: View) {
val position = random.nextInt(8)
loop_rv.smoothScrollToPosition(position)
Logger.d("randomLoop ------------ $position")
}
fun onlyNotify(view: View) {
mAdapter.notifyItemChanged(getIndex())
}
fun notifyWithPayloads(view: View) {
mAdapter.notifyItemChanged(getIndex(), "payload")
}
fun notifyAll(view: android.view.View) {
mAdapter.notifyDataSetChanged()
}
private fun getIndex(): Int {
val text = index.text
if (text.isNullOrBlank()) {
return 0
}
val index = text.toString().trim().toInt()
return if (index in 0 until 8) {
index
} else {
0
}
}
}
在android圈子里也摸爬滚打这么多年了,刷新一条数据的时候肯定不能使用notifyDataSetChanged(),而使用notifyItemChanged(position),当然,也不是能力多强知道notifyDataSetChanged()消耗资源,单纯的因为在现在的AndroidStudio中使用notifyDataSetChanged()刷新列表的时候,已经报警告了。
至于原因,当然是由于RecyclerView的缓存机制(参见:knyou的RecyclerView缓存机制),如果使用notifyDataSetChanged(),所有的ViewHolder都会跳过所有的缓存数据,直接在getRecycledViewPool().getRecycledView(type)中获取,消耗不必要的资源,运行一下看看效果。
却发现刷新两次,count才增加1,明显与我们的设计初衷不符,可代码逻辑明明没有问题啊,如果说是缓存问题,可是我只刷新了一个条目啊!不过为了验证一下,还是打印了一下ViewHolder的hashCode,看看究竟出了什么问题
class PayloadTestHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var count = -1
fun setText(
holder: PayloadTestHolder,
position: Int,
payloads: MutableList<Any>?
) {
val tv = holder.itemView.findViewWithTag<TextView>(-123456)
count++
tv.text =
"position = $position \n view code = ${tv.hashCode()}\n holder code = ${holder.hashCode()} \n payloads = $payloads \n count = $count"
Logger.i("$position --- ${holder.hashCode()}")
tv.setBackgroundColor(getBgColor(position))
}
fun getBgColor(position: Int): Int {
return when (position % 4) {
0 -> Color.GREEN
1 -> Color.BLUE
2 -> Color.RED
3 -> Color.YELLOW
else -> Color.GRAY
}
}
}
可以看到,竟然是两个Holder的hashCode在循环展示,也就是这两个ViewHolder的count轮流+1,,瞬间一脸懵逼,多的那个ViewHolder是哪里来的!经过一通断点,发现这个ViewHolder也是从getRecycledViewPool().getRecycledView(type)获取的。具体原因查看的层级胖菜鸟也看不懂,这里就直接给提供一下解决方案吧。
解决方案
- ViewHolder中缓存数据,使用notifyItemChanged(position, payloads)刷新数据,但是这样做的局限是,只能在条目刷新的时候有效,但是数据量过大时,ViewHolder的复用还会导致数据错乱
- 将数据提取到外部,使用数据Bean集合持有对应的count值,且数据的值的变更统一在对应条目的数据Bean中保存最终结果,且赋值逻辑也是从数据Bean中取值(推荐)
建议
当然上述的举例在开发中出现概率还是很低的,大家都知道数据集合存储的优雅形式。我当时使用的时候应该是缓存了一下Holder的hashCode作为状态的记录标签,同样出现了这个问题,总之都是对于ViewHolder的缓存理解不到位导致的,特此记录,注意就好。