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

使用MKLookAroundSnapshotter时Xcode Playground突然崩溃问题

批量生成MKLookAround快照时Xcode崩溃的解决方法

问题概述

读取CSV文件中的地址,通过CLGeocoder转换为坐标后,调用MKLookAroundSnapshotter批量生成快照并保存。单个坐标处理正常,但批量处理1000个坐标时,保存1-2张图片后Xcode会无报错崩溃。

问题根源分析

  1. 异步与同步逻辑冲突:原代码用DispatchGroup处理地理编码的异步任务,但后续快照生成逻辑直接在主线程通过sleep(5)等待,无法保证coordinatesWithAddress数组已完全填充,且阻塞主线程导致系统资源耗尽。
  2. 无限制并发请求:批量生成快照时直接循环发起异步请求,未控制并发数,MKLookAroundSnapshotter的资源占用超出系统限制,触发崩溃。
  3. 非法文件名:直接使用原始地址作为文件名,地址中可能包含/、:等非法字符,导致文件写入失败,进而引发崩溃。
  4. 主线程阻塞:sleep()函数会阻塞主线程,长时间阻塞会触发系统看门狗机制,强制终止进程。

修复方案

  1. 统一使用async/await重构异步逻辑:用异步地理编码方法替代回调式API,确保坐标数组完全生成后再执行快照任务。
  2. 控制并发数:使用TaskGroup限制快照生成的并发请求数量,避免资源过载。
  3. 处理文件名非法字符:对地址进行清洗,替换掉不能作为文件名的特殊字符。
  4. 后台线程执行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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 14:35:53