MKMapView with multiple overlays memory-issue

元气小坏坏 提交于 2019-11-30 05:46:29

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.

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()
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!