从源码层面 深入理解 Fragment事务异常 java.lang.IllegalArgumentException: No view found for id 0x7f0800c9

错误堆栈

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)
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。