ARKit

3 Hidden Memory Leaks in RealityKit That Apple Doesn't Tell You About

Sebastian Kotarski

Sebastian Kotarski

March 1, 2025
3 min read

RealityKit's Hidden Pitfalls

RealityKit is Apple's modern framework for 3D and AR content. It's powerful, well-designed, and — when used incorrectly — a prolific source of memory leaks that are nearly impossible to detect with standard tools.

After debugging dozens of production ARKit apps, I've identified three memory leak patterns that appear consistently. Apple's documentation doesn't warn about any of them.

Leak #1: Entity Component Subscriptions

When you subscribe to component changes on a RealityKit entity, the subscription creates a strong reference cycle if you're not careful:

// LEAKS: The closure captures self strongly
class GameManager {
    var entity: ModelEntity?

    func setupEntity() {
        entity?.scene?.subscribe(to: CollisionEvents.Began.self) { event in
            self.handleCollision(event) // Strong capture of self
        }
    }
}

The fix is straightforward but easy to forget:

// FIXED: Use weak capture
entity?.scene?.subscribe(to: CollisionEvents.Began.self) { [weak self] event in
    self?.handleCollision(event)
}

What makes this particularly insidious is that the leak doesn't show up in simple testing. It only manifests when entities are repeatedly created and destroyed — like in a game with multiple rounds or an AR experience with dynamic content.

Leak #2: ModelEntity Clone Accumulation

ModelEntity.clone(recursive:) is commonly used to create multiple instances of the same 3D model. The documentation suggests this is efficient because it shares mesh and material data.

What it doesn't tell you is that each clone maintains its own physics body, collision shapes, and animation state — and these aren't automatically cleaned up when you remove the clone from the scene:

// This leaks physics resources
func spawnEnemy() {
    let clone = templateEntity.clone(recursive: true)
    clone.generateCollisionShapes(recursive: true)
    arView.scene.addAnchor(clone)
}

func removeEnemy(_ entity: Entity) {
    entity.removeFromParent() // Physics resources NOT released
}

The fix requires explicit cleanup:

func removeEnemy(_ entity: Entity) {
    // Clean up physics first
    if let modelEntity = entity as? ModelEntity {
        modelEntity.collision = nil
        modelEntity.physicsBody = nil
    }
    entity.removeFromParent()
}

Leak #3: ARView's Debug Options Retain Cycle

This one surprised even me. If you toggle ARView debug options during runtime (common during development), certain debug visualizations create retain cycles with the view's internal rendering pipeline:

// Development debug toggle — causes leak if toggled repeatedly
arView.debugOptions = [.showPhysics, .showStatistics]
// Later...
arView.debugOptions = []

The workaround is to set debug options only once during setup, or to use a completely fresh ARView instance when toggling debug modes.

How I Found These

Standard memory profiling tools like Instruments' Leaks template don't catch these issues because they involve object graphs that are technically reachable but practically abandoned. The objects exist in RealityKit's internal pools and aren't flagged as leaks.

I use a combination of:

  1. Memory graph debugging in Xcode to visualize object relationships
  2. Custom allocation tracking that monitors specific RealityKit types
  3. Automated stress tests that create and destroy entities thousands of times

If your RealityKit app's memory grows over time, reach out for a technical audit. These leaks are fixable, but finding them requires specialized tools and experience.

Tags

RealityKit
memory leaks
best practices
ARKit

Share this article

Need Help With Your iOS Project?

I help startups and enterprises solve critical iOS, ARKit, and visionOS issues. From performance problems to app store rejections.