Android RecyclerView报Inconsistency detected随机崩溃问题
问题描述
我已查阅各技术渠道的相关排查方案均未解决问题,当前应用会无规律随机触发崩溃。
异常信息如下:
Fatal Exception: java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionkj8{1734d08 position=5 id=-1, oldPos=0, pLpos:0 scrap [attachedScrap] tmpDetached no parent} androidx.recyclerview.widget.RecyclerView{5662a59 VFED..... ......ID 0,0-0,0 #7f0b0f5c app:id/signUpFormRecyclerView}, adapter:k06@73d561e, layout:androidx.recyclerview.widget.LinearLayoutManager@e8efeff, context:android.view.ContextThemeWrapper@2562378 at androidx.recyclerview.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition(RecyclerView.java:6156) at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:6339) at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6300) at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6296) at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2330) at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1631) at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1591) at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:668) at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:4309) at androidx.recyclerview.widget.RecyclerView.onMeasure(RecyclerView.java:3686) at android.view.View.measure(View.java:27131) at android.widget.RelativeLayout.measureChildHorizontal(RelativeLayout.java:735) at android.widget.RelativeLayout.onMeasure(RelativeLayout.java:481) at android.view.View.measure(View.java:27131) at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7951) at androidx.coordinatorlayout.widget.CoordinatorLayout.onMeasureChild(CoordinatorLayout.java:760) at com.google.android.material.appbar.HeaderScrollingViewBehavior.onMeasureChild(HeaderScrollingViewBehavior.java:99) at com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior.onMeasureChild(AppBarLayout.java:2003) at androidx.coordinatorlayout.widget.CoordinatorLayout.onMeasure(CoordinatorLayout.java:831) at android.view.View.measure(View.java:27131) at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7951) at android.widget.FrameLayout.onMeasure(FrameLayout.java:194) at com.Custom.consumer.ui.view.CustomFrameLayout.onMeasure(CustomFrameLayout.java:69) at android.view.View.measure(View.java:27131) at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7951) at android.widget.FrameLayout.onMeasure(FrameLayout.java:194) at android.view.View.measure(View.java:27131) at androidx.constraintlayout.widget.ConstraintLayout$Measurer.measure(ConstraintLayout.java:811) at androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.measure(BasicMeasure.java:466) at androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.measureChildren(BasicMeasure.java:134) at androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.solverMeasure(BasicMeasure.java:278) at androidx.constraintlayout.core.widgets.ConstraintWidgetContainer.measure(ConstraintWidgetContainer.java:120) at androidx.constraintlayout.widget.ConstraintLayout.resolveSystem(ConstraintLayout.java:1594) at androidx.constraintlayout.widget.ConstraintLayout.onMeasure(ConstraintLayout.java:1708) at android.view.View.measure(View.java:27131) at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7951) at android.widget.FrameLayout.onMeasure(FrameLayout.java:194) at androidx.appcompat.widget.ContentFrameLayout.onMeasure(ContentFrameLayout.java:145) at android.view.View.measure(View.java:27131) at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7951) at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1552) at android.widget.LinearLayout.measureVertical(LinearLayout.java:842) at android.widget.LinearLayout.onMeasure(LinearLayout.java:721) at android.view.View.measure(View.java:27131) at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7951) at android.widget.FrameLayout.onMeasure(FrameLayout.java:194) at android.view.View.measure(View.java:27131) at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7951) at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1552) at android.widget.LinearLayout.measureVertical(LinearLayout.java:842) at android.widget.LinearLayout.onMeasure(LinearLayout.java:721) at android.view.View.measure(View.java:27131) at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7951) at android.widget.FrameLayout.onMeasure(FrameLayout.java:194) at com.android.internal.policy.DecorView.onMeasure(DecorView.java:1175) at android.view.View.measure(View.java:27131) at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java:4187) at android.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java:2936) at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3204) at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2618) at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:9971) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1010) at android.view.Choreographer.doCallbacks(Choreographer.java:809) at android.view.Choreographer.doFrame(Choreographer.java:744) at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:995) at android.os.Handler.handleCallback(Handler.java:938) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:246) at android.app.ActivityThread.main(ActivityThread.java:8633) at java.lang.reflect.Method.invoke(Method.java) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)
当前页面通过ViewStub懒加载包含id为signUpFormRecyclerView的注册表单布局,列表数据变更时通过LiveData观察者回调触发notifyDataSetChanged、notifyItemChanged方法刷新。
相关代码
Fragment 代码
class OnBoardingFragment : BaseFragment() { private val onBoardingConfig: OnBoardingFragmentInitConfig by lazy { requireArguments().getParcelable(Keys.INIT_CONFIG)!! } private val adapterItemList: ArrayList<EditViewType> = ArrayList() private val viewViewModel: OnBoardingViewModel by fragmentViewModelBuilder { OnBoardingViewModel( onBoardingConfig, adapterItemList, onBoardingReferralPresenter ) } private val onBoardingReferralPresenter: OnBoardingReferralPresenter by lazy { OnBoardingReferralPresenter(authNavigatorV2) } private val adapter by lazy { ProfileAdapter( adapterItemList, viewViewModel, lifecycleScope ) } private lateinit var bindingCollapse: DesignCollapsingtoolbarBinding private lateinit var binding: DesignSignupOnboardingBinding override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { bindingCollapse = DesignCollapsingtoolbarBinding.inflate(inflater, container, false) return bindingCollapse.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) onBoardingReferralPresenter.setReferralPresenterToViewModelCallBack(viewViewModel) initView() if (null == savedInstanceState) viewViewModel.startViewModel() } private fun initView() { setUpViewStub() setUpRecyclerView() startObservers() } private fun setUpViewStub() { bindingCollapse.commonHeaderViewstub.viewStub?.let { it.layoutResource = R.layout.design_signup_onboarding it.setOnInflateListener { _, inflated -> val stubBinding = DataBindingUtil.bind<DesignSignupOnboardingBinding>(inflated) if (null == stubBinding) { requireActivity().onBackPressed() return@setOnInflateListener } binding = stubBinding } it.inflate() } } private fun startObservers() { viewViewModel.loadAdapterItemList.observe(viewLifecycleOwner) { adapter.notifyDataSetChanged() } viewViewModel.notifyPosition.observe(viewLifecycleOwner) { adapter.notifyItemChanged(it) } } private fun setUpRecyclerView() { with(binding.signUpFormRecyclerView) { layoutManager = LinearLayoutManagerWrapper(context, LinearLayoutManager.VERTICAL, false) itemAnimator = null adapter = this@OnBoardingFragment.adapter } } companion object { private const val SOURCE = "source" private const val DIALOG_ALERT = 9899 const val SCREEN_NAME = GaScreen.ON_BOARDING_PAGE fun getInstance( onBoardingInitModel: OnBoardingFragmentInitConfig?, source: Int, ): OnBoardingFragment { val fragment = OnBoardingFragment() val bundle = Bundle() bundle.putInt(SOURCE, source) bundle.putParcelable(Keys.INIT_CONFIG, onBoardingInitModel) fragment.arguments = bundle return fragment } } }
XML 布局代码
design_collapsingtoolbar.xml
<?xml version="1.0" encoding="utf-8"?> <CusotmFrameLayout android:id="@+id/commonHeaderRoot" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.coordinatorlayout.widget.CoordinatorLayout android:id="@+id/commonHeaderCoordinator" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/transparent"> <include android:id="@+id/comonHeaderCollapsingToolbar" layout="@layout/collapsing_toolbar" /> <ViewStub android:id="@+id/commonHeaderViewstub" android:layout_width="match_parent" android:layout_height="match_parent" android:inflatedId="@+id/commonHeaderInflatedView" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> </androidx.coordinatorlayout.widget.CoordinatorLayout> <CustomTextView android:id="@+id/commonHeaderRightItem" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end"/> </CusotmFrameLayout>
design_signup_onboarding.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:theme="@style/AppTheme.MaterialComponents"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/signUpFormRecyclerView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_above="@+id/signupCreateAccount" /> <CustomButtonView android:id="@+id/signupCreateAccount" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_above="@+id/signupTermsCondition" tools:text="Create Account" /> <com.google.android.material.checkbox.MaterialCheckBox android:id="@+id/signupTermsCondition" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" /> </RelativeLayout>
崩溃根因
这是RecyclerView典型的内部缓存状态与数据源不一致崩溃,从堆栈可以看到崩溃发生在RecyclerView测量流程的ViewHolder合法性校验环节,具体触发原因有3个:
- 数据源操作无约束:
adapterItemList作为Adapter的数据源直接传给ViewModel,没有任何线程和边界保护。如果子线程直接修改列表内容、或者列表删减后还收到旧位置的notifyItemChanged回调(比如列表长度已经小于5,还收到刷新position=5的通知),会直接导致RecyclerView缓存的ViewHolder位置和当前数据长度不匹配,触发校验异常。 - 初始化时序存在竞态:原代码
initView顺序为初始化ViewStub、初始化RecyclerView、注册观察者,ViewModel在initView执行完成后立刻启动,数据回调速度快于ViewStub的DataBinding绑定、RecyclerView挂载完成的速度时,会出现RecyclerView还没完成Adapter绑定就收到刷新通知的情况,首次测量时会拿到错误的缓存状态。 - 布局测量触发重复计算:RecyclerView设置了
layout_height="wrap_content",在CoordinatorLayout+AppBarLayout的嵌套滚动体系下,wrap_content会触发多次重复测量,测量过程中如果数据刚好发生变更,RecyclerView的attachedScrap缓存里保留的旧ViewHolder位置信息和新数据不匹配,就会触发崩溃。
修复方案
- 修复数据通知逻辑:所有对
adapterItemList的增删改操作必须切换到主线程执行,禁止子线程直接修改列表。给notifyPosition回调增加边界校验,避免无效位置刷新:
viewViewModel.notifyPosition.observe(viewLifecycleOwner) { pos -> if (pos in 0 until adapter.itemCount) { adapter.notifyItemChanged(pos) } else { adapter.notifyDataSetChanged() } }
- 调整初始化时序:把
startObservers放到initView的最前面,把setUpRecyclerView的调用移到ViewStub的inflate回调内部,保证RecyclerView完成绑定和挂载后再接收数据通知:
private fun initView() { startObservers() setUpViewStub() } private fun setUpViewStub() { bindingCollapse.commonHeaderViewstub.viewStub?.let { it.layoutResource = R.layout.design_signup_onboarding it.setOnInflateListener { _, inflated -> val stubBinding = DataBindingUtil.bind<DesignSignupOnboardingBinding>(inflated) if (null == stubBinding) { requireActivity().onBackPressed() return@setOnInflateListener } binding = stubBinding // ViewStub加载完成、binding赋值后再初始化RecyclerView setUpRecyclerView() } it.inflate() } }
- 修复布局测量问题:把RecyclerView的
layout_height从wrap_content改成0dp,利用RelativeLayout的相对布局规则自动计算高度,避免wrap_content带来的重复测量:
<androidx.recyclerview.widget.RecyclerView android:id="@+id/signUpFormRecyclerView" android:layout_width="match_parent" android:layout_height="0dp" android:layout_alignParentTop="true" android:layout_above="@+id/signupCreateAccount" />
- 兜底加固:自定义
LinearLayoutManagerWrapper重写onLayoutChildren方法,捕获不一致异常做兜底刷新,避免极端场景崩溃:
class LinearLayoutManagerWrapper : LinearLayoutManager { // 补全对应构造方法 override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) { try { super.onLayoutChildren(recycler, state) } catch (e: IndexOutOfBoundsException) { // 捕获状态不一致异常,兜底全量刷新 } } }
内容的提问来源于stack exchange,提问作者Vikram Singh
相关产品推荐
相关产品推荐

