Thread 1: EXC_BAD_INSTRUCTION错误与Firebase专辑数据获取异常排查
问题解答:DispatchGroup异常原因及Async/Await改造方案
一、DispatchGroup异常的原因
你的代码出现随机错误和数据显示异常,核心问题出在DispatchGroup的使用和全局状态管理上:
- Enter/Leave调用顺序混乱:你在
unlocked文档的回调里才调用group.enter(),但在songs集合的回调里直接执行group.leave(),这会导致leave()调用时group还没有对应的计数,当leave()次数超过enter()次数时,就会触发EXC_BAD_INSTRUCTION错误。 - 全局状态被并发覆盖:
albumSongs是全局的@State数组,多个专辑的songs请求回调会同时修改这个变量,导致构造Album时拿到的是其他专辑的歌曲数据,出现歌曲错位、重复显示的问题。 - 全局DispatchGroup导致计数混乱:所有专辑的异步请求共享同一个全局group,无法精准跟踪每个专辑的任务完成状态,多专辑场景下计数完全混乱。
- UI操作不在主队列:
group.notify指定了后台队列,但修改SwiftUI的@State变量必须在主队列执行,这会引发视图刷新时的异常。
二、用Async/Await改造代码
Firebase Firestore从iOS 15+开始支持async/await API,完全可以替代回调和DispatchGroup,解决你的问题。以下是完整的改造代码:
1. 保留原有结构体(无需修改)
struct AlbumSong: Identifiable, Codable { @DocumentID var id: String? = UUID().uuidString let title: String let duration: TimeInterval var image: String let artist: String let track: String } struct Album: Identifiable, Codable { @DocumentID var id: String? = UUID().uuidString let name: String let artist: String let songs: [AlbumSong] let releaseDate: Date let price: Int let albumImageUrl: String }
2. 重构fetchAlbums为Async函数
@State var albums: [Album] = [] func fetchAlbums() async { do { // 获取所有专辑文档 let albumsSnapshot = try await FirebaseManager.shared.firestore.collection("albums").getDocuments() guard let uid = FirebaseManager.shared.auth.currentUser?.uid else { return } var fetchedAlbums: [Album] = [] for albumDoc in albumsSnapshot.documents { // 解析专辑基础信息 let data = albumDoc.data() let name = data["name"] as? String ?? "" let artist = data["artist"] as? String ?? "" let releaseDate = data["releaseDate"] as? Date ?? Date() let price = data["price"] as? Int ?? 0 let albumImageUrl = data["albumImageUrl"] as? String ?? "" let docID = albumDoc.documentID // 并行发起歌曲和解锁状态的请求,提升效率 async let songsTask: [AlbumSong] = fetchSongs(forAlbumID: docID) async let unlockedTask: Bool = fetchUnlockedStatus(forAlbumID: docID, uid: uid) // 等待两个请求完成 let (songs, isUnlocked) = await (songsTask, unlockedTask) // 仅添加已解锁的专辑 if isUnlocked { let album = Album( name: name, artist: artist, songs: songs, releaseDate: releaseDate, price: price, albumImageUrl: albumImageUrl ) fetchedAlbums.append(album) } } // 回到主队列更新UI状态(SwiftUI要求UI操作必须在主队列) await MainActor.run { self.albums = fetchedAlbums } } catch { print("获取专辑数据失败:\(error.localizedDescription)") } } // 封装获取专辑歌曲的异步函数 private func fetchSongs(forAlbumID albumID: String) async -> [AlbumSong] { do { let songsSnapshot = try await FirebaseManager.shared.firestore .collection("albums") .document(albumID) .collection("songs") .getDocuments() return songsSnapshot.documents.compactMap { doc in try? doc.data(as: AlbumSong.self) } } catch { print("获取专辑歌曲失败:\(error.localizedDescription)") return [] } } // 封装获取专辑解锁状态的异步函数 private func fetchUnlockedStatus(forAlbumID albumID: String, uid: String) async -> Bool { do { let unlockedDoc = try await FirebaseManager.shared.firestore .collection("albums") .document(albumID) .collection("unlocked") .document(uid) .getDocument() return unlockedDoc.data()?["locked"] as? Bool ?? false } catch { print("获取解锁状态失败:\(error.localizedDescription)") return false } }
3. 在视图中调用Async函数
.onAppear { Task { await fetchAlbums() } }
改造核心优势
- 避免回调地狱:async/await让异步代码逻辑线性化,可读性和维护性大幅提升。
- 并行任务优化:用
async let同时发起歌曲和解锁状态请求,比串行请求更快完成数据获取。 - 状态隔离:用局部数组
fetchedAlbums存储临时结果,避免全局变量被并发覆盖,确保每个专辑的歌曲数据独立。 - 线程安全:用
MainActor.run确保UI状态修改在主队列执行,符合SwiftUI的线程规范。 - 错误统一处理:do-catch块集中捕获所有异步请求的错误,便于调试和异常处理。
内容的提问来源于stack exchange,提问作者AdamLeet
相关产品推荐
相关产品推荐

