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

标签 android kotlin android-room android-paging

我正在使用带 Room 实时数据的分页库,在某些设备中,有时我会收到 IndexOutOfBoundsException 索引超出范围 - 传递位置 = 75,旧列表大小 = 75(大小始终为 75,我的数据库都没有固定大小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$2$1.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/fragment 时 对于我的情况,我有 4 个 fragment ,我在恢复后立即刷新所有列表,这意味着我插入新数据导致多次调用 observable 导致 mDiffer.submitList 的调用被多次调用,按理说这不应该崩溃,因为 mDiffer 在后台完成所有工作 但我猜测 mDiffer Runnable 之间存在同步问题

所以我的解决方案是通过以下方法最小化 mDiffer.submitList 调用:

  • 保存/恢复项目选项卡 fragment :

    public override fun onSaveInstanceState(savedInstanceState: Bundle) { super.onSaveInstanceState(保存实例状态) supportFragmentManager.putFragment(savedInstanceState, “itemsFragment1”,itemsFragment1) } 公共(public)覆盖乐趣 onRestoreInstanceState(savedInstanceState: bundle ) { 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):

此修复将捕获错误,但不会阻止问题的发生,因为它与尝试调用 Handler.createAsync 时来自 Android SDK 的处理程序类有关,一些具有旧 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'

修复了闪光问题

关于android - 带有分页的房间实时数据在某些设备中出现 IndexOutOfBoundsException,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55546116/

相关文章:

android - channel 或 mutablesharedflow ,哪个是已弃用的 localbroadcastmanager 的更好替代品

rx-java - 我可以创建一个 Kotlin 扩展方法来将 rxJava 订阅添加到 CompositeSubscription 吗?

Android - 多数据库一个项目

android - 从文件加载数据库

android - ACTION_IMAGE CAPTURE(从第三方应用拍照)传递结果异常

javascript - Android 的 HTML5 应用程序可以在主屏幕上有一个小部件吗?

android - Keytool 不要求输入别名密码

android chrome html5 实时音频在 5 分钟后停止播放

android - 电话身份验证在 Firebase 上创建新帐户并断开链接

具有延迟外键的 Android Room