java.lang.IllegalArgumentException: No view found for id 崩溃总结

出现崩溃

项目在发布前测试测出一个偶现崩溃,起初因为无法复现,就直接带 bug 上线了,灰度后有少量的上报,评估后不影响放量,决定直接放开全量。第二天就收到了告警,线上出现了大量的相同的崩溃,崩溃堆栈如下:

03-06 20:34:50.224 13790 13790 D AndroidRuntime: Shutting down VM
03-06 20:34:50.225 13790 13790 E AndroidRuntime: FATAL EXCEPTION: main
03-06 20:34:50.225 13790 13790 E AndroidRuntime: Process: com.freeman.test.application, PID: 13790
03-06 20:34:50.225 13790 13790 E AndroidRuntime: java.lang.IllegalArgumentException: No view found for id 0x7f0801b5 (com.freeman.test.application:id/test_fragment_container_cl) for fragment TestFragment{6fe966e} (9df3ca82-4248-4a56-a1dc-7a250159e6ec) id=0x7f0801b5 TestFragment}
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:315)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1187)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1356)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.fragment.app.FragmentManager.moveFragmentToExpectedState(FragmentManager.java:1434)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1497)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:447)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.fragment.app.FragmentManager.executeOps(FragmentManager.java:2169)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1992)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1947)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.fragment.app.FragmentManager.execSingleAction(FragmentManager.java:1818)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.fragment.app.BackStackRecord.commitNowAllowingStateLoss(BackStackRecord.java:303)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at com.freeman.test.application.crash.TestActivity$FragmentHolder.update(TestActivity.kt:89)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at com.freeman.test.application.crash.TestActivity$TestAdapter.onBindViewHolder(TestActivity.kt:60)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.recyclerview.widget.RecyclerView$Adapter.onBindViewHolder(RecyclerView.java:7065)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.recyclerview.widget.RecyclerView$Adapter.bindViewHolder(RecyclerView.java:7107)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.recyclerview.widget.RecyclerView$Recycler.tryBindViewHolderByDeadline(RecyclerView.java:6012)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:6279)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6118)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6114)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2303)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1627)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1587)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:665)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:4134)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3851)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:4404)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.View.layout(View.java:23109)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.ViewGroup.layout(ViewGroup.java:6460)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.constraintlayout.widget.ConstraintLayout.onLayout(ConstraintLayout.java:1762)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.View.layout(View.java:23109)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.ViewGroup.layout(ViewGroup.java:6460)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.View.layout(View.java:23109)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.ViewGroup.layout(ViewGroup.java:6460)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at androidx.appcompat.widget.ActionBarOverlayLayout.onLayout(ActionBarOverlayLayout.java:530)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.View.layout(View.java:23109)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.ViewGroup.layout(ViewGroup.java:6460)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.View.layout(View.java:23109)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.ViewGroup.layout(ViewGroup.java:6460)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.widget.LinearLayout.onLayout(LinearLayout.java:1582)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.View.layout(View.java:23109)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.ViewGroup.layout(ViewGroup.java:6460)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at com.android.internal.policy.DecorView.onLayout(DecorView.java:797)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.View.layout(View.java:23109)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.ViewGroup.layout(ViewGroup.java:6460)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:3625)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3084)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2074)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8507)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1077)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.Choreographer.doCallbacks(Choreographer.java:897)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.Choreographer.doFrame(Choreographer.java:826)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1062)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.os.Handler.handleCallback(Handler.java:938)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.os.Handler.dispatchMessage(Handler.java:99)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.os.Looper.loop(Looper.java:233)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at android.app.ActivityThread.main(ActivityThread.java:7892)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at java.lang.reflect.Method.invoke(Native Method)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
03-06 20:34:50.225 13790 13790 E AndroidRuntime: 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)

分析崩溃

从崩溃堆栈 log 大概意思是找不到一个 id0x7f0801b5 的容器去承载 TestFragment,从代码上看

itemView.findViewById<ConstraintLayout>(R.id.test_fragment_container_cl)?.let {
    fragmentManager.beginTransaction()
        .replace(R.id.test_fragment_container_cl, TestFragment(), "TestFragment")
        .commitNowAllowingStateLoss()
}

就是将 Fragment 添加到容器上的一次常规简单操作,在这里就得到第一个疑惑:

在找不到 R.id.test_fragment_container_cl 这个容器添加 TestFragment 之前已经有个 itemView.findViewById<ConstraintLayout>(R.id.test_fragment_container_cl) 的调用,并且已经找到这个 View,不然就不会有接下来的 Fragment 的添加操作

这说明 Fragment 的容器对象是已经创建成功,并且是可以索引到的。接下去看报错的具体代码

    // FragmentStateManager#createView(FragmentContainer)

    void createView(@NonNull FragmentContainer fragmentContainer) {
        // ... 代码省略
        ViewGroup container = null;
        if (mFragment.mContainer != null) {
            container = mFragment.mContainer;
        } else if (mFragment.mContainerId != 0) {
            if (mFragment.mContainerId == View.NO_ID) {
                throw new IllegalArgumentException("Cannot create fragment " + mFragment
                        + " for a container view with no id");
            }
            container = (ViewGroup) fragmentContainer.onFindViewById(mFragment.mContainerId);
            if (container == null && !mFragment.mRestored) {
                String resName;
                try {
                    resName = mFragment.getResources().getResourceName(mFragment.mContainerId);
                } catch (Resources.NotFoundException e) {
                    resName = "unknown";
                }
                throw new IllegalArgumentException("No view found for id 0x"
                        + Integer.toHexString(mFragment.mContainerId) + " ("
                        + resName + ") for fragment " + mFragment);
            }
        }
        // ... 代码省略
        }
    }

从源码上看是由于 container = (ViewGroup) fragmentContainer.onFindViewById(mFragment.mContainerId); fragmentContainer 找不到 mContainerId 导致的崩溃,而 mContainerId 则就是 Fragment 中的容器 id,在这个崩溃中就是 R.id.test_fragment_container_cl。接下来看 fragmentContainer 为什么会找不到这个 id 对应的容器。

可以看到 fragmentContainer 是一个局部变量,由 FragmentManager#moveToState 中传入

    // FragmentManager#moveToState

    void moveToState(@NonNull Fragment f, int newState) {
        // ...代码省略
        newState = Math.min(newState, fragmentStateManager.computeMaxState());
        if (f.mState <= newState) {
            // ...代码省略
            switch (f.mState) {
                // ...代码省略
                case Fragment.CREATED:
                    // ...代码省略

                    if (newState > Fragment.CREATED) {
                        // mContainer 是 FragmentManager 的全局变量
                        fragmentStateManager.createView(mContainer);
                        fragmentStateManager.activityCreated();
                        fragmentStateManager.restoreViewState();
                    }
                 // ...代码省略
            }
        } else if (f.mState > newState) {
            // ...代码省略
        }
        // ...代码省略
    }

可以确定 mContainer 并不是一个 null 值,通过查看 Fragment 源码,可以确认最终 mContainer 其实是一个 HostCallbacks 对象,HostCallbacksFragmentActivity 的一个内部类,回到前面 container = (ViewGroup) fragmentContainer.onFindViewById(mFragment.mContainerId); 对应的代码则为:

@Nullable
@Override
public View onFindViewById(int id) {
    return FragmentActivity.this.findViewById(id);
}

本质上就是一个简单的 findViewById 的调用,这就说明 R.id.test_fragment_container_cl 这个 View 还没有依附到 Activity 所在的 view hierarchy 上面。而由于 R.id.test_fragment_container_cl 所在是一个 RecyclerView 的一个 ViewHolder,就可以联想到 ViewHolder 的创建和绑定。

事实上,在执行完 Adapter 的 onCreateViewHolderonBindViewHolder 后,在 ViewHolder 中的 itemView 确实是可以通过 findViewById 找到 itemView 自身的 child, 但并不能确保 ViewHolder 中的 View 已经被添加到了 Activity 所在的 view hierarchy 中,真正被依附是在执行完 Adapter 的 onViewAttachedToWindow。通过 Demo 也可以简单证明这个逻辑:

adapterCallLog.png

到这就可以知道在 onBindViewHolder 执行一些 Fragment 的添加删除是一个及其危险的事情,在我写的 Demo 中是一个必现的崩溃

崩溃代码

    private class FragmentHolder(view: View) : RecyclerView.ViewHolder(view) {
        // 在 onBindViewHolder 中立刻更新 ViewHolder,将 ViewHolder 中的 itemView 作为容器去添加一个 Fragment。出现必现崩溃
        fun update(fragmentManager: FragmentManager) {
            itemView.findViewById<ConstraintLayout>(R.id.test_fragment_container_cl)?.let {
                fragmentManager.beginTransaction()
                    .replace(R.id.test_fragment_container_cl, TestFragment(), "TestFragment")
                    .commitNowAllowingStateLoss()
            }
        }
    }

崩溃路径Log
crash.png

而线上却没有必现,那是因为线上需要等待服务端数据返回,在异步获得数据前,数据为 null,并不会触发 ViewHolder 的更新,此时改 ViewHolder 已经执行过 onViewAttachedToWindow,通过模拟延迟更新看起来能规避调崩溃

    private class FragmentHolder(view: View) : RecyclerView.ViewHolder(view) {
        // 在 onBindViewHolder 中延迟更新 ViewHolder,此时改 ViewHolder 已经被执行完 onViewAttachedToWindow
        fun update(fragmentManager: FragmentManager) {
            itemView.postDelayed({
                itemView.findViewById<ConstraintLayout>(R.id.test_fragment_container_cl)?.let {
                    fragmentManager.beginTransaction()
                        .replace(R.id.test_fragment_container_cl, TestFragment(), "TestFragment")
                        .commitNowAllowingStateLoss()
                }
            }, 1000) // 延迟一秒
        }
    }

延迟加载

1646710881187.gif

延迟加载Log

notCrash.png

按理说线上需要等待服务端数据返回,类似于延迟加载 Fragment,就不应该有这么多的崩溃。这时再看崩溃的机型,发现大多数是 Android 低版本,分辨率较低的手机,崩溃的 RecyclerView 显示的区域都不会太大。在拿本地几款低版本手机自测没有复现的情况下,想到将 RecyclerView 的高度设置为一个很小的值,此时只要一滑动就出现了必现的崩溃

滑动崩溃

1646711199053.gif

打印的 log 如果最开始直接更新 Fragment 是一致的

滑动崩溃Log

pullupcrash.png

根本原因就是小屏手机的 RecyclerView 高度较小,第一次更新时未能够放置多条 Item,导致加载 FragmentViewHolder 在上拉是才动态创建出来,这时 ViewHolderitemView 还没有 attach 到 window 导致了崩溃

解决崩溃

结合需求场景,在加载 FragmentViewHolder attach 到 window 时再执行 Fragment 的操作

        override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
            super.onViewAttachedToWindow(holder)
            LogUtil.i("Freeman", "onViewAttachedToWindow position = ${rv?.indexOfChild(holder.itemView)}")
            if (holder is FragmentHolder) {
                LogUtil.i("Freeman", "Add TestFragment")
                holder.itemView.findViewById<ConstraintLayout>(R.id.test_fragment_container_cl)?.let {
                    fragmentManager.beginTransaction()
                        .replace(R.id.test_fragment_container_cl, TestFragment(), "TestFragment")
                        .commitNowAllowingStateLoss()
                }
            }
        }

这样就确保 R.id.test_fragment_container_cl 已经被 attach 到 Activity 的 view hierarchy

截止至文章发布,新版本已经没有上报该崩溃,随着旧版本升级,崩溃也呈现收敛状态

结语

最后附上 stack overflow 上的一个回答

https://stackoverflow.com/questions/18645316/add-fragment-into-listview-item/18645419#18645419

翻译过来,大概的意思就是:不推荐在 ListView(RecyclderView) 中使用 Fragment。Fragment 是由 FragmentManager 管理,ListView 中的 itemView 是受ListView adapter 的管理,这样 Fragment 的状态需要受到它的容器的状态影响(在 ListView 中由于滑动列表,itemView 将频繁产生 attach 和 detach 的状态切换),容易发生不可控的情况

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值