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

旋转MKMapView时Image MKOverlay崩溃且内存暴涨问题求助

问题:MKMapView图片Overlay旋转时内存暴涨崩溃

尝试在MKMapView中添加地理范围约1km的单张图片作为Overlay,目标是每秒更新一次图片。目前图片能正确绘制,移动、缩放正常,但旋转地图时屏幕锁定,内存以每秒10MB速率持续增长,直至耗尽内存退出,模拟器和真机均出现此问题,且期间无自定义代码调用、无输出。改为UIKit原生地图应用可正常旋转,最简测试代码同样崩溃。

测试代码

class ImageOverlay : NSObject, MKOverlay {

    let image:UIImage
    let boundingMapRect: MKMapRect
    let coordinate:CLLocationCoordinate2D

    init(image: UIImage, rect: MKMapRect) {
        self.image = image
        self.boundingMapRect = rect
        var coord = rect.origin
        coord.x += rect.size.width / 2
        coord.y += rect.size.height / 2;
        self.coordinate = CLLocationCoordinate2D(latitude: coord.x, longitude: coord.y)
    }
}

class ImageOverlayRenderer : MKOverlayRenderer {

    override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext)
    {
        guard let overlay = self.overlay as? ImageOverlay else {
            return
        }
        let rect = self.rect(for: overlay.boundingMapRect)
        context.saveGState()
        context.setAlpha(0.6)
        context.draw(overlay.image.cgImage!, in: rect, byTiling: false)
        context.flush()
        context.restoreGState()
    }
}

struct MapView: UIViewRepresentable {
    @Binding var imgOver: ImageOverlay?

    let mapViewDelegate = MapViewDelegate()
    
    func makeUIView(context: Context) -> MKMapView {
        MKMapView(frame:.zero)
    }
    
    func updateUIView(_ view: MKMapView, context: Context) {
        view.delegate = mapViewDelegate 
        view.translatesAutoresizingMaskIntoConstraints = false  
        view.mapType = .hybrid
        addImage(to: view)
    }
}

 extension MapView {
    
    func addImage(to view: MKMapView) {
        if !view.overlays.isEmpty {
            view.removeOverlays(view.overlays)
        }
        guard let img = imgOver else { return }
        let mapRect = img.boundingMapRect
        view.setVisibleMapRect(mapRect, edgePadding: UIEdgeInsets(top: 100, left: 100, bottom: 100, right: 100), animated: true)
        view.addOverlay(img)
    }
}

class MapViewDelegate: NSObject, MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        ImageOverlayRenderer(overlay: overlay)
    }
}

struct ContentView: View {
    @State var imageOverlay: ImageOverlay?

    var body: some View {
        MapView(imgOver:$imageOverlay)
                .onAppear {
                    let image = UIImage(named: "portland.png")!
                    let p1 =  MKMapPoint( CLLocationCoordinate2DMake(50.564479946138384, -2.4423953407134054))
                    let p2 = MKMapPoint( CLLocationCoordinate2DMake(50.57264968724567, -2.415437272738347))
                    let bounds = MKMapRect(x: fmin(p1.x,p2.x), y: fmin(p1.y,p2.y), width: fabs(p1.x-p2.x), height: fabs(p1.y-p2.y));
                    self.showImage(image:image, bounds:bounds )
        }
    }
}

private extension ContentView {
    func showImage(image:UIImage?, bounds:MKMapRect) {
        if image == nil {
            return
        }
        self.imageOverlay = ImageOverlay(image:image!, rect:bounds)
    }
}

问题根源分析

  1. 坐标计算错误:直接将MKMapPoint的x/y值作为经纬度,属于概念混淆,会导致Overlay的坐标逻辑异常,间接引发绘制时的额外计算开销。
  2. Renderer绘制逻辑缺陷:每次draw调用都直接访问overlay.image.cgImage!,旋转地图时MapKit会高频触发draw,而UIImage.cgImage每次访问都会生成新的CGImage实例,且未被正确释放,导致内存持续暴涨。
  3. SwiftUI生命周期滥用:updateUIView会在视图状态变化(包括旋转)时频繁调用,每次都重复设置代理、移除并添加Overlay,加剧内存压力和不必要的对象创建。

修复方案

1. 修正ImageOverlay的坐标计算

将MKMapRect中心的投影坐标正确转换为经纬度:

class ImageOverlay : NSObject, MKOverlay {
    let image: UIImage
    let boundingMapRect: MKMapRect
    let coordinate: CLLocationCoordinate2D

    init(image: UIImage, rect: MKMapRect) {
        self.image = image
        self.boundingMapRect = rect
        // 正确转换投影坐标为经纬度
        let centerPoint = MKMapPoint(x: rect.midX, y: rect.midY)
        self.coordinate = centerPoint.coordinate
        super.init()
    }
}

2. 优化Renderer的CGImage缓存

提前缓存CGImage,避免每次绘制都生成新实例:

class ImageOverlayRenderer : MKOverlayRenderer {
    private let cachedCGImage: CGImage
    
    init(overlay: ImageOverlay) {
        self.cachedCGImage = overlay.image.cgImage!
        super.init(overlay: overlay)
    }

    override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
        let rect = self.rect(for: overlay.boundingMapRect)
        context.saveGState()
        context.setAlpha(0.6)
        context.draw(cachedCGImage, in: rect, byTiling: false)
        context.restoreGState()
        // 移除不必要的context.flush(),MapKit会自行管理上下文刷新
    }
}

3. 规范UIViewRepresentable的生命周期

使用Coordinator管理代理,仅在Overlay变化时更新:

struct MapView: UIViewRepresentable {
    @Binding var imgOver: ImageOverlay?

    // 用Coordinator托管代理,避免重复创建
    class Coordinator: NSObject, MKMapViewDelegate {
        func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
            guard let imageOverlay = overlay as? ImageOverlay else {
                return MKOverlayRenderer(overlay: overlay)
            }
            return ImageOverlayRenderer(overlay: imageOverlay)
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator()
    }
    
    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView(frame: .zero)
        mapView.delegate = context.coordinator
        mapView.translatesAutoresizingMaskIntoConstraints = false
        mapView.mapType = .hybrid
        return mapView
    }
    
    func updateUIView(_ view: MKMapView, context: Context) {
        // 仅当Overlay实例变化时才更新
        if let newOverlay = imgOver {
            if view.overlays.first !== newOverlay {
                view.removeOverlays(view.overlays)
                view.addOverlay(newOverlay)
                let mapRect = newOverlay.boundingMapRect
                view.setVisibleMapRect(mapRect, edgePadding: UIEdgeInsets(top: 100, left: 100, bottom: 100, right: 100), animated: true)
            }
        } else {
            view.removeOverlays(view.overlays)
        }
    }
}

内存泄漏调试方法

  • Memory Graph Debugger:点击Xcode调试栏的内存图标,查看对象引用关系,定位未释放的CGImage、Overlay或Renderer实例,找到泄漏点。
  • Instruments Allocations模板:运行Allocations工具,旋转地图后查看内存增长的对象类型(如CGImage数量持续增加),通过调用栈追踪创建来源。
  • 日志与断点:在draw、addImage方法中添加打印日志,观察旋转时的调用频率,判断是否存在重复执行逻辑。
  • 对比UIKit测试:将SwiftUI封装的MapView替换为纯UIKit视图控制器测试,快速区分是SwiftUI生命周期问题还是MapKit代码问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 17:20:29