Proper use of MKOverlayView

2019-02-26 18:54发布

问题:

I am writing an iPhone app in which I place a large PNG image (1936 × 2967) on an MKMapView using MKOverlayView. I am a little confused about how to appropriately implement the drawMapRect: function in MKOverlayView - should I manually segment my image before drawing it? Or should I let the mechanisms of MKOverlayView handle all that?

My impression from other posts is that before MKOverlayView was available, you were expected to segment images yourself for this kind of task, and use a CATiledLayer. I thought maybe MKOverlayView took care of all the dirty work.

The real reason I ask though is because when I run my app through Instruments using the allocations tool, I find that the number of live bytes my app is using steadily increases with the introduction of the custom image on the map. Right now I am NOT segmenting my image, but I also am seeing no record of memory leaks in the leaks tool in Instruments. Here is my drawMapRect: function:

- (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context{
    // Load image from applicaiton bundle
    NSString* imageFileName = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"map.png"];
    CGDataProviderRef provider = CGDataProviderCreateWithFilename([imageFileName UTF8String]);
    CGImageRef image = CGImageCreateWithPNGDataProvider(provider, NULL, true, kCGRenderingIntentDefault);
    CGDataProviderRelease(provider);

    MKMapRect overlayMapRect = [self.overlay boundingMapRect];
    CGRect overlayRect = [self rectForMapRect:overlayMapRect];

    // draw image
    CGContextSaveGState(context);
    CGContextDrawImage(context, overlayRect, image);
    CGContextRestoreGState(context);

    CGImageRelease(image);

}

If my drawMapRect: function is not the cause of these memory issues, does anybody know what it might be? I know through debugging that my viewForOverlay: function for the mapView only gets called once for each overlay, so it's not that memory is leaking there or something.

Any advice is welcome!

Thanks, -Matt

EDIT: so it turns out that the memory issue is actually being caused by MKMapView - every time I move the map at all the memory usage goes up very steadily and never comes down - this doesn't seem good :(

回答1:

A bit of a late answer, leaving it here if somebody else hits the same problem in the future. The flaw here is trying to render a whole image while documentation clearly says -

In addition, you should avoid drawing the entire contents of the overlay each time this method is called. Instead, always take the mapRect parameter into consideration and avoid drawing content outside that rectangle.

so, you have to only draw the part of the image in the area defined by mapRect

updated: keep in mind that drawRect here can be larger than mapRect, need to adjust the paint and cut regions accordingly

let overlayMapRect = overlay.boundingMapRect
let overlayDrawRect = self.rect(for: overlayMapRect)

// watch out for draw rect adjustment here --
let drawRect = self.rect(for: mapRect).intersection(overlayDrawRect)
let scaleX = CGFloat(image.width) / overlayRect.width
let scaleY = CGFloat(image.height) / overlayRect.height
let transform = CGAffineTransform.init(scaleX: scaleX, y: scaleY)
let imageCut = drawRect.applying(transform)

// omitting optionals checks, you should not
let cutImage = image.cropping(to: imageCut)

// the usual vertical flip issue with image.draw
context.translateBy(x: 0, y: drawRect.maxY + drawRect.origin.y)
context.scaleBy(x: 1, y: -1)
context.draw(image, in: drawRect, byTiling: false)


回答2:

Here is the objc version based on epolyakov's answer. It works great, but only without any rotation.

- (void) drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context
{
    CGImageRef overlayImage = <your_uiimage>.CGImage;

    CGRect overlayRect = [self rectForMapRect:[self.overlay boundingMapRect]];
    CGRect drawRect = [self rectForMapRect:mapRect];

    CGRect rectPortion = CGRectIntersection(overlayRect, drawRect);
    CGFloat scaleX = rotatedImage.size.width / overlayRect.size.width;
    CGFloat scaleY = rotatedImage.size.height / overlayRect.size.height;
    CGAffineTransform transform = CGAffineTransformMakeScale(scaleX, scaleY);
    CGRect imagePortion = CGRectApplyAffineTransform(rectPortion, transform);

    CGImageRef cutImage = CGImageCreateWithImageInRect(overlayImage, imagePortion);

    CGRect finalRect = rectPortion;

    CGContextTranslateCTM(context, 0, finalRect.origin.y + CGRectGetMaxY(finalRect));
    CGContextScaleCTM(context, 1.0, -1.0);

    CGContextSetAlpha(context, self.alpha);
    CGContextDrawImage(context, finalRect, cutImage);
}

If you need to manage also the rotation of your image, I found a trick using a rotated version of the original image (this because the map rendering always draw vertical rects and rotating the image in this method will cut it). So using a rotated version of the original image allows to render with vertical rects as the map expects

    UIImage* rotatedImage = [self rotatedImage:<your_uiimage> withAngle:<angle_of_image>];

    CGImageRef overlayImage = rotatedImage.CGImage;

And this is the method that produce a rotated image in a bounding rect

- (UIImage*) rotatedImage:(UIImage*)image withAngle:(CGFloat)angle
{
    float radians = degreesToRadians(angle);

    CGAffineTransform xfrm = CGAffineTransformMakeRotation(radians);
    CGRect imageRect = CGRectMake(0, 0, image.size.width, image.size.height);
    CGRect rotatedImageBoundingRect = CGRectApplyAffineTransform (imageRect, xfrm);


    UIGraphicsBeginImageContext(rotatedImageBoundingRect.size);
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    CGContextTranslateCTM (ctx, rotatedImageBoundingRect.size.width/2., rotatedImageBoundingRect.size.height/2.);
    CGContextScaleCTM(ctx, 1.0, -1.0);
    CGContextRotateCTM (ctx, radians);
    CGContextDrawImage (ctx, CGRectMake(-image.size.width / 2, -image.size.height / 2, image.size.width, image.size.height), image.CGImage);

    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return newImage;
}