使用MKLookAroundSnapshotter时Xcode Playground突然崩溃问题
批量生成MKLookAround快照时Xcode崩溃的解决方法
问题概述
读取CSV文件中的地址,通过CLGeocoder转换为坐标后,调用MKLookAroundSnapshotter批量生成快照并保存。单个坐标处理正常,但批量处理1000个坐标时,保存1-2张图片后Xcode会无报错崩溃。
问题根源分析
- 异步与同步逻辑冲突:原代码用
DispatchGroup处理地理编码的异步任务,但后续快照生成逻辑直接在主线程通过sleep(5)等待,无法保证coordinatesWithAddress数组已完全填充,且阻塞主线程导致系统资源耗尽。 - 无限制并发请求:批量生成快照时直接循环发起异步请求,未控制并发数,
MKLookAroundSnapshotter的资源占用超出系统限制,触发崩溃。 - 非法文件名:直接使用原始地址作为文件名,地址中可能包含
/、:等非法字符,导致文件写入失败,进而引发崩溃。 - 主线程阻塞:
sleep()函数会阻塞主线程,长时间阻塞会触发系统看门狗机制,强制终止进程。
修复方案
- 统一使用
async/await重构异步逻辑:用异步地理编码方法替代回调式API,确保坐标数组完全生成后再执行快照任务。 - 控制并发数:使用
TaskGroup限制快照生成的并发请求数量,避免资源过载。 - 处理文件名非法字符:对地址进行清洗,替换掉不能作为文件名的特殊字符。
- 后台线程执行IO操作:将文件写入等耗时操作放到后台队列,避免阻塞主线程。
完整修复代码
import Foundation import CoreLocation import MapKit import UIKit // 读取CSV文件 guard let path = Bundle.main.url(forResource: "tst1", withExtension: "csv") else { fatalError("无法找到CSV文件") } let cleanString = try! String(contentsOf: path).trimmingCharacters(in: .whitespacesAndNewlines) let rows = cleanString.components(separatedBy: "\n") // 地理编码:用async/await替代回调 func geocodeAddress(_ address: String) async throws -> (latitude: Double, longitude: Double, address: String) { let geocoder = CLGeocoder() guard let placemark = try await geocoder.geocodeAddressString(address).first, let location = placemark.location else { throw NSError(domain: "GeocodingError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无法解析地址:\(address)"]) } return (location.coordinate.latitude, location.coordinate.longitude, address) } // 生成LookAround快照 enum LookaroundError: Error { case unableToCreateScene } func snapshotImage(for coordinate: CLLocationCoordinate2D) async throws -> UIImage { guard let scene = try await MKLookAroundSceneRequest(coordinate: coordinate).scene else { throw LookaroundError.unableToCreateScene } let options = MKLookAroundSnapshotter.Options() options.size = CGSize(width: 512, height: 512) return try await MKLookAroundSnapshotter(scene: scene, options: options).snapshot.image } // 清洗文件名,替换非法字符 func sanitizedFileName(from address: String) -> String { let invalidCharacters = CharacterSet(charactersIn: "/\\?%*:|\"<>") return address.components(separatedBy: invalidCharacters).joined(separator: "_") } // 主异步任务 Task { do { // 1. 批量地理编码,控制并发数(避免触发地理编码频率限制) var coordinatesWithAddress: [(latitude: Double, longitude: Double, address: String)] = [] try await withThrowingTaskGroup(of: (Double, Double, String).self) { group in // 限制并发数为5,可根据实际情况调整 let maxConcurrent = 5 var submitted = 0 while submitted < rows.count { // 提交任务直到达到并发上限 while submitted < rows.count && group.tasks.count < maxConcurrent { let address = rows[submitted] group.addTask { try await geocodeAddress(address) } submitted += 1 } // 处理已完成的任务 if let result = try await group.next() { coordinatesWithAddress.append((result.0, result.1, result.2)) } } // 等待剩余任务完成 for try await result in group { coordinatesWithAddress.append((result.0, result.1, result.2)) } } // 2. 保存地理编码结果到CSV var csvText = "latitude,longitude,address\n" for coordinate in coordinatesWithAddress { let newLine = String(format: "%f,%f,%@\n", coordinate.latitude, coordinate.longitude, coordinate.address) csvText.append(newLine) } if let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { let fileURL = directory.appendingPathComponent("coordinates.csv") try csvText.write(to: fileURL, atomically: true, encoding: .utf8) print("CSV文件保存成功") } // 3. 批量生成并保存快照,控制并发数 try await withThrowingTaskGroup(of: Void.self) { group in let maxConcurrent = 3 // MKLookAround的并发限制更严格,建议调低 var submitted = 0 while submitted < coordinatesWithAddress.count { while submitted < coordinatesWithAddress.count && group.tasks.count < maxConcurrent { let coordinateData = coordinatesWithAddress[submitted] group.addTask { let image = try await snapshotImage(for: CLLocationCoordinate2D(latitude: coordinateData.latitude, longitude: coordinateData.longitude)) let sanitizedName = sanitizedFileName(from: coordinateData.address) let url = try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) .appending(component: "\(sanitizedName).png") guard let data = image.pngData() else { throw NSError(domain: "ImageError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无法转换图片为PNG"]) } try data.write(to: url) print("已保存图片:\(sanitizedName).png") } submitted += 1 } // 处理已完成的任务 try await group.next() } // 等待剩余任务完成 try await group.waitForAll() } print("所有任务完成") } catch { print("任务出错:\(error)") } }
关键优化点说明
- 并发控制:用
withThrowingTaskGroup限制地理编码和快照生成的并发数,避免触发API频率限制和资源过载。 - 异步流程同步:通过
Task和async/await确保地理编码完全完成后再执行快照任务,避免数组未填充就开始处理。 - 文件名处理:
sanitizedFileName函数替换非法字符,避免文件写入失败。 - 主线程释放:所有异步任务都在后台执行,不会阻塞主线程,避免看门狗杀死进程。
内容的提问来源于stack exchange,提问作者Pooriya Rezaei
相关产品推荐
相关产品推荐

