在某些设备中使用分页获取 IndexOutOfBoundsException 的房间实时数据

Jor*_*oza 5 android kotlin android-room android-paging

我正在使用带有 Room livedata 的分页库,在某些设备中,有时我会收到 IndexOutOfBoundsException 索引越界 - 传递位置 = 75,旧列表大小 = 75(总是大小为 75,我的数据库都没有固定大小为 75)

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

致命异常:java.lang.IndexOutOfBoundsException:索引越界 - 传递位置 = 75,旧列表大小 = 75

在androidx.recyclerview.widget.DiffUtil $ DiffResult.convertOldPositionToNew(DiffUtil.java:672)
在androidx.paging.PagedStorageDiffHelper.transformAnchorIndex(PagedStorageDiffHelper.java:215)
在androidx.paging.AsyncPagedListDiffer.latchPagedList(AsyncPagedListDiffer.java:382)
在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)
在 android.app.ActivityThread.main(ActivityThread.java:6543)
在 java.lang.reflect.Method.invoke(Method.java)
在 com.android.internal.os。 RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:440)
在 com.android.internal.os.ZygoteInit.main(ZygoteInit.java:810)

有解决方法吗?还是与我的应用程序有关?

Ahm*_* na 8

旧答案(推荐 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()
Run Code Online (Sandbox Code Playgroud)

所以只是设置setEnablePlaceholders(false)应该避免这种情况发生,注意这会影响滚动,导致它在加载更多数据时闪烁,直到从 android sdk 修复。

UPDATED 1 (not recommended, see update 2):
The issue happens mainly when restoring a destroyed activity / fragment for my case i have 4 fragments, I was refreshing all the lists at once after restoring, meaning i insert new data causing the observable to be called multiple times causing the call of mDiffer.submitList to be called multiple, by right this should not crash as mDiffer do all the work in the background but my guess that there is a synchronisation issue between the mDiffer Runnables

so my solution was to minimizethe mDiffer.submitList calls by the following methods :

  • save/restore items tab fragments:

    public override fun onSaveInstanceState(savedInstanceState: Bundle) { super.onSaveInstanceState(savedInstanceState) supportFragmentManager.putFragment(savedInstanceState, "itemsFragment1", itemsFragment1) } public override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) newsMainFragment = supportFragmentManager.getFragment(savedInstanceState, "itemsFragment1") as ItemsFragment1? }

  • Make sure only one list to be submit at time (ignoring too much changes and keep last one only):

      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)
              }
          }
        }
      } 
    
    Run Code Online (Sandbox Code Playgroud)

UPDATED 2 (fix but not recommended, see update 3):

This fix will catch the error not prevent the issue from happening as it is related to the Handler Class from the Android SDK when trying to call Handler.createAsync some old devices with old sdk will create synchronised handler which will lead to the crash

In your adapter that exptend PagedListAdapter add the following :

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)
    }
Run Code Online (Sandbox Code Playgroud)

Also if you are using pro-guard or R8 add the following rules:

-keep class androidx.paging.PagedListAdapter.** { *; }
-keep class androidx.paging.AsyncPagedListDiffer.** { *; }
Run Code Online (Sandbox Code Playgroud)

ofc this is a work around so when updating the sdk be careful with the naming does not change for the reflection .

UPDATED 3 (14/2/2020):

use :

 setEnablePlaceholders(false)
Run Code Online (Sandbox Code Playgroud)

but after updating paging library to :

 implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
Run Code Online (Sandbox Code Playgroud)

the flash issue got fixed