RecyclerView异常之getSpaceForSpanRange越界

92 阅读2分钟

版本:recyclerView1.3.0

RecyclerView越界异常:getSpaceForSpanRange导致的ArrayIndexOutOfBoundsException,这个异常并不常见 java.lang.ArrayIndexOutOfBoundsException: length=5; index=5 at androidx.recyclerview.widget.GridLayoutManager.getSpaceForSpanRange(GridLayoutManager.java:364)

详细日志:只是Android源码里的异常,并未打印出自己业务逻辑代码那里出现异常。

java.lang.ArrayIndexOutOfBoundsException: length=5; index=5 at androidx.recyclerview.widget.GridLayoutManager.getSpaceForSpanRange(GridLayoutManager.java:364) at androidx.recyclerview.widget.GridLayoutManager.measureChild(GridLayoutManager.java:744) at androidx.recyclerview.widget.GridLayoutManager.layoutChunk(GridLayoutManager.java:619) at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1622) at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:687) at androidx.recyclerview.widget.GridLayoutManager.onLayoutChildren(GridLayoutManager.java:182) at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:4604) at androidx.recyclerview.widget.RecyclerView.onMeasure(RecyclerView.java:3981) at android.view.View.measure(View.java:23608) at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6960) at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1535) at android.widget.LinearLayout.measureHorizontal(LinearLayout.java:1187) at android.widget.LinearLayout.onMeasure(LinearLayout.java:706) at android.view.View.measure(View.java:23608) at android.widget.FrameLayout.onMeasure(FrameLayout.java:254) at android.view.View.measure(View.java:23608) at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6960) at android.widget.FrameLayout.onMeasure(FrameLayout.java:185) at android.view.View.measure(View.java:23608) at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6960) at android.widget.FrameLayout.onMeasure(FrameLayout.java:185) at android.view.View.measure(View.java:23608)

经过多方排查并未找到原因。于是问下GPT如何复现上面问题。经过多方修改,终于可以复现demo。

class MainActivity2 : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main2)
        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
        val spanCount = 4 // Set the span count to 5 (crucial!)
        val layoutManager = MyGrideManager(this, spanCount)
        recyclerView.layoutManager = layoutManager

        // 创建一个会导致错误的 SpanSizeLookup
        layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
            override fun getSpanSize(position: Int): Int {
                // 通过为最后一个项目返回无效的跨度大小来模拟错误
                Log.d(TAG, "getSpanSize position:$position")
                return when (position) {
                    3 -> 2  // 特殊跨度项
                    else -> 1
                }
            }

            override fun getSpanIndex(position: Int, spanCount: Int): Int {
                Log.d(TAG, "getSpanIndex position:$position")
                // Alternate the starting column between 0 and 3
                return when (position) {
                    3 -> spanCount - 1 // 关键修改:起始位置设为最大值 4-1=3--》
                    else -> 0
                }
            }
        }

        val data = (0..15).map { "Item $it" }.toMutableList()
        val adapter = MyAdapter(data)
        recyclerView.adapter = adapter
        // 添加布局后动态修改参数
        recyclerView.post {
            // 制造测量冲突
            recyclerView.layoutParams.width = 0 // 强制无效测量
        }
    }
}

class MyAdapter(private val data: MutableList<String>) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {

    class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val textView: TextView = itemView.findViewById(android.R.id.text1)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val itemView = LayoutInflater.from(parent.context).inflate(android.R.layout.simple_list_item_1, parent, false)
        return MyViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.textView.text = data[position]
    }

    override fun getItemCount(): Int {
        return data.size
    }
}
<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:layout_marginStart="50dp"/>

通过上面可以手动上下滑动模拟出上面异常。然后重写GridLayoutManager的getSpaceForSpanRange方法来解决越界问题。

class MyGridLayoutManager(
    context: Context,
    spanCount: Int
) : GridLayoutManager(context, spanCount) {

    /**
     * java.lang.ArrayIndexOutOfBoundsException: length=5; index=5
     * at androidx.recyclerview.widget.GridLayoutManager.getSpaceForSpanRange(GridLayoutManager.java:364)
     * 版本:recyclerView1.3.0
     */
    override fun getSpaceForSpanRange(startSpan: Int, spanSize: Int): Int {
        var range = 0
        try {
            range = super.getSpaceForSpanRange(startSpan, spanSize)
        } catch (e: Exception) {
            if (mOrientation == VERTICAL && isLayoutRTL()) {
                return mCachedBorders[mSpanCount - startSpan]
                -mCachedBorders[mSpanCount - startSpan - spanSize]
            } else {
                val cacheSize = mCachedBorders.size
                var index1 = startSpan + spanSize
                if (index1 >= cacheSize) {
                    index1 = cacheSize - 1
                    if (index1 > startSpan) {
                        range = mCachedBorders[index1] - mCachedBorders[startSpan]
                    }
                } else {
                    range = mCachedBorders[index1] - mCachedBorders[startSpan]
                }
                return range
            }
        }
        return range
    }
}

上面是解决思路,为什么会这样解决呢?因为没有找到线上代码具体异常点。无论怎样测试都是正常的,只有在线上海量用户出现过,并且无论怎么处理代码都无法解决。于是想出这个方案。特别对于比较复杂的项目,防止闪退是第一要务,只要不闪退,下次更新GridLayoutManager的时候就自然就正常了。这种方案特别适合频繁更新布局异常的情况。但不是最终解决办法,等找到原因再补充...