正如评论中所讨论的,该问题可能与强引用循环有关。
下一步
- 重新创建一个简单的游戏,其中场景已正确释放,但某些节点未正确释放。
- 我会多次重新加载场景。您将看到场景已正确释放,但场景中的某些节点未正确释放。每次我们用新场景替换旧场景时,这都会导致更大的内存消耗。
- 我将向您展示如何使用 Instruments 查找问题根源
- 最后我将向您展示如何解决该问题。
1. 让我们创建一个有内存问题的游戏
让我们用 Xcode 创建一个基于 SpriteKit 的新游戏。
我们需要创建一个新文件Enemy.swift
包含以下内容
import SpriteKit
class Enemy: SKNode {
private let data = Array(0...1_000_000) // just to make the node more memory consuming
var friend: Enemy?
override init() {
super.init()
print("Enemy init")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print("Enemy deinit")
}
}
我们还需要替换的内容Scene.swift
具有以下源代码
import SpriteKit
class GameScene: SKScene {
override init(size: CGSize) {
super.init(size: size)
print("Scene init")
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
print("Scene init")
}
override func didMove(to view: SKView) {
let enemy0 = Enemy()
let enemy1 = Enemy()
addChild(enemy0)
addChild(enemy1)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let newScene = GameScene(size: self.size)
self.view?.presentScene(newScene)
}
deinit {
print("Scene deinit")
}
}
正如您所看到的,游戏的设计目的是在用户每次点击屏幕时用新场景替换当前场景。
让我们开始游戏并查看控制台。等着瞧
Scene init
Enemy init
Enemy init
这意味着我们总共有 3 个节点。
现在让我们点击屏幕,再次查看控制台
Scene init
Enemy init
Enemy init
Scene init
Enemy init
Enemy init
Scene deinit
Enemy deinit
Enemy deinit
我们可以看到创建了一个新场景和 2 个新敌人(第 4、5、6 行)。最后,旧场景被解除分配(第 7 行),2 个旧敌人被解除分配(第 8 行和第 9 行)。
所以内存中还有 3 个节点。而且这也好,我们没有内存韭菜。
如果我们使用 Xcode 监控内存消耗,我们可以验证每次重新启动场景时内存需求没有增加。
2.创建一个强引用循环
我们可以像下面这样更新 Scene.swift 中的 didMove 方法
override func didMove(to view: SKView) {
let enemy0 = Enemy()
let enemy1 = Enemy()
// ☠️☠️☠️ this is a scary strong retain cycle ☠️☠️☠️
enemy0.friend = enemy1
enemy1.friend = enemy0
// **************************************************
addChild(enemy0)
addChild(enemy1)
}
正如你所看到的,我们现在有一个敌人0和敌人1之间的强循环.
让我们再次运行游戏。
如果现在我们点击屏幕并查看控制台,我们会看到
Scene init
Enemy init
Enemy init
Scene init
Enemy init
Enemy init
Scene deinit
正如您所看到的,场景已被释放,但敌人不再从内存中删除。
我们来看看 Xcode 内存报告
现在,每次我们用新场景替换旧场景时,内存消耗都会增加。
3. 查找 Instruments 的问题
当然,我们确切地知道问题出在哪里(我们在一分钟前添加了强保留周期)。但是我们如何在一个大项目中检测到强大的保留周期呢?
单击 Xcode 中的 Instrument 按钮(当游戏在模拟器中运行时)。
让我们点击Transfer
在下一个对话框中。
现在我们需要选择Leak Checks
好的,此时一旦检测到泄漏,它就会出现在 Instruments 的底部。
4.让泄漏发生
返回模拟器并再次点击。场景将再次被替换。
返回到Instruments
,等几秒钟然后...
这是我们的泄漏。
让我们扩展一下。
Instruments 准确地告诉我们 8 个 Enemy 类型的对象已被泄露。
我们还可以选择视图 Cycles 和 Root 和 Instrument 将向我们展示这一点
这就是我们强大的保留周期!
具体来说,仪器显示 4 个强保留周期(由于我点击模拟器屏幕 4 次,总共泄漏了 8 个敌人)。
5. 解决问题
现在我们知道问题出在 Enemy 类上,我们可以返回到我们的项目并解决问题。
我们可以简单地使friend
财产weak
.
让我们更新一下Enemy
class.
class Enemy: SKNode {
private let data = Array(0...1_000_000)
weak var friend: Enemy?
...
我们可以再次检查以验证问题是否已消失。