10

当使用带有 LinearLayoutManager 并且“reverseLayout”标志设置为 true 的 RecyclerView 时,当通过notifyItemChanged它通知任何项目时,也会调用onBindViewHolder第一个不可见项目。之后它不会要求onViewRecycled该项目。因此,如果 ViewHolder 在 onBind 中进行某种订阅,它将永远不会被释放,因为不会调用 onRecycle。

这实际上看起来像LinearLayoutManager. 如果您查看fillLinearLayoutManager 中的方法,则有以下代码:

if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null || !state.isPreLayout()) {
    layoutState.mAvailable -= layoutChunkResult.mConsumed;
    // we keep a separate remaining space because mAvailable is important for recycling
    remainingSpace -= layoutChunkResult.mConsumed;
}

据我了解,我们迭代子视图,直到我们填满所有需要的空间,换句话说layoutState.mAvailableremainingSpace它们都以像素为单位。如果您进一步查看layoutChunk方法中发生的事情,您将看到这段代码:


// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
    result.mIgnoreConsumed = true;
}

因此,LLM 将跳过任何具有FLAG_UPDATE在我的情况下是我调用的项目的项目notifyItemChanged。通过跳过我的意思是项目的高度不会从这两个变量中减去:

layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;

这将使循环再迭代一个视图。并且由于 LLM 没有缓存该视图(请参阅tryGetViewHolderForPositionByDeadline-> getScrapOrHiddenOrCachedHolderForPosition)(因为如果我没记错的话它在屏幕边界之外)它将被重新创建。但是如果reverseLayout设置为 false (默认 LLM 状态),它不会迭代它,因为它会首先到达列表的末尾:

while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { ... }

Namely here:

boolean hasMore(RecyclerView.State state) {
    return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
}

reverseLayout== false 的情况下,我们从当前位置开始按升序进行迭代,即:

    [ 0 ]   
 +--[ 1 ]--+
 |  [ 2 ]  |
 |  [ 3 ]  |
 |  [ 4 ]  |
 |  [ 5 ]  |
 +--[ 6 ]--+

假设我们调用notifyItemChanged位置为 3 的项目。因此 LLM 将迭代 1、2、(跳过 3)、4、5 和 6。由于它跳过了 3,因此将留下像素来填充layoutState.mAvailable变量 BUT,因为我们在循环结束它会立即停止。

现在让我们看看当reverseLayout== true 时会发生什么。

    [ 6 ]   
 +--[ 5 ]--+
 |  [ 4 ]  |
 |  [ 3 ]  |
 |  [ 2 ]  |
 |  [ 1 ]  |
 +--[ 0 ]--+

因此,我们再次调用notifyItemChanged(3). LLM 将以相反的顺序开始迭代:0、1、2、(跳过 3)、4 和 5。然后因为它跳过了 3,所以仍有像素要填充,我们不在列表的末尾,所以它将迭代 6 为出色地。

最奇怪的是,在这个代码示例中,它只能在第一次长按时重现,之后第一个屏幕外视图onBind将不会被调用。但是在发现这个东西的项目中,每次调用notifyItemChanged视图时它都是 100% 可重现的。

这是最小的可重现示例:

class MainActivity : AppCompatActivity() {
    private val id = AtomicLong(0)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val images = (0..6).map {
            return@map ImageItem(
                "https://i.imgur.com/BBcy6Wc.jpg",
                id.getAndIncrement()
            )
        }

        recycler.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, true)
        recycler.adapter = Adapter(images).apply { setHasStableIds(true) }
        recycler.setRecyclerListener { viewHolder ->
            if (viewHolder is Adapter.MyViewHolder) {
                viewHolder.onRecycle()
            }
        }
        recycler.adapter!!.notifyDataSetChanged()
    }

    data class ImageItem(val url: String, val id: Long)

    class Adapter(
        private val items: List<ImageItem>
    ) : RecyclerView.Adapter<Adapter.MyViewHolder>(), OnRecyclerItemClick {

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
            println("TTTAAA onCreateViewHolder")

            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.my_view_holder, parent, false)

            return MyViewHolder(view, this)
        }

        override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
            holder as MyViewHolder
            holder.onBind(items[position])
        }

        override fun getItemCount(): Int {
            return items.size
        }

        override fun getItemId(position: Int): Long {
            return items[position].id
        }

        override fun onLongClick(position: Int) {
            println("TTTAAA onLongClick($position)")
            notifyItemChanged(position)
        }

        class MyViewHolder(
            private val view: View,
            private val callback: OnRecyclerItemClick
        ) : RecyclerView.ViewHolder(view) {
            private val imageView: AppCompatImageView = view.findViewById(R.id.my_image)

            fun onBind(imageItem: ImageItem) {
                println("TTTAAA onBind $layoutPosition")

                imageView.setOnLongClickListener {
                    callback.onLongClick(layoutPosition)
                    return@setOnLongClickListener true
                }

                Glide.with(imageView.context)
                    .load(imageItem.url)
                    .centerCrop()
                    .into(imageView)
            }

            fun onRecycle() {
                println("TTTAAA onRecycle $layoutPosition")
                imageView.setOnClickListener(null)

                Glide.with(imageView.context)
                    .clear(imageView)
            }

        }
    }
}

interface OnRecyclerItemClick {
    fun onLongClick(position: Int)
}

这是日志:

onLongClick(0)
onCreateViewHolder
onBind 3
onCreateViewHolder
onBind 0
onRecycle 0
onLongClick(0)
onBind 0
onRecycle 0
onLongClick(0)
onBind 0
onRecycle 0

我正在长按位置 0 的视图,并且位置 3 的附加视图(在屏幕外)也被绑定。

4

0 回答 0