美文网首页
RecycleView角标越界问题分析

RecycleView角标越界问题分析

作者: 馒Care | 来源:发表于2022-06-07 09:31 被阅读0次

    1、问题如下

    java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 19(offset:19).state:20 androidx.recyclerview.widget.RecyclerView{2a4d50e VFED..... .......D 0,0-1080,1979 #7f080160 app:id/rvNewsHome}, adapter:com.zj.architecture.mainscreen.TestNewsRvAdapter@fed2b2f, layout:androidx.recyclerview.widget.LinearLayoutManager@7de53c, context:com.zj.architecture.testrv.TestRvActivity@f4dbc62

    2、模拟源码如下

    package com.zj.architecture.testrv
    
    import android.os.Bundle
    import androidx.appcompat.app.AppCompatActivity
    import com.zj.architecture.R
    import com.zj.architecture.mainscreen.TestNewsRvAdapter
    import com.zj.architecture.repository.NewsItem
    import kotlinx.android.synthetic.main.activity_main.*
    import kotlinx.coroutines.*
    
    class TestRvActivity : AppCompatActivity() {
        private var dataItem = mutableListOf<NewsItem>()
        private val newsRvAdapter by lazy {
            TestNewsRvAdapter(
                {
    
                }, dataItem
            )
        }
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main_rv)
            fabStar.setOnClickListener {
                dataItem.removeAt(0)
                GlobalScope.launch {
                    delay(3000)
                    withContext(Dispatchers.Main) {
    
                        newsRvAdapter?.notifyItemRangeRemoved(0,dataItem.size)
                        newsRvAdapter?.notifyDataSetChanged()
                    }
                }
    
    
            }
            newsRvAdapter.setHasStableIds(false)
            rvNewsHome.adapter = newsRvAdapter
    
            initData()
            srlNewsHome.setOnRefreshListener {
                initData()
                srlNewsHome.isRefreshing = false
    
            }
        }
    
        private fun initData() {
            dataItem.clear()
            for (i in 0 until 20) {
                var imageUrl = "https://t7.baidu.com/it/u=4162611394,4275913936&fm=193&f=GIF"
                if (i % 2 == 0) {
                    imageUrl = "https://t7.baidu.com/it/u=1951548898,3927145&fm=193&f=GIF"
                }
                dataItem.add(NewsItem("title$i", "descriptioni$i", imageUrl))
            }
            rvNewsHome.adapter?.notifyDataSetChanged()
        }
    }
    

    3、问题拆解

    • 以上问题主要是由于内部数据源、与外部数据源长度不一致导致的。
    • 内部数据源如下:
    class TestNewsRvAdapter(private val listener: (View) -> Unit, private var data: List<NewsItem>) :
        RecyclerView.Adapter<TestNewsRvAdapter.MyViewHolder>() {
        val TAG = "TestNewsRvAdapter"
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
            Log.d(TAG, "onCreateViewHolder: ")
            return MyViewHolder(inflate(parent.context, R.layout.item_view, parent), listener)
        }
    
        override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
            Log.d(TAG, "onBindViewHolder: ")
            holder.bind(data[position])
        }
    
        override fun getItemCount() = data.size
    
        override fun getItemId(position: Int): Long {
            return data[position].title.hashCode().toLong()
        }
        inner class MyViewHolder(override val containerView: View, listener: (View) -> Unit) :
            RecyclerView.ViewHolder(containerView),
            LayoutContainer {
    
            init {
                itemView.setOnClickListener(listener)
            }
    
            fun bind(newsItem: NewsItem) =
                with(itemView) {
                    itemView.tag = newsItem
                    tvTitle.text = newsItem.title
                    tvDescription.text = newsItem.description
                    ivThumbnail.load(newsItem.imageUrl) {
                        crossfade(true)
                        placeholder(R.mipmap.ic_launcher)
                    }
                }
        }
    
    }
    
    • 可以知道adapter内部设置的getItemCount,正是我们外部初始化传入的dataItem
    • 在Activity里面,我们对dataItem做了删除的操作。但是没有立马执行notify等操作
    • 通过源码RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline
    if (holder == null) {
                    final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                    if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
                        throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                                + "position " + position + "(offset:" + offsetPosition + ")."
                                + "state:" + mState.getItemCount() + exceptionLabel());
                    }
    
    • mAdapter.getItemCount()我们可以知道,内部数据源初始化的长度是20、由于我在Activity里面操作了删除,外部数据源dataItem长度变成了19.等待3秒后,执行notify操作。这个时候。我们执行滑动操作。基于RecycleView的复用原理。可以知道,会执行到以上源码处,进行offsetPosition判断。由于offsetPosition获取的位置是根据外部数据源决定的。所以导致了,内部跟外部数据源长度不一致。外部数据源长度19,内部数据源认为还是20.这个时候就出现了角标越界问题。

    解决方案:

    1、使用DiffUtils替代notify等操作,原理后续分析
    2、对外部数据源操作后,要及时执行notify等操作。切勿类似demo中有延迟。很多业务其实都会忽略这一点,进行了很多耗时操作后,再执行notify操作。这个会导致内部部数据源不一致的角标越界崩溃
    3、网上很多说法,自定义LinerLayoutManager,个人认为这个无效。可能因为执行顺序的原因吧。

    @Override
        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
            try {
                super.onLayoutChildren(recycler, state);
            } catch (IndexOutOfBoundsException e) {
                e.printStackTrace();
            }
        }
    

    4、网上还有很多说法,去除动画,其实,个人认为也没什么必要。角标越界,从源码分析来看,基本都是内外部数据源长度不一致导致的。动画的执行,耗时很短,类似我上面做的延迟。其实在这一点上,如果不是很花里胡哨的写了一堆动画,正常不用去掉

    rvNewsHome.animation = null
    

    5、以上4点我认为还有优化空间,基于旧业务,不太可能大改。所以,还在思考怎么处理更加合适。暂且先记录

    相关文章

      网友评论

          本文标题:RecycleView角标越界问题分析

          本文链接:https://www.haomeiwen.com/subject/wzosmrtx.html