SceneKit:内存过多

SceneKit: too much memory persisting

我在这里没有想法,SceneKit 正在堆积内存,我才刚刚开始。我正在显示存储在数组中的 SNCNodes,这样我就可以为动画分离分子的组成部分。这些树模型分子,我最终可能会展示 50 个,比如每个“章节”一个。问题是当我转到另一章时,前几章的分子仍然在记忆中。

分子节点是子节点树。大约一半的节点是用于定位目的的空容器。否则,几何形状为 SCNPrimitives(球体、胶囊和圆柱体)。每个几何体都有一个镜面反射和一个由 UIColor 组成的漫反射 material,不使用任何纹理。

当应用程序首次启动时,这些分子由代码构建并存档到字典中。然后,在随后的引导中,存档字典被读入本地字典以供 VC 使用。 (为简洁起见,我删除了此 post 中的安全功能。)

moleculeDictionary = Molecules.readFile() as! [String: [SCNNode]]

当一个章节想要显示一个分子时,它会调用一个特定的函数,该函数将给定分子所需的组件从本地字典加载到本地 SCNNode 属性中。

// node stores (reuseable)
var atomsNode_1 = SCNNode() 
var atomsNode_2 = SCNNode()
        . . .

func lysozyme() {   // called by a chapter to display this molecule 
        . . .
    components = moleculeDictionary["lysozyme"]

    atomsNode_1 = components[0]         // protein w/CPK color
    baseNode.addChildNode(atomsNode_1)
    atomsNode_2 = components[2]         // NAG
    baseNode.addChildNode(atomsNode_2)
        . . .
 }

在显示下一个分子之前,我调用了一个“清理”函数:

atomsNode_1.removeFromParentNode()
atomsNode_2.removeFromParentNode()
        . . .

当我在仪器中进行调查时,大部分膨胀的内存是 C3DMeshCreateFromProfile 调用的 32 kB 块和 C3DMeshCreateCopyWithInterleavedSources.

调用的 80 kB 块。

我也有需要追踪的泄漏,这些泄漏可追溯到存档的 NSKeyedUnarchiver 解码。所以我也需要处理这些,但它们只是累积每个分子调用的内存使用的一小部分。

如果我return到一个之前查看过的分子,内存使用量没有进一步增加,它都累积并持续存在。

我试过将 atomsNode_1 及其亲属声明为可选项,然后在清理时将它们设置为 nil。没有帮助。我试过了,在清理函数中,

atomsNode_1.enumerateChildNodesUsingBlock({
    node, stop in
    node.removeFromParentNode()
})

好吧,内存又下降了,但是节点现在似乎从加载的字典中永久消失了。该死的引用类型!

所以也许我需要一种方法来归档 [SCNNode] 数组,以便单独取消归档和检索它们。在这种情况下,我会在完成后将它们从内存中清除,并在重新访问该分子时从存档中重新加载。但我还不知道如何做这些中的任何一个。在投入更多时间感到沮丧之前,我会很感激对此的评论。

球体、胶囊体和圆柱体都有相当密集的网格。你需要那么多细节吗?尝试减少各种段数属性(segmentCountradialSegmentCount 等)。作为快速测试,将 SCNPyramid 替换为所有基元类型(即向量计数最低的基元)。如果这是一个因素,您应该会看到内存使用量显着减少(它看起来很难看,但会立即反馈您是否在可用轨道上)。你能用长的 SCNBox 而不是圆柱体吗?

另一个优化步骤是使用 SCNLevelOfDetail 以在对象距离较远时允许替代、低顶点数几何体。这比简单地统一减少段数要多做一些工作,但如果您有时需要更多细节,将会有所回报。

与其自己管理数组中的组件,不如使用节点层次结构来做到这一点。将每个分子或分子的可动画部分创建为 SCNNodes 的树。给它起个名字。做一个flattenedClone。现在存档。在需要时从存档中读取节点树;不用担心节点数组。

考虑编写两个程序。一个是 iOS 程序 manipulates/displays 分子。另一个是 Mac(或 iOS?)生成分子节点树并将其存档的程序。这将为您提供一堆 SCNNode 树存档,您可以将其作为资源嵌入到您的显示程序中,无需即时生成。

的回答指出需要清空 "textures"(materialsfirstMaterial 属性?)以释放节点。似乎值得一看,但由于您只是使用 UIColor,我怀疑这是一个因素。

下面是创建复合节点并将其存档的示例。在实际代码中,您会将归档与创建分开。还要注意使用长而瘦的盒子来模拟一条线。尝试倒角半径为 0!

extension SCNNode {

public class func gizmoNode(axisLength: CGFloat) -> SCNNode {
    let offset = CGFloat(axisLength/2.0)
    let axisSide = CGFloat(0.1)
    let chamferRadius = CGFloat(axisSide)

    let xBox = SCNBox(width: axisLength, height: axisSide, length: axisSide, chamferRadius: chamferRadius)
    xBox.firstMaterial?.diffuse.contents = NSColor.redColor()
    let yBox = SCNBox(width: axisSide, height: axisLength, length: axisSide, chamferRadius: chamferRadius)
    yBox.firstMaterial?.diffuse.contents = NSColor.greenColor()
    let zBox = SCNBox(width: axisSide, height: axisSide, length: axisLength, chamferRadius: chamferRadius)
    zBox.firstMaterial?.diffuse.contents = NSColor.blueColor()
    let xNode = SCNNode(geometry: xBox)
    xNode.name = "X axis"
    let yNode = SCNNode(geometry: yBox)
    yNode.name = "Y axis"
    let zNode = SCNNode(geometry: zBox)
    zNode.name = "Z axis"

    let result = SCNNode()
    result.name = "Gizmo"
    result.addChildNode(xNode)
    result.addChildNode(yNode)
    result.addChildNode(zNode)
    xNode.position.x = offset
    yNode.position.y = offset
    zNode.position.z = offset

    let data = NSKeyedArchiver.archivedDataWithRootObject(result)
    let filename = "gizmo"

    // Save data to file
    let DocumentDirURL = try! NSFileManager.defaultManager().URLForDirectory(.DocumentDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: true)

    // made the extension "plist" so you can easily inspect it by opening in Finder. Could just as well be "scn" or "node"
    // ".scn" can be opened in the Xcode Scene Editor
    let fileURL = DocumentDirURL.URLByAppendingPathComponent(filename).URLByAppendingPathExtension("plist")
    print("FilePath:", fileURL.path)

    if (!data.writeToURL(fileURL, atomically: true)) {
        print("oops")
    }
    return result
}
}   

我也确实在我的应用程序中经历过 SceneKit 的大量内存膨胀,与 Instruments 中的内存块相似(C3DGenericSourceCreateDeserializedDataWithAccessorsC3DMeshSourceCreateMutable 等)。我发现在 SCNNode 对象上将 geometry 属性 设置为 nil 然后让 Swift 取消初始化它们解决了它。

在你的例子中,在你的清理函数中,做类似的事情:

atomsNode_1.removeFromParentNode()
atomsNode_1.geometry = nil
atomsNode_2.removeFromParentNode()
atomsNode_2.geometry = nil

关于如何实施清洁的另一个示例:

class ViewController: UIViewController {
    @IBOutlet weak var sceneView: SCNView!
    var scene: SCNScene!

    // ...

    override func viewDidLoad() {
        super.viewDidLoad()
        scene = SCNScene()
        sceneView.scene = scene

        // ...
    }

    deinit {
        scene.rootNode.cleanup()
    }

    // ...
}

extension SCNNode {
    func cleanup() {
        for child in childNodes {
            child.cleanup()
        }
        geometry = nil
    }
}

如果这不起作用,您可以通过将其纹理设置为 nil 获得更好的成功,如 所报告的那样。