出现崩溃
项目在发布前测试测出一个偶现崩溃,起初因为无法复现,就直接带 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 也可以简单证明这个逻辑:
到这就可以知道在
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
而线上却没有必现,那是因为线上需要等待服务端数据返回,在异步获得数据前,数据为 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) // 延迟一秒
}
}
延迟加载
延迟加载Log
按理说线上需要等待服务端数据返回,类似于延迟加载 Fragment,就不应该有这么多的崩溃。这时再看崩溃的机型,发现大多数是 Android 低版本,分辨率较低的手机,崩溃的 RecyclerView 显示的区域都不会太大。在拿本地几款低版本手机自测没有复现的情况下,想到将 RecyclerView 的高度设置为一个很小的值,此时只要一滑动就出现了必现的崩溃
滑动崩溃
打印的 log 如果最开始直接更新 Fragment 是一致的
滑动崩溃Log
根本原因就是小屏手机的
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 的状态切换),容易发生不可控的情况