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

iOS14.2调用SKCloudServiceController.requestUserToken致APP冻结求助

问题根源:主线程死锁导致APP冻结

你的APP之所以每次运行都会冻结,核心原因是你在主线程中使用DispatchSemaphore.wait()阻塞了线程,而异步回调无法触发signal(),最终造成死锁。

具体来说:

  • SKCloudServiceController.requestUserToken、URLSession.dataTask这类API都是异步执行的,它们的回调会在后台队列(或主线程队列)触发。
  • 当你在主线程调用lock.wait()时,主线程会进入休眠状态,完全无法处理任何事件循环(包括UI更新、系统回调)。
  • 如果requestUserToken的回调默认需要回到主线程执行(很多系统API的回调都是如此),那主线程已经被wait()卡住了,永远无法执行回调里的lock.signal(),直接导致死锁——APP彻底失去响应。

这个问题和Xcode 12.2的更新无关,是异步代码同步化的典型错误用法。


修复方案:改用异步回调/Async/Await

正确的做法是放弃用信号量强行同步异步代码,改用异步回调或者iOS 15+支持的async/await,让主线程始终保持响应。

方案1:基于Completion Handler的异步重构

把所有同步方法改成带回调的异步版本,避免阻塞主线程:

class AppleMusicAPI { 
    let developerToken = "b'eyJ0{...}RDlRSlFw'"
    
    // 重构getUserToken为异步回调
    func getUserToken(completion: @escaping (String?) -> Void) { 
        SKCloudServiceController().requestUserToken(forDeveloperToken: developerToken) { receivedToken, error in 
            completion(receivedToken) 
        } 
    } 
    
    // 重构fetchStorefrontID为异步回调,依赖getUserToken的结果
    func fetchStorefrontID(completion: @escaping (String?) -> Void) { 
        getUserToken { userToken in
            guard let userToken = userToken else {
                completion(nil)
                return
            }
            
            let musicURL = URL(string: "https://api.music.apple.com/v1/me/storefront")! 
            var musicRequest = URLRequest(url: musicURL) 
            musicRequest.httpMethod = "GET" 
            musicRequest.addValue("Bearer \(self.developerToken)", forHTTPHeaderField: "Authorization") 
            musicRequest.addValue(userToken, forHTTPHeaderField: "Music-User-Token") 
            
            URLSession.shared.dataTask(with: musicRequest) { data, response, error in 
                guard error == nil, let data = data else { 
                    completion(nil)
                    return 
                } 
                if let json = try? JSON(data: data) { 
                    let storefrontID = json["data"].array?[0]["id"].stringValue
                    completion(storefrontID) 
                } else {
                    completion(nil)
                }
            }.resume() 
        }
    } 
    
    // 重构searchAppleMusic为异步回调
    func searchAppleMusic(_ searchTerm: String, completion: @escaping ([Song]) -> Void) { 
        fetchStorefrontID { storefrontID in
            guard let storefrontID = storefrontID, !searchTerm.isEmpty else {
                completion([])
                return
            }
            
            let encodedTerm = searchTerm.replacingOccurrences(of: " ", with: "+")
            let musicURL = URL(string: "https://api.music.apple.com/v1/catalog/\(storefrontID)/search?term=\(encodedTerm)&types=songs&limit=25")! 
            var musicRequest = URLRequest(url: musicURL) 
            musicRequest.httpMethod = "GET" 
            musicRequest.addValue("Bearer \(self.developerToken)", forHTTPHeaderField: "Authorization") 
            
            self.getUserToken { userToken in
                guard let userToken = userToken else {
                    completion([])
                    return
                }
                musicRequest.addValue(userToken, forHTTPHeaderField: "Music-User-Token") 
                
                URLSession.shared.dataTask(with: musicRequest) { data, response, error in 
                    guard error == nil, let data = data else { 
                        completion([])
                        return 
                    } 
                    
                    var songs = [Song]()
                    if let json = try? JSON(data: data) { 
                        let songResults = json["results"]["songs"]["data"].array ?? []
                        for song in songResults { 
                            let attributes = song["attributes"]
                            guard let songID = attributes["playParams"]["id"].string,
                                  let songName = attributes["name"].string,
                                  let artistName = attributes["artistName"].string,
                                  let artworkURL = attributes["artwork"]["url"].string else {
                                continue
                            }
                            let currentSong = Song(id: songID, name: songName, artistName: artistName, artworkURL: artworkURL) 
                            songs.append(currentSong) 
                        } 
                    } 
                    completion(songs)
                }.resume() 
            }
        }
    } 
}

调用示例:

let api = AppleMusicAPI()
api.searchAppleMusic("Hello") { songs in
    // 拿到结果后回到主线程更新UI
    DispatchQueue.main.async {
        // 处理歌曲数据,比如刷新列表
    }
}

方案2:用Async/Await简化代码(iOS 15+)

如果你的项目支持iOS 15及以上,用async/await会让代码逻辑更清晰,可读性更高:

class AppleMusicAPI { 
    let developerToken = "b'eyJ0{...}RDlRSlFw'"
    
    func getUserToken() async -> String? {
        await withCheckedContinuation { continuation in
            SKCloudServiceController().requestUserToken(forDeveloperToken: developerToken) { token, error in
                continuation.resume(returning: token)
            }
        }
    }
    
    func fetchStorefrontID() async -> String? {
        guard let userToken = await getUserToken() else { return nil }
        
        let musicURL = URL(string: "https://api.music.apple.com/v1/me/storefront")! 
        var musicRequest = URLRequest(url: musicURL) 
        musicRequest.httpMethod = "GET" 
        musicRequest.addValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization") 
        musicRequest.addValue(userToken, forHTTPHeaderField: "Music-User-Token")
        
        do {
            let (data, _) = try await URLSession.shared.data(for: musicRequest)
            let json = try JSON(data: data)
            return json["data"].array?[0]["id"].stringValue
        } catch {
            return nil
        }
    }
    
    func searchAppleMusic(_ searchTerm: String) async -> [Song] {
        guard let storefrontID = await fetchStorefrontID(), !searchTerm.isEmpty else { return [] }
        
        let encodedTerm = searchTerm.replacingOccurrences(of: " ", with: "+")
        guard let musicURL = URL(string: "https://api.music.apple.com/v1/catalog/\(storefrontID)/search?term=\(encodedTerm)&types=songs&limit=25") else { return [] }
        
        var musicRequest = URLRequest(url: musicURL) 
        musicRequest.httpMethod = "GET" 
        musicRequest.addValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization")
        
        guard let userToken = await getUserToken() else { return [] }
        musicRequest.addValue(userToken, forHTTPHeaderField: "Music-User-Token")
        
        do {
            let (data, _) = try await URLSession.shared.data(for: musicRequest)
            let json = try JSON(data: data)
            let songResults = json["results"]["songs"]["data"].array ?? []
            
            return songResults.compactMap { song in
                let attributes = song["attributes"]
                guard let id = attributes["playParams"]["id"].string,
                      let name = attributes["name"].string,
                      let artistName = attributes["artistName"].string,
                      let artworkURL = attributes["artwork"]["url"].string else {
                    return nil
                }
                return Song(id: id, name: name, artistName: artistName, artworkURL: artworkURL)
            }
        } catch {
            return []
        }
    } 
}

调用示例:

let api = AppleMusicAPI()
Task {
    let songs = await api.searchAppleMusic("Hello")
    // 回到主线程更新UI
    DispatchQueue.main.async {
        // 处理歌曲数据
    }
}

关键提醒

永远不要在主线程调用DispatchSemaphore.wait(),这是阻塞主线程的大忌,会直接导致APP无响应。所有涉及网络请求、系统异步API的操作,都应该用异步方式处理,让主线程专注于UI渲染和用户交互。

内容的提问来源于stack exchange,提问作者Papi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.11 07:36:40