错误堆栈
java.lang.IllegalArgumentException: No view found for id 0x7f0800c9 (com.wyapp.wydemo:id/fragment_test_id) for fragment TestFragment{82a7f22} (36a5c1af-fc3f-458f-a37d-7d4c98e1f3bd id=0x7f0800c9)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:514)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:261)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1817)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1760)
at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:547)
at android.os.Handler.handleCallback(Handler.java:958)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:222)
at android.os.Looper.loop(Looper.java:314)
at android.app.ActivityThread.main(ActivityThread.java:8716)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:565)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1081)
相信很多同学或多或少在项目中出现过以上异常,(而且是偶现的极其难以复现)看堆栈信息可以看出是我们可以看出是因为Fragment在被创建将要添加到ViewGroup中的时候,这个ViewGroup没有被找到导致的。
往往我们调用的方法只是提交了一次需要显示fragment的事务
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_test_id, TestFragment())
.commitAllowingStateLoss()
上面代码片段可以看出就是简单提交了一次事务,可能看不出啥原因,其实原因就是因为commitAllowingStateLoss方法提交的事务是异步处理的,向主线程提交了一个事务,会在下几次消息循环的时候执行。问题就出现在这里,我们往主线程提交了一个Message或者Runable他不是严格按照顺序执行的,这里两种情况会被优先执行
1、当主线程空闲的时候正在 nativePollOnce空闲挂起时,被事件输入唤醒
2、同步消息栅栏插入之后会优先处理异步消息,不会处理普通消息
此时,我们正在主线程执行,并且post了一次,那么可以排除1的情况,优先考虑2的情况
那么就是我们先执行到了commitAllowingStateLoss提交事务,也会被同步消息栅栏插入导致优先处理异步消息。我们就用以下demo简单验证一下
class MainActivity : AppCompatActivity() {
var token = 0
var handler: Handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
// 处理消息
Log.e("MainActivity", "handleMessage================== ${msg.isAsynchronous} arg1:${msg.arg1}")
findViewById<View>(R.id.fragment_test_id)?.apply {
(parent as? ViewGroup)?.removeView(this)
}
try {
// 移除栅栏
// 获取主线程的MessageQueue
val queue = Looper.getMainLooper().queue
val removeMethod =
MessageQueue::class.java.getDeclaredMethod(
"removeSyncBarrier",
Int::class.javaPrimitiveType
)
removeMethod.isAccessible = true
removeMethod.invoke(queue, token)
Log.e("MainActivity", "removeSyncBarrier ==================")
} catch (e: Exception) {
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val fragmentContainer = findViewById<ViewGroup>(R.id.test_container)
val testContainer = FrameLayout(this).apply {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
id = R.id.fragment_test_id
}
findViewById<View>(R.id.txt).setOnClickListener {
if (testContainer.parent == null) {
fragmentContainer.addView(testContainer)
}
Log.e("MainActivity", "onClickStart==================")
val t1 = SystemClock.uptimeMillis()
handler.sendMessage(Message.obtain(handler,{
Log.e("MainActivity", "post 11111================== ${t1}")
}).apply {
arg1 = 1
})
val t2 = SystemClock.uptimeMillis()
handler.sendMessage(Message.obtain(handler,{
Log.e("MainActivity", "post 22222================== ${t2}")
}).apply {
arg1 = 2
})
val t3 = SystemClock.uptimeMillis()
handler.sendMessage(Message.obtain(handler,{
Log.e("MainActivity", "post 33333================== ${t3}")
}).apply {
arg1 = 3
})
val t4 = SystemClock.uptimeMillis()
handler.sendMessage(Message.obtain(handler,{
Log.e("MainActivity", "post 444444================== ${t4}")
}).apply {
arg1 = 4
})
handler.sendMessage(Message.obtain(handler).apply {
arg1 = 444
})
val t5 = SystemClock.uptimeMillis()
handler.sendMessage(Message.obtain(handler,{
Log.e("MainActivity", "post 5555================== ${t5}")
}).apply {
arg1 = 5
})
try {
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_test_id, TestFragment())
.commitAllowingStateLoss()
}catch (e:Exception){
Log.e("MainActivity", "-----------------------------",e)
}
sendTestRemoveView()
}
}
fun sendTestRemoveView() {
// 获取主线程的MessageQueue
val queue = Looper.getMainLooper().queue
try {
val t6 = SystemClock.uptimeMillis()
Log.e("MainActivity", "sendTestRemoveView start ================== ${t6}")
// 通过反射获取postSyncBarrier方法
val method = MessageQueue::class.java.getDeclaredMethod("postSyncBarrier")
method.isAccessible = true
// 插入栅栏,返回token用于后续移除栅栏
token = method.invoke(queue) as Int
// 创建消息并设置为异步
val msg = Message.obtain()
msg.isAsynchronous = true
msg.what = 1 // 消息标识
handler.sendMessage(msg)
val t7 = SystemClock.uptimeMillis()
Log.e("MainActivity", "sendTestRemoveView end ------------------ ${t7}")
} catch (e: Exception) {
e.printStackTrace()
}
}
}
class TestFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val ctx = context ?: return null
return View(ctx).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
setBackgroundColor(Color.BLUE)
}
}
}
代码不是很多,简单描述一下就是,
有一个按钮txt
在点击的时候会将把testContainer添加到当前Activity的View里面并且设置id为R.id.fragment_test_id
调用handler的post/sendMessage来往主线程的MessageQuene里面插入消息(消息的runable是打印)
提交framgent相关的事务
继续调用handler的post/sendMessage来往主线程的MessageQuene里面插入消息(消息的runable是打印)
反射调用插入同步消息栅栏 postSyncBarrier
发射异步消息
handler里面处理异步消息
移除id为R.id.fragment_test_id的testContainer
移除同步消息栅栏
最后多次执行发现就会很容易出现上面崩溃,并且日志输入顺序是
2025-05-15 15:06:12.772 6833-6833 MainActivity com.wyapp.wydemo E onClickStart==================
2025-05-15 15:06:12.772 6833-6833 MainActivity com.wyapp.wydemo E sendTestRemoveView start ================== 608343510
2025-05-15 15:06:12.773 6833-6833 MainActivity com.wyapp.wydemo E sendTestRemoveView end ------------------ 608343510
2025-05-15 15:06:12.773 6833-6833 MainActivity com.wyapp.wydemo E handleMessage================== true arg1:0
2025-05-15 15:06:12.773 6833-6833 MainActivity com.wyapp.wydemo E removeSyncBarrier ==================
2025-05-15 15:06:12.784 6833-6833 MainActivity com.wyapp.wydemo E post 11111================== 608343510
2025-05-15 15:06:12.785 6833-6833 MainActivity com.wyapp.wydemo E post 22222================== 608343510
2025-05-15 15:06:12.785 6833-6833 MainActivity com.wyapp.wydemo E post 33333================== 608343510
2025-05-15 15:06:12.785 6833-6833 MainActivity com.wyapp.wydemo E post 444444================== 608343510
2025-05-15 15:06:12.785 6833-6833 MainActivity com.wyapp.wydemo E handleMessage================== false arg1:444
2025-05-15 15:06:12.786 6833-6833 MainActivity com.wyapp.wydemo E post 5555================== 608343510
可以看出来。哪怕我们优先往主线程的MessageQuenu里面提交了Message,后再执行模拟插入同步消息栅栏,当When时间相当的时候,就会出现后执行的插入同步消息栅栏会抢先在普通Message之前生效,具体原因是因为我们MessageQuene在插入消息的时候排序规则决定的。
总所周知:MessageQuene在插入Message时,是通过当前时间进行排序的。也就是when变量
从源码角度分析
普通消息的插入逻辑(MessageQueue.enqueueMessage()),如果碰到When相同会插入到when相同组的最后
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
msg.when = when;
Message p = mMessages;
// 情况1:队列为空,或新消息需要立即执行(when=0)
if (p == null || when == 0 || when < p.when) {
msg.next = p;
mMessages = msg;
}
// 情况2:按 when 顺序插入到链表中间
else {
Message prev;
for (;;) {
prev = p;
p = p.next;
// 就因为这里是 when < p.when 是严格小于,所以when相同就会到最后去了
if (p == null || when < p.when) {
break; // 找到插入位置
}
}
msg.next = p;
prev.next = msg;
}
}
return true;
}
异步消息栅栏的插入逻辑(MessageQueue.postSyncBarrier())会插入到when相同的最前面
private int postSyncBarrier(long when) {
synchronized (this) {
// 创建栅栏(target=null)
Message msg = Message.obtain();
msg.when = when;
Message p = mMessages;
if (when != 0) {
// 遍历找到第一个 when >= 当前时间的消息 就插入
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
// !!!!!!! 插入栅栏到同 when 组的最前面!!!!!!!!!!!!!!!!!!!!!!!!!
if (prev != null) {
msg.next = prev.next;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg; // 直接成为队列头部
}
}
}
这就解释通了,我们就可以得出结论,就算我们先执行提交事务/往主线程插入message,也会在极限情况下(when相等,也就侧面佐证为啥复现几率很小)会被某一些同步消息栅栏+异步消息抢先执行,并且我们抢先执行里面有移除View得操作或者其他导致状态变化的操作,就会出现这个情况
接下来就是修复方案
1、建议try catch + commonNow同步提交,出现异常可以被catch住
2、增强方案,我们主动post一次,保证View已经被正常添加到View种了。并且在执行前做状态判断
handler.post {
try {
findViewById<View>(R.id.fragment_test_id) ?: return@post
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_test_id, TestFragment())
.commitNowAllowingStateLoss()
}catch (e:Exception){
Log.e("MainActivity", "-----------------------------",e)
}
}