You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

从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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 13:20:15