从ARSCNView移除节点引发随机崩溃(btCollisionWorld::updateSingleAabb)
ARSCNView中小球节点删除重建偶现崩溃问题
我在ARSCNView上实现了小球下落动画,1秒后删除小球节点并重新添加到视图原位置,但操作偶尔会引发崩溃。
代码示例
private func dropBall(){ // do ball dropping animation let randomX = Float.random(in: -2...2) * 0.001 let randomY = Float.random(in: 3...10) * 0.001 ballNode?.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil) ballNode?.physicsBody?.mass = 0.005 let position = SCNVector3(x: 0.05, y: 0.05, z: 0.05) let force = SCNVector3(x: randomX, y: randomY , z: 0) ballNode?.physicsBody?.applyForce(force, at: position, asImpulse: true) let tv = SCNVector4(x: -0.2 + Float(0.4 * randomCGFloat()), y: 0 , z: 0.2, w: 0) ballNode?.physicsBody?.applyTorque(tv, asImpulse: true) //remove the ball DispatchQueue.main.asyncAfter(deadline: .now() + 1){ self.ballNode?.physicsBody = nil self.ballNode?.removeFromParentNode() self.ballNode = nil // add ball again let ballNode = SCNReferenceNode(named: "myscn.scn") contentNode?.addChildNode(ballNode) self.ballNode = ballNode } }
崩溃堆栈信息
Exception Type: EXC_BAD_ACCESS (SIGSEGV) Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000068 Exception Codes: 0x0000000000000001, 0x0000000000000068 VM Region Info: 0x68 is not in any region. Bytes before following region: 68719476632 REGION TYPE START - END [ VSIZE] PRT/MAX SHRMOD REGION DETAIL UNUSED SPACE AT START ---> commpage (reserved) 1000000000-7000000000 [384.0G] ---/--- SM=NUL ...(unallocated) Termination Reason: SIGNAL 11 Segmentation fault: 11 Terminating Process: exc handler [30984] Triggered by Thread: 11 Thread 11 name: Thread 11 Crashed: 0 SceneKit 0x00000001cf3ef998 btDbvtBroadphase::setAabb(btBroadphaseProxy*, btVector3 const&, btVector3 const&, btDispatcher*) + 52 1 SceneKit 0x00000001cf65126c btCollisionWorld::updateSingleAabb(btCollisionObject*) + 284 2 SceneKit 0x00000001cf65133c btCollisionWorld::updateAabbs() + 56 3 SceneKit 0x00000001cf3ff39c btCollisionWorld::performDiscreteCollisionDetection() + 32 4 SceneKit 0x00000001cf406964 btDiscreteDynamicsWorld::internalSingleStepSimulation(float) + 120 5 SceneKit 0x00000001cf3ff574 btDiscreteDynamicsWorld::stepSimulation(float, int, float) + 276 6 SceneKit 0x00000001cf507ad8 -[SCNPhysicsWorld _step:] + 156 (SCNPhysicsWorld.mm:1423) 7 SceneKit 0x00000001cf3ae9fc -[SCNRenderer _update:] + 932 (SCNRenderer.m:3814) 8 SceneKit 0x00000001cf3ae364 -[SCNRenderer _drawSceneWithNewRenderer:] + 152 (SCNRenderer.m:5303) 9 SceneKit 0x00000001cf3ae278 -[SCNRenderer _drawScene:] + 40 (SCNRenderer.m:5537) 10 SceneKit 0x00000001cf3a78b4 -[SCNRenderer _drawAtTime:] + 500 (SCNRenderer.m:5727) 11 SceneKit 0x00000001cf3a74cc -[SCNView _drawAtTime:] + 368 (SCNView.m:1542) 12 SceneKit 0x00000001cf4ba478 __83-[NSObject(SCN_DisplayLinkExtensions) SCN_setupDisplayLinkWithQueue:screen:policy:]_block_invoke + 44 (SCNDisplayLink_ARC.m:42) 13 SceneKit 0x00000001cf595fa0 -[SCNDisplayLink _displayLinkCallbackReturningImmediately] + 148 (SCNDisplayLink.m:381)
问题分析与修复方案
崩溃原因
从堆栈信息看,崩溃发生在SceneKit的物理引擎专属线程(Thread 11),属于野指针访问。核心问题是:你在主线程异步删除节点时,物理引擎线程可能还在引用该节点的physicsBody对象——虽然你先将physicsBody设为nil,但这个操作和物理引擎的更新循环存在竞态条件,物理线程可能还在处理旧的物理体资源,导致访问已释放的内存。
修复方案
方案1:避免频繁销毁重建,直接重置节点状态(最优解)
减少内存管理风险,同时从根源避免线程竞态:
private func dropBall(){ guard let ballNode = ballNode else { return } // 重置节点到初始状态并启动动画 resetBallToInitialState(ballNode) applyForceAndTorque(to: ballNode) // 1秒后再次重置并启动 DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // 使用SCNTransaction保证操作在SceneKit渲染周期内原子执行 SCNTransaction.begin() SCNTransaction.animationDuration = 0 self.resetBallToInitialState(ballNode) SCNTransaction.commit() self.applyForceAndTorque(to: ballNode) } } // 抽离重置节点状态的函数 private func resetBallToInitialState(_ node: SCNNode){ node.physicsBody = nil // 替换为你的小球初始位置 node.position = SCNVector3(x: 0, y: 2, z: 0) } // 抽离施加力和扭矩的函数 private func applyForceAndTorque(to node: SCNNode){ node.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil) node.physicsBody?.mass = 0.005 let randomX = Float.random(in: -2...2) * 0.001 let randomY = Float.random(in: 3...10) * 0.001 let position = SCNVector3(x: 0.05, y: 0.05, z: 0.05) let force = SCNVector3(x: randomX, y: randomY , z: 0) node.physicsBody?.applyForce(force, at: position, asImpulse: true) let tv = SCNVector4(x: -0.2 + Float(0.4 * randomCGFloat()), y: 0 , z: 0.2, w: 0) node.physicsBody?.applyTorque(tv, asImpulse: true) }
方案2:必须销毁重建时,确保线程安全
如果业务逻辑要求必须销毁重建节点,需要通过SCNTransaction包装操作,并延迟新节点的创建,确保旧节点资源完全释放:
private func dropBall(){ // 原有施加力和扭矩的代码 let randomX = Float.random(in: -2...2) * 0.001 let randomY = Float.random(in: 3...10) * 0.001 ballNode?.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil) ballNode?.physicsBody?.mass = 0.005 let position = SCNVector3(x: 0.05, y: 0.05, z: 0.05) let force = SCNVector3(x: randomX, y: randomY , z: 0) ballNode?.physicsBody?.applyForce(force, at: position, asImpulse: true) let tv = SCNVector4(x: -0.2 + Float(0.4 * randomCGFloat()), y: 0 , z: 0.2, w: 0) ballNode?.physicsBody?.applyTorque(tv, asImpulse: true) DispatchQueue.main.asyncAfter(deadline: .now() + 1){ SCNTransaction.begin() SCNTransaction.animationDuration = 0 // 先彻底移除物理体引用 self.ballNode?.physicsBody?.removeFromParentPhysicsWorld() self.ballNode?.physicsBody = nil self.ballNode?.removeFromParentNode() SCNTransaction.commit() // 延迟创建新节点,避免物理线程还在处理旧节点 DispatchQueue.main.async { guard let newBallNode = SCNReferenceNode(named: "myscn.scn") else { return } // 设置初始位置 newBallNode.position = SCNVector3(x: 0, y: 2, z: 0) self.contentNode?.addChildNode(newBallNode) self.ballNode = newBallNode // 重新施加力和扭矩 self.applyForceAndTorque(to: newBallNode) } } }
核心注意事项
- 避免频繁节点销毁:创建和删除SCNNode会涉及资源分配与释放,容易引发线程竞态
- 强制使用SCNTransaction:所有SceneKit节点的属性修改、添加/删除操作,都应该用
SCNTransaction包装,确保操作在渲染周期内原子执行,避免和物理引擎线程冲突 - 先清理物理体:修改节点物理状态前,先将
physicsBody设为nil,确保物理引擎停止对该节点的处理
内容的提问来源于stack exchange,提问作者Haozes
相关产品推荐
相关产品推荐

