排查嵌套画布导致的崩溃问题
更新(2025-09-25)
问题
项目在线上有一些崩溃:
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,怀疑是不是在OnDisable或OnDestroy中,进行改变层级、销毁对象等操作导致了崩溃。测试了一些想到的情况,发现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 发生了变化。进一步测试发现在那个界面的操作过程中,一些物件会归还给对象池,然后从对象池中拿一份新的,旧的物件过一段时间会销毁。怀疑正是这样的逻辑引起了崩溃
构造复现工程
经过一些测试,发现以下方式可以复现崩溃:
设置场景中的物件层级:

在添加了
-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
规避方式
使用复现工程测试,发现以下情况不会崩溃:
- 不使用嵌套画布
- 不使用
enabled禁用顶层画布(Canvas1),而是使用以下任一方式:- 使用
SetActive(false) - 把缩放设置为0
- 在顶层画布的
GameObject挂一个CanvasGroup,并把它的alpha设置为0
- 使用