带有分页的房间实时数据在某些设备中出现 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 问题已解决
我正在使用带 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 问题已解决