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

  • Post author:
  • Post category:java




出现崩溃

项目在发布前测试测出一个偶现崩溃,起初因为无法复现,就直接带 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 大概意思是找不到一个


id





0x7f0801b5


的容器去承载


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


对象,


HostCallbacks





FragmentActivity


的一个内部类,回到前面


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 的


onCreateViewHolder





onBindViewHolder


后,在


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,导致加载


Fragment





ViewHolder


在上拉是才动态创建出来,这时


ViewHolder





itemView


还没有 attach 到 window 导致了崩溃



解决崩溃

结合需求场景,在加载


Fragment





ViewHolder


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 的状态切换),容易发生不可控的情况



版权声明:本文为HJXASLZYY原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。