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]
数组,以便单独取消归档和检索它们。在这种情况下,我会在完成后将它们从内存中清除,并在重新访问该分子时从存档中重新加载。但我还不知道如何做这些中的任何一个。在投入更多时间感到沮丧之前,我会很感激对此的评论。
球体、胶囊体和圆柱体都有相当密集的网格。你需要那么多细节吗?尝试减少各种段数属性(segmentCount
、radialSegmentCount
等)。作为快速测试,将 SCNPyramid
替换为所有基元类型(即向量计数最低的基元)。如果这是一个因素,您应该会看到内存使用量显着减少(它看起来很难看,但会立即反馈您是否在可用轨道上)。你能用长的 SCNBox
而不是圆柱体吗?
另一个优化步骤是使用 SCNLevelOfDetail
以在对象距离较远时允许替代、低顶点数几何体。这比简单地统一减少段数要多做一些工作,但如果您有时需要更多细节,将会有所回报。
与其自己管理数组中的组件,不如使用节点层次结构来做到这一点。将每个分子或分子的可动画部分创建为 SCNNodes
的树。给它起个名字。做一个flattenedClone
。现在存档。在需要时从存档中读取节点树;不用担心节点数组。
考虑编写两个程序。一个是 iOS 程序 manipulates/displays 分子。另一个是 Mac(或 iOS?)生成分子节点树并将其存档的程序。这将为您提供一堆 SCNNode 树存档,您可以将其作为资源嵌入到您的显示程序中,无需即时生成。
对 的回答指出需要清空 "textures"(materials
或 firstMaterial
属性?)以释放节点。似乎值得一看,但由于您只是使用 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 中的内存块相似(C3DGenericSourceCreateDeserializedDataWithAccessors
、C3DMeshSourceCreateMutable
等)。我发现在 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
获得更好的成功,如 所报告的那样。
我在这里没有想法,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
.
我也有需要追踪的泄漏,这些泄漏可追溯到存档的 NSKeyedUnarchiver
解码。所以我也需要处理这些,但它们只是累积每个分子调用的内存使用的一小部分。
如果我return到一个之前查看过的分子,内存使用量没有进一步增加,它都累积并持续存在。
我试过将 atomsNode_1
及其亲属声明为可选项,然后在清理时将它们设置为 nil。没有帮助。我试过了,在清理函数中,
atomsNode_1.enumerateChildNodesUsingBlock({
node, stop in
node.removeFromParentNode()
})
好吧,内存又下降了,但是节点现在似乎从加载的字典中永久消失了。该死的引用类型!
所以也许我需要一种方法来归档 [SCNNode]
数组,以便单独取消归档和检索它们。在这种情况下,我会在完成后将它们从内存中清除,并在重新访问该分子时从存档中重新加载。但我还不知道如何做这些中的任何一个。在投入更多时间感到沮丧之前,我会很感激对此的评论。
球体、胶囊体和圆柱体都有相当密集的网格。你需要那么多细节吗?尝试减少各种段数属性(segmentCount
、radialSegmentCount
等)。作为快速测试,将 SCNPyramid
替换为所有基元类型(即向量计数最低的基元)。如果这是一个因素,您应该会看到内存使用量显着减少(它看起来很难看,但会立即反馈您是否在可用轨道上)。你能用长的 SCNBox
而不是圆柱体吗?
另一个优化步骤是使用 SCNLevelOfDetail
以在对象距离较远时允许替代、低顶点数几何体。这比简单地统一减少段数要多做一些工作,但如果您有时需要更多细节,将会有所回报。
与其自己管理数组中的组件,不如使用节点层次结构来做到这一点。将每个分子或分子的可动画部分创建为 SCNNodes
的树。给它起个名字。做一个flattenedClone
。现在存档。在需要时从存档中读取节点树;不用担心节点数组。
考虑编写两个程序。一个是 iOS 程序 manipulates/displays 分子。另一个是 Mac(或 iOS?)生成分子节点树并将其存档的程序。这将为您提供一堆 SCNNode 树存档,您可以将其作为资源嵌入到您的显示程序中,无需即时生成。
对 materials
或 firstMaterial
属性?)以释放节点。似乎值得一看,但由于您只是使用 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 中的内存块相似(C3DGenericSourceCreateDeserializedDataWithAccessors
、C3DMeshSourceCreateMutable
等)。我发现在 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
获得更好的成功,如