如何高效加载显示MapView各缩放层级对应的瓦片图层?
MKMapView 瓦片叠加层性能优化方案
一、预缓存瓦片实现方案
你可以通过自定义MKTileOverlay子类实现全缩放级别瓦片预缓存,在启动阶段后台完成加载:
- 子类化
MKTileOverlay,重写瓦片加载方法,内置本地缓存逻辑(可使用NSCache+沙盒文件存储两级缓存),优先读取本地缓存,无缓存再发起网络/本地资源请求,请求完成后写入缓存 - 应用启动阶段开启后台并发队列,遍历你所需覆盖区域的所有缩放级别、所有瓦片坐标,主动调用瓦片加载方法将数据提前写入缓存,注意控制并发数避免线程阻塞或接口限流
- 示例代码:
class CachedTileOverlay: MKTileOverlay { private let cache = NSCache<NSString, NSData>() private let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].appendingPathComponent("TileCache") override init(urlTemplate URLTemplate: String?) { super.init(urlTemplate: URLTemplate) try? FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true) } override func loadTile(at path: MKTileOverlayPath, resultHandler: @escaping (Data?, Error?) -> Void) { let key = "\(path.z)/\(path.x)/\(path.y)" as NSString // 先查内存缓存 if let data = cache.object(forKey: key) as? Data { resultHandler(data, nil) return } // 再查沙盒缓存 let fileUrl = cacheDir.appendingPathComponent("\(path.z)_\(path.x)_\(path.y)") if let data = try? Data(contentsOf: fileUrl) { cache.setObject(data as NSData, forKey: key) resultHandler(data, nil) return } // 无缓存走原加载逻辑 super.loadTile(at: path) { [weak self] data, error in guard let data = data, error == nil else { resultHandler(nil, error) return } self?.cache.setObject(data as NSData, forKey: key) try? data.write(to: fileUrl) resultHandler(data, nil) } } // 预缓存调用方法 func preloadTile(z: Int, x: Int, y: Int) async { let path = MKTileOverlayPath(x: x, y: y, z: z, contentScaleFactor: UIScreen.main.scale) return await withCheckedThrowingContinuation { con in loadTile(at: path) { data, error in if let error = error { con.resume(throwing: error) } else { con.resume() } } } } }
二、现有代码核心优化点
你当前的切换逻辑存在严重性能损耗,是卡顿的核心原因:
每次切换都移除所有叠加层再重新添加,会触发渲染器重建、瓦片重新加载,完全可以避免
- 优化切换逻辑:提前将所有9组共18个叠加层全部添加到地图中,切换时仅修改对应叠加层的
isHidden属性即可,无需反复增删 - 优化渲染器逻辑:缓存已创建的渲染器实例,无需每次回调都新建对象
- 优化后切换代码示例:
// 初始化阶段提前添加所有叠加层 func setupOverlays() { for i in 0..<9 { let euOverlay = europeOverlays[i] euOverlay.isHidden = i != 0 mapView.addOverlay(euOverlay, level: .aboveRoads) let naOverlay = northAmericaOverlays[i] naOverlay.isHidden = i != 0 mapView.addOverlay(naOverlay, level: .aboveRoads) } currentOverlay = 0 } @objc func changeOverlay() { // 隐藏当前层 europeOverlays[currentOverlay].isHidden = true northAmericaOverlays[currentOverlay].isHidden = true // 计算下一层索引 currentOverlay = (currentOverlay + 1) % 9 // 显示新层 europeOverlays[currentOverlay].isHidden = false northAmericaOverlays[currentOverlay].isHidden = false overlayTimeLabel.text = overlayTimeArray[currentOverlay] }
- 优化后渲染器代码示例:
private var rendererCache = [MKOverlay: MKTileOverlayRenderer]() func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { if let renderer = rendererCache[overlay] { return renderer } let renderer = MKTileOverlayRenderer(overlay: overlay) renderer.alpha = 0.8 rendererCache[overlay] = renderer return renderer }
三、其他通用优化建议
- 如果瓦片资源是固定的,直接打包到App安装包内,走本地文件加载,速度远高于网络请求
- 若你的瓦片叠加层完全覆盖对应区域的底图内容,将
MKTileOverlay的canReplaceMapContent属性设为true,系统会跳过对应区域底图渲染,大幅降低渲染开销 - 瓦片导出时可直接调整透明度到0.8,避免渲染时实时计算alpha值消耗GPU性能
- 预缓存阶段仅缓存用户可能用到的区域的瓦片,不要无差别全量缓存,避免占用过多存储空间和加载时间
内容的提问来源于stack exchange,提问作者this-isa-stickup
相关产品推荐
相关产品推荐

