带有分页的房间实时数据在某些设备中出现 IndexOutOfBoundsException

Room livedata with paging getting IndexOutOfBoundsException in some devices

我正在使用带 Room 实时数据的分页库,在某些设备中,有时我会收到 IndexOutOfBoundsException 索引超出范围 - 传递位置 = 75,旧列表大小 = 75(大小始终为 75,我的 none数据库的固定大小为 75)

问题是日志显示更像是内部错误而不是我的应用程序错误

Fatal Exception: java.lang.IndexOutOfBoundsException: Index out of bounds - passed position = 75, old list size = 75

at androidx.recyclerview.widget.DiffUtil$DiffResult.convertOldPositionToNew(DiffUtil.java:672)
at androidx.paging.PagedStorageDiffHelper.transformAnchorIndex(PagedStorageDiffHelper.java:215)
at androidx.paging.AsyncPagedListDiffer.latchPagedList(AsyncPagedListDiffer.java:382)
at androidx.paging.AsyncPagedListDiffer.run(AsyncPagedListDiffer.java:345)
at android.os.Handler.handleCallback(Handler.java:790)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6543)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:440)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:810)

有解决办法吗?或者与我的应用有关?

旧答案(推荐 14/2/2020):

经过多天调查导致此错误的选项是:

protected val pagedListConfig: PagedList.Config = PagedList.Config.Builder()
        .setEnablePlaceholders(true) . <------------ set to false 
        .setPrefetchDistance(PREFETCH_DISTANCE_SIZE)
        .setPageSize(DATABASE_PAGE_SIZE)
        .setInitialLoadSizeHint(INITIAL_DATABASE_PAGE_SIZE)
        .build()

所以只需设置 setEnablePlaceholders(false) 就可以避免这种情况发生 请注意,这会影响滚动,导致它在加载更多数据时闪烁,直到 android sdk.

修复为止

更新 1(不推荐,见更新 2):
该问题主要发生在恢复已损坏的 activity / 片段时 对于我的情况,我有 4 个片段,我在恢复后立即刷新所有列表,这意味着我插入新数据导致多次调用 observable 导致 mDiffer.submitList 的调用被多次调用,按理说这不应该崩溃,因为 mDiffer 在后台完成所有工作 但我猜测 mDiffer Runnables

之间存在同步问题

所以我的解决方案是通过以下方法最大限度地减少 mDiffer.submitList 调用:

  • save/restore 项选项卡片段:

    public override fun onSaveInstanceState(savedInstanceState: Bundle) { super.onSaveInstanceState(已保存实例状态) supportFragmentManager.putFragment(savedInstanceState, “itemsFragment1”,itemsFragment1) } public 覆盖乐趣 onRestoreInstanceState(savedInstanceState: 捆) { super.onRestoreInstanceState(已保存实例状态) newsMainFragment = supportFragmentManager.getFragment(savedInstanceState, “itemsFragment1”)作为 ItemsFragment1? }

  • 确保一次只提交一个列表(忽略太多更改,只保留最后一个):

      abstract class MyPagedListAdapter<T, VH : RecyclerView.ViewHolder> 
       (diffCallback: DiffUtil.ItemCallback<T>) : PagedListAdapter<T, VH> 
      .(diffCallback) {
       var submitting = false
       val latestPage: Queue<PagedList<T>?> = LinkedList()
      val runnable = Runnable {
          synchronized(this) {
              submitting = false
              if (latestPage.size > 0) {
                  submitFromLatest()
              }
          }
      }
    
      override fun submitList(pagedList: PagedList<T>?) {
          synchronized(this) {
              if(latestPage.size > 0){
                  Log.d("MyPagedListAdapter","ignored ${latestPage.size}")
              }
              latestPage.clear()
              latestPage.add(pagedList)
              submitFromLatest()
    
          }
      }
    
      fun submitFromLatest() {
          synchronized(this) {
              if (!submitting) {
                  submitting = true
                  if (latestPage.size > 1 || latestPage.size < 1) {
                      Crashlytics.logException(Throwable("latestPage size is ${latestPage.size}"))
                      Log.d("MyPagedListAdapter","latestPage size is ${latestPage.size}")
                  }
                  super.submitList(latestPage.poll(), runnable)
              }
          }
        }
      } 
    

更新 2(修复但不推荐,参见更新 3):

此修复将捕获错误,但不会阻止问题的发生,因为它与 Android SDK 中的处理程序 Class 有关,当尝试调用 Handler.createAsync 一些旧设备时旧的 SDK 将创建同步处理程序,这将导致崩溃

在扩展 PagedListAdapter 的适配器中添加以下内容:

init {
  
    try {
        val mDiffer = PagedListAdapter::class.java.getDeclaredField("mDiffer")
        val excecuter = AsyncPagedListDiffer::class.java.getDeclaredField("mMainThreadExecutor")
        mDiffer.isAccessible = true
        excecuter.isAccessible = true

        val myDiffer = mDiffer.get(this) as AsyncPagedListDiffer<*>
        val foreGround = object : Executor {
            val mHandler = createAsync(Looper.getMainLooper())
            override fun execute(command: Runnable?) {
                try {
                    mHandler.post {
                        try {
                            command?.run()
                        } catch (e: Exception) {
                            e.printStackTrace()
                        }
                    }
                } catch (e: Exception) {
                    e.printStackTrace()
                }
            }
        }


        excecuter.set(myDiffer, foreGround)
    } catch (e: Exception) {
        e.printStackTrace()
    }

}

private fun createAsync(looper: Looper): Handler {
        if (Build.VERSION.SDK_INT >= 28) {
            return Handler.createAsync(looper)
        }
        if (Build.VERSION.SDK_INT >= 16) {
            try {
                return Handler::class.java.getDeclaredConstructor(Looper::class.java, Handler.Callback::class.java,
                        Boolean::class.javaPrimitiveType)
                        .newInstance(looper, null, true)
            } catch (ignored: IllegalAccessException) {
            } catch (ignored: InstantiationException) {
            } catch (ignored: NoSuchMethodException) {
            } catch (e: InvocationTargetException) {
                return Handler(looper)
            }

        }
        return Handler(looper)
    }

此外,如果您使用的是 pro-guard 或 R8,请添加以下规则:

-keep class androidx.paging.PagedListAdapter.** { *; }
-keep class androidx.paging.AsyncPagedListDiffer.** { *; }

ofc 这是一种解决方法,因此在更新 sdk 时请注意命名不会因反射而改变。

更新 3 (14/2/2020):

使用:

 setEnablePlaceholders(false)

但是在将分页库更新为 :

之后
 implementation 'androidx.paging:paging-runtime-ktx:2.1.2'

Flash 问题已解决