MKMapView with multiple overlays memory-issue

后端 未结 2 836
青春惊慌失措
青春惊慌失措 2020-12-29 15:22

There seems to be an \"issue\" with overlays and the MapKit. Unlike annotations, overlays aren\'t reused and therefore when adding multiple overlays would cause

相关标签:
2条回答
  • 2020-12-29 16:11

    The Answer to this is not "reusing" but to draw them all in to one MKOverlayView and then draw that on the map.

    Multiple MKPolygons, MKOverlays etc. cause heavy memory-usage when drawing on the map. This is due the NOT reusing of overlays by MapKit. As annotations have the reuseWithIdentifier, overlays however don't. Each overlay creates a new layer as a MKOverlayView on the map with the overlay in it. In that way memory-usage will rise quite fast and map-usage becomes... let's say sluggish to almost impossible.

    Therefore there is a work-around: Instead of plotting each overlay individually, you can add all of the MKOverlays to one MKOverlayView. This way you're in fact creating only one MKOverlayView and thus there's no need to reuse.

    This is the work-around, in this case for MKPolygons but should be not very different for others like MKCircles etc.

    create a class: MultiPolygon (subclass of NSObject)

    in MultiPolygon.h:

    #import <MapKit/MapKit.h> //Add import MapKit
    
    @interface MultiPolygon : NSObject <MKOverlay> {
     NSArray *_polygons;
     MKMapRect _boundingMapRect;
    }
    
    - (id)initWithPolygons:(NSArray *)polygons;
    @property (nonatomic, readonly) NSArray *polygons;
    
    @end
    

    in MultiPolygon.m:

    @implementation MultiPolygon
    
    @synthesize polygons = _polygons;
    
    - (id)initWithPolygons:(NSArray *)polygons
    {
     if (self = [super init]) {
        _polygons = [polygons copy];
    
        NSUInteger polyCount = [_polygons count];
         if (polyCount) {
            _boundingMapRect = [[_polygons objectAtIndex:0] boundingMapRect];
            NSUInteger i;
            for (i = 1; i < polyCount; i++) {
                _boundingMapRect = MKMapRectUnion(_boundingMapRect, [[_polygons objectAtIndex:i] boundingMapRect]);
            }
        }
     }
     return self;
    }
    
    - (MKMapRect)boundingMapRect
    {
     return _boundingMapRect;
    }
    
    - (CLLocationCoordinate2D)coordinate
    {
     return MKCoordinateForMapPoint(MKMapPointMake(MKMapRectGetMidX(_boundingMapRect), MKMapRectGetMidY(_boundingMapRect)));
    }
    
    @end
    

    Now create a class: MultiPolygonView (subclass of MKOverlayPathView)

    in MultiPolygonView.h:

    #import <MapKit/MapKit.h>
    
    @interface MultiPolygonView : MKOverlayPathView
    
    @end
    

    In MultiPolygonView.m:

    #import "MultiPolygon.h"  //Add import "MultiPolygon.h"
    
    
    @implementation MultiPolygonView
    
    
    - (CGPathRef)polyPath:(MKPolygon *)polygon
    
    {
     MKMapPoint *points = [polygon points];
     NSUInteger pointCount = [polygon pointCount];
     NSUInteger i;
    
     if (pointCount < 3)
         return NULL;
    
     CGMutablePathRef path = CGPathCreateMutable();
    
     for (MKPolygon *interiorPolygon in polygon.interiorPolygons) {
         CGPathRef interiorPath = [self polyPath:interiorPolygon];
         CGPathAddPath(path, NULL, interiorPath);
         CGPathRelease(interiorPath);
     }
    
     CGPoint relativePoint = [self pointForMapPoint:points[0]];
     CGPathMoveToPoint(path, NULL, relativePoint.x, relativePoint.y);
     for (i = 1; i < pointCount; i++) {
         relativePoint = [self pointForMapPoint:points[i]];
         CGPathAddLineToPoint(path, NULL, relativePoint.x, relativePoint.y);
     }
    
     return path;
    }
    
    - (void)drawMapRect:(MKMapRect)mapRect
          zoomScale:(MKZoomScale)zoomScale
          inContext:(CGContextRef)context
    {
     MultiPolygon *multiPolygon = (MultiPolygon *)self.overlay;
     for (MKPolygon *polygon in multiPolygon.polygons) {
        CGPathRef path = [self polyPath:polygon];
         if (path) {
             [self applyFillPropertiesToContext:context atZoomScale:zoomScale];
             CGContextBeginPath(context);
             CGContextAddPath(context, path);
             CGContextDrawPath(context, kCGPathEOFill);
             [self applyStrokePropertiesToContext:context atZoomScale:zoomScale];
             CGContextBeginPath(context);
             CGContextAddPath(context, path);
             CGContextStrokePath(context);
             CGPathRelease(path);
         }
     }
    }
    
    @end
    

    To us it import MultiPolygon.h and MultiPolygonView.h in your ViewController

    Create one polygon from all: As an example I've got an array with polygons: polygonsInArray.

    MultiPolygon *allPolygonsInOne = [[MultiPolygon alloc] initWithPolygons:polygonsInArray];
    

    Add the allPolygonsInOne to the mapView:

    [mapView addOverlay:allPolygonsInOne];
    

    Also change your viewForOverlay method:

    -(MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id<MKOverlay>)overlay
    {
    
     if ([overlay isKindOfClass:[MultiPolygon class]]) {
         MultiPolygonView *polygonsView = [[MultiPolygonView alloc] initWithOverlay:(MultiPolygon*)overlay];
         polygonsView.fillColor = [[UIColor magentaColor] colorWithAlphaComponent:0.8];
         polygonsView.strokeColor = [[UIColor blueColor] colorWithAlphaComponent:0.8];
         polygonsView.lineWidth = 1;
         return polygonsView;
     }
     else {
       return nil;
     }
    
    }
    

    And this greatly reduced memory usage for multiple overlays on the mapView. You're not reusing now because only one OverlayView is drawn. So no need to reuse.

    0 讨论(0)
  • 2020-12-29 16:18

    A Swift 4 version of the Objective-C code posted by @wkberg:

    MultiPolygon.swift:

    import MapKit
    
    /// A concatenation of multiple polygons to allow a single overlay to be drawn in the map,
    /// which will consume less resources
    class MultiPolygon: NSObject, MKOverlay {
        var polygons: [MKPolygon]?
    
        var boundingMapRect: MKMapRect
    
        init(polygons: [MKPolygon]?) {
            self.polygons = polygons
            self.boundingMapRect = MKMapRect.null
    
            super.init()
    
            guard let pols = polygons else { return }
            for (index, polygon) in pols.enumerated() {
                if index == 0 { self.boundingMapRect = polygon.boundingMapRect; continue }
                boundingMapRect = boundingMapRect.union(polygon.boundingMapRect)
            }
        }
    
        var coordinate: CLLocationCoordinate2D {
            return MKMapPoint(x: boundingMapRect.midX, y: boundingMapRect.maxY).coordinate
        }
    }
    

    MultiPolygonPathRenderer.swift:

    import MapKit
    
    /// A MKOverlayPathRenderer that can draw a concatenation of multiple polygons as a single polygon
    /// This will consume less resources
    class MultiPolygonPathRenderer: MKOverlayPathRenderer {
        /**
         Returns a `CGPath` equivalent to this polygon in given renderer.
    
         - parameter polygon: MKPolygon defining coordinates that will be drawn.
    
         - returns: Path equivalent to this polygon in given renderer.
         */
        func polyPath(for polygon: MKPolygon?) -> CGPath? {
            guard let polygon = polygon else { return nil }
            let points = polygon.points()
    
            if polygon.pointCount < 3 { return nil }
            let pointCount = polygon.pointCount
    
            let path = CGMutablePath()
    
            if let interiorPolygons = polygon.interiorPolygons {
                for interiorPolygon in interiorPolygons {
                    guard let interiorPath = polyPath(for: interiorPolygon) else { continue }
                    path.addPath(interiorPath, transform: .identity)
                }
            }
    
            let startPoint = point(for: points[0])
            path.move(to: CGPoint(x: startPoint.x, y: startPoint.y), transform: .identity)
    
            for i in 1..<pointCount {
                let nextPoint = point(for: points[i])
                path.addLine(to: CGPoint(x: nextPoint.x, y: nextPoint.y), transform: .identity)
            }
    
            return path
        }
    
        /// Draws the overlay’s contents at the specified location on the map.
        override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
            // Taken from: http://stackoverflow.com/a/17673411
    
            guard let multiPolygon = self.overlay as? MultiPolygon else { return }
            guard let polygons = multiPolygon.polygons else { return }
    
            for polygon in polygons {
                guard let path = self.polyPath(for: polygon) else { continue }
                self.applyFillProperties(to: context, atZoomScale: zoomScale)
                context.beginPath()
                context.addPath(path)
                context.drawPath(using: CGPathDrawingMode.eoFill)
                self.applyStrokeProperties(to: context, atZoomScale: zoomScale)
                context.beginPath()
                context.addPath(path)
                context.strokePath()
            }
        }
    }
    

    Usage - Adding the overlay to your MKMapView:

    // Add the overlay to mapView
    let polygonsArray: [MKPolygon] = self.buildMKPolygons()
    let multiPolygons = MultiPolygon.init(polygons: polygonsArray)
    self.mapView.addOverlay(multiPolygons)
    

    Usage - Implementing the viewForOverlay in your MKMapViewDelegate:

    // Method viewForOverlay:
    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        if overlay is MultiPolygon {
            let polygonRenderer = MultiPolygonPathRenderer(overlay: overlay)
            polygonRenderer.lineWidth = 0.5
            polygonRenderer.strokeColor = .mainGreen
            polygonRenderer.miterLimit = 2.0
            polygonRenderer.fillColor = UIColor.mainGreen.withAlphaComponent(0.2)
            return polygonRenderer
        }
    
        return MKOverlayRenderer()
    }
    
    0 讨论(0)
提交回复
热议问题