RecycleView角標(biāo)越界問題分析

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、問題拆解

  • 以上問題主要是由于內(nèi)部數(shù)據(jù)源、與外部數(shù)據(jù)源長度不一致導(dǎo)致的。
  • 內(nèi)部數(shù)據(jù)源如下:
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內(nèi)部設(shè)置的getItemCount,正是我們外部初始化傳入的dataItem
  • 在Activity里面,我們對dataItem做了刪除的操作。但是沒有立馬執(zhí)行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()我們可以知道,內(nèi)部數(shù)據(jù)源初始化的長度是20、由于我在Activity里面操作了刪除,外部數(shù)據(jù)源dataItem長度變成了19.等待3秒后,執(zhí)行notify操作。這個時候。我們執(zhí)行滑動操作?;?a href="http://www.itdecent.cn/p/253fb64bfa22" target="_blank">RecycleView的復(fù)用原理??梢灾?,會執(zhí)行到以上源碼處,進(jìn)行offsetPosition判斷。由于offsetPosition獲取的位置是根據(jù)外部數(shù)據(jù)源決定的。所以導(dǎo)致了,內(nèi)部跟外部數(shù)據(jù)源長度不一致。外部數(shù)據(jù)源長度19,內(nèi)部數(shù)據(jù)源認(rèn)為還是20.這個時候就出現(xiàn)了角標(biāo)越界問題。

解決方案:

1、使用DiffUtils替代notify等操作,原理后續(xù)分析
2、對外部數(shù)據(jù)源操作后,要及時執(zhí)行notify等操作。切勿類似demo中有延遲。很多業(yè)務(wù)其實(shí)都會忽略這一點(diǎn),進(jìn)行了很多耗時操作后,再執(zhí)行notify操作。這個會導(dǎo)致內(nèi)部部數(shù)據(jù)源不一致的角標(biāo)越界崩潰
3、網(wǎng)上很多說法,自定義LinerLayoutManager,個人認(rèn)為這個無效??赡芤?yàn)閳?zhí)行順序的原因吧。

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

4、網(wǎng)上還有很多說法,去除動畫,其實(shí),個人認(rèn)為也沒什么必要。角標(biāo)越界,從源碼分析來看,基本都是內(nèi)外部數(shù)據(jù)源長度不一致導(dǎo)致的。動畫的執(zhí)行,耗時很短,類似我上面做的延遲。其實(shí)在這一點(diǎn)上,如果不是很花里胡哨的寫了一堆動畫,正常不用去掉

rvNewsHome.animation = null

5、以上4點(diǎn)我認(rèn)為還有優(yōu)化空間,基于舊業(yè)務(wù),不太可能大改。所以,還在思考怎么處理更加合適。暫且先記錄

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容