排查嵌套画布导致的崩溃问题

更新(2025-09-25)

Unity 已复现问题:Crash on UI::Canvas::UpdateEventIndexesRecursive when a script modifies nested Canvases in Play mode while using command line argument -debugallocator

问题

项目在线上有一些崩溃:

Thread 0 Crashed:
0   UnityFramework                  0x0000000118dd7314 RemoveFromList + 4 (LinkedList.h:307)
1   UnityFramework                  0x0000000118dd7314 MonoBehaviour::RemoveNodesFromLists() + 144 (MonoBehaviour.cpp:355)
2   UnityFramework                  0x0000000118dd77e4 MonoBehaviour::RemoveFromManager() + 84 (MonoBehaviour.cpp:366)
3   UnityFramework                  0x0000000118ba5b20 GameObject::ActivateAwakeRecursivelyInternal(DeactivateOperation, AwakeFromLoadQueue&) + 452 (GameObject.cpp:230)
4   UnityFramework                  0x0000000118ba5aa8 GameObject::ActivateAwakeRecursivelyInternal(DeactivateOperation, AwakeFromLoadQueue&) + 332 (GameObject.cpp:214)
5   UnityFramework                  0x0000000118ba5bc8 GameObject::ActivateAwakeRecursively(DeactivateOperation) + 60 (GameObject.cpp:247)
6   UnityFramework                  0x0000000118ba6084 GameObject::Deactivate(DeactivateOperation) + 44 (GameObject.cpp:519)
7   UnityFramework                  0x0000000118a8bbb4 GameObject_CUSTOM_SetActive(ScriptingBackendNativeObjectPtrOpaque*, unsigned char) + 96 (CoreBindings.gen.cpp:60539)
...
Thread 0 Crashed:
0   UnityFramework                  0x000000011a267314 RemoveFromList + 4 (LinkedList.h:307)
1   UnityFramework                  0x000000011a267314 MonoBehaviour::RemoveNodesFromLists() + 144 (MonoBehaviour.cpp:355)
2   UnityFramework                  0x000000011a267258 MonoBehaviour::MainThreadCleanup() + 20 (MonoBehaviour.cpp:111)
3   UnityFramework                  0x000000011a02ee9c delete_object_internal_step1(Object*) + 196 (BaseObject.cpp:513)
4   UnityFramework                  0x000000011a1281c4 CommitBatchDelete(BatchDelete&) + 48 (BatchDeleteObjects.cpp:138)
5   UnityFramework                  0x000000011a1335d4 DestroyGameObjectHierarchy + 120 (GameObjectUtility.cpp:1902)
6   UnityFramework                  0x000000011a1335d4 DestroyObjectHighLevel_Internal(Object*, bool) + 484 (GameObjectUtility.cpp:2062)
7   UnityFramework                  0x000000011a058c1c DelayedDestroyCallback(Object*, void*) + 16 (DestroyDelayed.cpp:9)
8   UnityFramework                  0x000000011a055fc0 DelayedCallManager::Update(int) + 544 (CallDelayed.cpp:188)
9   UnityFramework                  0x000000011a134b90 ExecutePlayerLoop(NativePlayerLoopSystem*) + 140 (PlayerLoop.cpp:406)
10  UnityFramework                  0x000000011a134bd0 ExecutePlayerLoop(NativePlayerLoopSystem*) + 204 (PlayerLoop.cpp:427)
11  UnityFramework                  0x000000011a134ecc PlayerLoop() + 280 (PlayerLoop.cpp:534)
12  UnityFramework                  0x000000011a884908 UnityPlayerLoopImpl(bool) + 128 (LibEntryPoint.mm:347)
13  UnityFramework                  0x0000000119ea81b4 UnityRepaint + 40 (UnityAppController+Rendering.mm:216)
...
Thread 0 Crashed:
0   UnityFramework                  0x000000011be67314 RemoveFromList + 4 (LinkedList.h:307)
1   UnityFramework                  0x000000011be67314 MonoBehaviour::RemoveNodesFromLists() + 144 (MonoBehaviour.cpp:355)
2   UnityFramework                  0x000000011be67258 MonoBehaviour::MainThreadCleanup() + 20 (MonoBehaviour.cpp:111)
3   UnityFramework                  0x000000011bc2ee9c delete_object_internal_step1(Object*) + 196 (BaseObject.cpp:513)
4   UnityFramework                  0x000000011bd281c4 CommitBatchDelete(BatchDelete&) + 48 (BatchDeleteObjects.cpp:138)
5   UnityFramework                  0x000000011bd35f70 AddOneBatchDeleteObject + 20 (BatchDeleteObjects.h:32)
6   UnityFramework                  0x000000011bd35f70 AddToBatchDeleteAndMakeUnpersistent(Object&, BatchDelete&) + 64 (GameObjectUtility.cpp:1719)
7   UnityFramework                  0x000000011bd32e58 DestroyGameObjectRecursive(GameObject&, BatchDelete&) + 272 (GameObjectUtility.cpp:1786)
8   UnityFramework                  0x000000011bd32d9c DestroyGameObjectRecursive(GameObject&, BatchDelete&) + 84 (GameObjectUtility.cpp:1773)
9   UnityFramework                  0x000000011bd335cc DestroyGameObjectHierarchy + 112 (GameObjectUtility.cpp:1899)
10  UnityFramework                  0x000000011bd335cc DestroyObjectHighLevel_Internal(Object*, bool) + 476 (GameObjectUtility.cpp:2062)
11  UnityFramework                  0x000000011bd60420 UnloadSceneObjects + 32 (UnityScene.cpp:190)
12  UnityFramework                  0x000000011bd60420 RuntimeSceneManager::UnloadSceneInternal(UnityScene*, UnloadSceneOptions) + 76 (SceneManager.cpp:287)
13  UnityFramework                  0x000000011bd553a4 UnloadSceneOperation::IntegrateMainThread() + 60 (UnloadSceneOperation.cpp:25)
14  UnityFramework                  0x000000011bd545cc PreloadManager::UpdatePreloadingSingleStep(PreloadManager::UpdatePreloadingFlags, int) + 228 (PreloadManager.cpp:413)
15  UnityFramework                  0x000000011bd55034 PreloadManager::UpdatePreloading() + 276 (PreloadManager.cpp:578)
16  UnityFramework                  0x000000011bd34b90 ExecutePlayerLoop(NativePlayerLoopSystem*) + 140 (PlayerLoop.cpp:406)
17  UnityFramework                  0x000000011bd34bd0 ExecutePlayerLoop(NativePlayerLoopSystem*) + 204 (PlayerLoop.cpp:427)
18  UnityFramework                  0x000000011bd34ecc PlayerLoop() + 280 (PlayerLoop.cpp:534)
19  UnityFramework                  0x000000011c484908 UnityPlayerLoopImpl(bool) + 128 (LibEntryPoint.mm:347)
20  UnityFramework                  0x000000011baa81b4 UnityRepaint + 40 (UnityAppController+Rendering.mm:216)
...

崩溃的原因是尝试从非法地址读取。地址都是一个很小的值,但每次崩溃时各不相同

Android 和 iOS 都有这个崩溃,Unity版本是 2022.3.57f1

排查

查看MonoBehaviour::RemoveNodesFromLists的汇编代码,发现是一个指针变成了一个很小的值,导致了这个崩溃。怀疑是某个地方把这个内存写坏了,导致出现了非法的指针值。由于很多崩溃的调用栈中都是在递归遍历GameObject,怀疑是不是在OnDisableOnDestroy中,进行改变层级、销毁对象等操作导致了崩溃。测试了一些想到的情况,发现Unity都做了检查,只是有报错提示不允许这样的操作,没有崩溃

搁置了一段时间后,偶然在Xcode的Organizer中,看到有多个玩家反馈这类崩溃是在某个界面操作的时候发生的(应该是使用了 TestFlight 包的玩家)。在编辑器中,尝试在这个界面到处点了点,发现确实会崩溃,但调用栈和上面的不一样。之后在Windows包中测试,发现崩溃的调用栈和上面的类似,都是在MonoBehaviour::RemoveNodesFromLists

接下来,为了找到真正把内存写坏的地方,在游戏的命令行参数中加上-debugallocator,让访问非法地址时能够第一时间崩溃。重复复现步骤,发现编辑器和Windows包都崩溃在同一个调用栈中:

Unity.exe!UI::Canvas::UpdateEventIndexesRecursive(int &)
Unity.exe!UI::Canvas::UpdateEventIndexesRecursive(int &)
Unity.exe!UI::Canvas::UpdateBatchOrder(void)
Unity.exe!UI::Canvas::UpdateBatches(bool)
Unity.exe!`UI::InitializeCanvasManager'::`2'::UIEventsWillRenderCanvasesRegistrator::Forward()
Unity.exe!`InitPlayerLoopCallbacks'::`2'::PostLateUpdatePlayerUpdateCanvasesRegistrator::Forward()
Unity.exe!ExecutePlayerLoop(struct NativePlayerLoopSystem *)
Unity.exe!ExecutePlayerLoop(struct NativePlayerLoopSystem *)
Unity.exe!PlayerLoop(void)
Unity.exe!PlayerLoopController::InternalUpdateScene(bool,bool)
Unity.exe!PlayerLoopController::UpdateSceneIfNeededFromMainLoop(void)
Unity.exe!Application::TickTimer(void)
Unity.exe!MainMessageLoop()
Unity.exe!WinMain()

崩溃的原因是尝试从一个非法地址中读取,地址看起来是在堆上,因此怀疑这是一个野指针(已经被释放了)。结合调用栈,猜测是Canvas中原本记录的某个数据被销毁了,但指针还残留着。基于此,尝试把界面刚打开时的所有UI的物件层级(GameObject及它的所有组件)和崩溃前的所有UI的物件层级输出,发现崩溃前的物件层级中,一部分物件虽然类型没有变化,但 InstanceId 发生了变化。进一步测试发现在那个界面的操作过程中,一些物件会归还给对象池,然后从对象池中拿一份新的,旧的物件过一段时间会销毁。怀疑正是这样的逻辑引起了崩溃

构造复现工程

经过一些测试,发现以下方式可以复现崩溃:

  1. 设置场景中的物件层级:

  2. 在添加了-debugallocator的编辑器中,运行代码:

private IEnumerator Run()
{
    Canvas1.enabled = false;

    yield return null;

    Image.transform.SetParent(Canvas2.transform, true);

    yield return null;

    Destroy(Image.gameObject);

    yield return null;

    Canvas1.enabled = true;
}

复现工程已反馈给Unity

规避方式

使用复现工程测试,发现以下情况不会崩溃:

  1. 不使用嵌套画布
  2. 不使用enabled禁用顶层画布(Canvas1),而是使用以下任一方式:
    1. 使用SetActive(false)
    2. 把缩放设置为0
    3. 在顶层画布的GameObject挂一个CanvasGroup,并把它的alpha设置为0

链接