You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 02:01:29