问题描述
在《方方块》游戏中,Xcode首次安装游戏后,第一次将方块放置到棋盘上时,就会出现卡顿的情况,后面的方块放置以及重新打开游戏,不会复现卡顿问题。

这一问题的推测是懒加载或者跟内存有关。
经过Xcode输出发现,可能跟下面的isGameOver方法有关。
func isGameOver() -> Bool {
// 棋盘尺寸
let rows = grid.count
let cols = grid[0].count
// 遍历当前的方块
for block in CurrentBlock.compactMap({ $0 }) {
print("进入循环测试卡顿")
// 方块行列
let blockRows = block.shape.count
let blockCols = block.shape[0].count
// 在棋盘的每个位置尝试放置方块
for startRow in 0...(rows - blockRows) {
for startCol in 0...(cols - blockCols) {
if canPlaceBlock(block, atRow: startRow, col: startCol) {
print("当前方块可以放置,游戏继续。")
return false
}
}
}
}
print("没有方块可以放置,游戏结束。")
return true
}
✓
当方块放置后,视图出现卡顿,卡顿1-2秒后,输出:
当前方块可以放置,游戏继续。
✓
因此,推断代码跟isGameOver方法有关系。
考虑到卡顿出现在“当前方块可以放置,游戏继续。”之前,因此推测是isGameOver方法的执行时间较长,特别是在 canPlaceBlock 这个循环内的计算可能导致了主线程阻塞。
尝试思路
在游戏启动时,手动预加载数据:
// 在游戏启动时调用
func preloadGameData() {
_ = isGameOver() // 预加载逻辑
print("游戏数据已预加载")
DispatchQueue.global(qos: .background).async {
_ = UserDefaults.standard.object(forKey: "some_key") // 预读取,减少主线程卡顿
}
print("确保 UserDefaults 放在后台线程")
}
VStack {
Text("游戏结束")
Button("重新开始") {
// 重新开始游戏
restartGame()
}
}
.onAppear(perform: preloadGameData)
✓
这一预加载代码实现了两个功能,一个是预加载isGameOver方法,另一个是预加载UserDefaults,避免第一次访问产生额外的开销。
使用这一预加载代码,重新测试游戏,发现问题仍然存在。
拆分代码
根据Xcode的输出,发现canPlaceBlock正常返回true后,出现卡顿,然后输出:“当前方块可以放置,游戏继续。”
因此,考虑拆分if canPlaceBlock方法:
if canPlaceBlock(block, atRow: startRow, col: startCol) {
print("当前方块可以放置,游戏继续。")
return false
}
✓
拆分为:
let isCanPlaceBlock = canPlaceBlock(block, atRow: startRow, col: startCol)
print("isCanPlaceBlock:\(isCanPlaceBlock)")
print("进入判断")
if isCanPlaceBlock {
print("当前方块可以放置,游戏继续。")
return false
}
✓
当canPlaceBlock方法正常返回,然后出现卡顿,那么问题可能就不在这个地方。
拆分代码后,重新运行,Xcode在进入canPlaceBlock方法并返回true后,出现卡顿,同时Xcode输出报错信息:
Invalid frame dimension (negative or non-finite).
✓

在输出一连串报错信息后,输出:
需要清除的行: []
rowsToClear.sorted().reversed():[]
rowsToClear.sorted().enumerated():[]
rowsToClear.sorted().reversed().enumerated():[]
进入循环测试卡顿
进入canPlaceBlock方法
✓
经过代码定位,发现输出报错信息后的输出内容为clearFullRowsAndColumns方法,这个方法的作用是用于消除整行/整列的方法。
这也意味着当前卡顿原因从isGameOver方法变成了clearFullRowsAndColumns方法。
通过查询发现,“Invalid frame dimension (negative or non-finite)”报错意味着SwiftUI在渲染视图时遇到了非法的尺寸,,例如 负数、高度/宽度为 NaN(非数值)或无限大。
排查调用方法
但目前还是没有定位到具体的代码,因此尝试在clearFullRowsAndColumns方法中,添加输出定位。
// 消除整行/整列的方块
func clearFullRowsAndColumns() {
print("进入clearFullRowsAndColumns方法")
var newGrid = grid
print("创建newGrid")
// 找到需要消除的行
var rowsToClear: Set<Int> = []
print("新增rowsToClear方法")
for row in 0..<9 {
if newGrid[row].allSatisfy({ $0 == 1 }) {
rowsToClear.insert(row)
}
}
print("补充rowsToClear方法")
// 找到需要消除的列
print("新增colsToClear方法")
var colsToClear: Set<Int> = []
for col in 0..<9 {
if (0..<9).allSatisfy({ newGrid[$0][col] == 1 }) {
colsToClear.insert(col)
}
}
print("补充colsToClear方法")
if !rowsToClear.isEmpty || !colsToClear.isEmpty {
if appStorage.Music {
sound.playSound(named: "clearSoundeffects")
}
}
print("需要清除的行: \(rowsToClear.sorted())")
// 其他代码
}
✓
重新测试,发现应用仍然卡顿,并且在卡顿的过程中没有输出clearFullRowsAndColumns方法中的print。
因此,我把问题重新定位到调用clearFullRowsAndColumns方法的地方:
DraggableBlockView(block: block,
GestureOffset: GestureOffset,
onDrop: {start, end, geo in
print("放置方块")
print("gridOrigin:\(gridOrigin)")
placeBlock(block, start, end, geo, item)
// 放置方块
print("设置shadowPosition和shadowBlock为nil")
shadowPosition = nil
shadowBlock = nil
// 消除行、列的方块
print("进入clearFullRowsAndColumns方法")
clearFullRowsAndColumns()
// 其他代码
})
✓
在放置方块时,会进入到onDrop方法,然后在onDrop方法中调用clearFullRowsAndColumns方法。
在设置print输出后,重新运行Xcode安装应用。
经过定位发现,当卡顿结束后,输出
print("设置shadowPosition和shadowBlock为nil")
✓
在卡顿前输出的是:
放置方块
gridOrigin:(7.666666666666666, 216.33333333333331)
进入placeBlock方法
start:(25.666661580403655, 51.666666666666686)
end:(-1.0, -127.33334350585938)
origins:(147.66666666666666, 626.3333333333333)
gridOrigin:(7.666666666666666, 216.33333333333331)
touchOffsetX:147.66666666666666
touchOffsetY:626.3333333333333
row:5, col: 3
进入canPlaceBlock方法
计算出的 row:5, col:3
当前可放置的方块为:Block(shape: [[1, 0], [1, 1]])
可放置行数为:5
可放置列数为:3
返回true
✓
Xcode在placeBlock方法中输出到一半停止了,因此推断问题跟placeBlock方法有关。
给placeBlock方法添加输出定位:
// 放置方块
func placeBlock(_ block: Block, _ start: CGPoint, _ end: CGSize, _ origins: CGPoint, _ indices : Int) {
if isCanPlaceBlock{
// 其他代码
print("棋盘遍历完成")
// 播放放置方块音效
print("播放音效")
if appStorage.Music {
sound.playSound(named: "blockSound")
}
// 计算得分
print("计算得分")
} else {
print("无法放置方块")
}
}
✓
经过添加print后,Xcode重新安装应用,定位到卡顿代码:
播放音效
--- 发生卡顿 ---
计算得分
✓
这也就意味着可能是sound.playSound方法导致的问题。
为了进一步明确,在隐藏sound.playSound方法后,重新使用Xcode安装应用。

卡顿问题得到解决。
问题分析
最终,通过层层的代码筛选和设置print定位,发现卡顿的原因是由于AVAudioPlayer创建导致的卡顿。
由于 AVAudioPlayer 的初始化 (AVAudioPlayer(contentsOf:)) 是 IO 操作,会造成主线程的短暂卡顿,特别是在应用刚安装后第一次读取音频文件时。
在Xcode中,找到SoundManager方法,添加print输出测试:
class SoundManager {
static let shared = SoundManager() // 单例
var player: AVAudioPlayer?
func playSound(named soundName: String) {
print("进入playSound方法")
if let url = Bundle.main.url(forResource: soundName, withExtension: "mp3") {
print("查找\(soundName)文件")
do {
print("尝试设置AVAudioPlayer")
self.player = try AVAudioPlayer(contentsOf: url)
print("播放\(soundName)文件")
self.player?.play()
} catch {
print("播放失败: \(error.localizedDescription)")
}
} else {
print("找不到音效文件: \(soundName)")
}
}
}
✓
最终,定位问题为try AVAudioPlayer(contentsOf: url)代码,当Xcode输出“尝试设置AVAudioPlayer”后,出现卡顿。卡顿结束,输出“播放blockSound文件”。
总结
最终的结局方案就是,预加载 AVAudioPlayer,避免重复初始化。
import AVFoundation
class SoundManager {
static let shared = SoundManager() // 单例
private var players: [String: AVAudioPlayer] = [:]
private init() {
preloadSounds(["blockSound"]) // 预加载音效
}
private func preloadSounds(_ soundNames: [String]) {
for soundName in soundNames {
if let url = Bundle.main.url(forResource: soundName, withExtension: "mp3") {
do {
let player = try AVAudioPlayer(contentsOf: url)
player.prepareToPlay() // 预加载到缓存
players[soundName] = player
print("预加载音效: \(soundName)")
} catch {
print("预加载失败: \(soundName), 错误: \(error.localizedDescription)")
}
} else {
print("找不到音效文件: \(soundName)")
}
}
}
func playSound(named soundName: String) {
DispatchQueue.global(qos: .userInitiated).async {
if let player = self.players[soundName] {
player.currentTime = 0
DispatchQueue.main.async {
player.play()
}
} else {
print("音效未加载: \(soundName)")
}
}
}
}
✓
在preloadSounds中添加需要加载的音频文件。
在Xcode入口文件中,设置共享示例:
import SwiftUI
@main
struct FaockApp: App {
@StateObject private var sound = SoundManager.shared
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(sound)
}
}
}
✓
在视图文件中,使用@ EnvironmentObject注入。
struct Game: View {
@EnvironmentObject var sound: SoundManager // 通过 Sound 注入
func clearFullRowsAndColumns() {
// 其他代码
sound.playSound(named: "clearSoundeffects")
}
var body: some View {
...
}
}
#Preview {
Game(viewStep: .constant(1))
.environmentObject(SoundManager.shared)
}
✓
通过预加载音频文件:游戏启动时就创建 AVAudioPlayer,解码一次,避免首次播放时解码导致卡顿。
问题得到解决。