iOS: what's the fastest, most performant way to make a screenshot programmatically?

自古美人都是妖i 提交于 2019-11-26 16:55:37

I've found a better method that uses the snapshot API whenever possible.

I hope it helps.

class func screenshot() -> UIImage {
    var imageSize = CGSize.zero

    let orientation = UIApplication.shared.statusBarOrientation
    if UIInterfaceOrientationIsPortrait(orientation) {
        imageSize = UIScreen.main.bounds.size
    } else {
        imageSize = CGSize(width: UIScreen.main.bounds.size.height, height: UIScreen.main.bounds.size.width)
    }

    UIGraphicsBeginImageContextWithOptions(imageSize, false, 0)
    for window in UIApplication.shared.windows {
        window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
    }

    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return image!
}

Wanna know more about iOS 7 Snapshots?

Objective-C version:

+ (UIImage *)screenshot
{
    CGSize imageSize = CGSizeZero;

    UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
    if (UIInterfaceOrientationIsPortrait(orientation)) {
        imageSize = [UIScreen mainScreen].bounds.size;
    } else {
        imageSize = CGSizeMake([UIScreen mainScreen].bounds.size.height, [UIScreen mainScreen].bounds.size.width);
    }

    UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0);
    CGContextRef context = UIGraphicsGetCurrentContext();
    for (UIWindow *window in [[UIApplication sharedApplication] windows]) {
        CGContextSaveGState(context);
        CGContextTranslateCTM(context, window.center.x, window.center.y);
        CGContextConcatCTM(context, window.transform);
        CGContextTranslateCTM(context, -window.bounds.size.width * window.layer.anchorPoint.x, -window.bounds.size.height * window.layer.anchorPoint.y);
        if (orientation == UIInterfaceOrientationLandscapeLeft) {
            CGContextRotateCTM(context, M_PI_2);
            CGContextTranslateCTM(context, 0, -imageSize.width);
        } else if (orientation == UIInterfaceOrientationLandscapeRight) {
            CGContextRotateCTM(context, -M_PI_2);
            CGContextTranslateCTM(context, -imageSize.height, 0);
        } else if (orientation == UIInterfaceOrientationPortraitUpsideDown) {
            CGContextRotateCTM(context, M_PI);
            CGContextTranslateCTM(context, -imageSize.width, -imageSize.height);
        }
        if ([window respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) {
            [window drawViewHierarchyInRect:window.bounds afterScreenUpdates:YES];
        } else {
            [window.layer renderInContext:context];
        }
        CGContextRestoreGState(context);
    }

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}
Trenskow

EDIT October 3. 2013 Updated to support the new super fast drawViewHierarchyInRect:afterScreenUpdates: method in iOS 7.


No. CALayer's renderInContext: is as far as I know the only way to do this. You could create a UIView category like this, to make it easier for yourself going forward:

UIView+Screenshot.h

#import <UIKit/UIKit.h>

@interface UIView (Screenshot)

- (UIImage*)imageRepresentation;

@end

UIView+Screenshot.m

#import <QuartzCore/QuartzCore.h>
#import "UIView+Screenshot.h"

@implementation UIView (Screenshot)

- (UIImage*)imageRepresentation {

    UIGraphicsBeginImageContextWithOptions(self.bounds.size, YES, self.window.screen.scale);

    /* iOS 7 */
    if ([self respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)])            
        [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:NO];
    else /* iOS 6 */
        [self.layer renderInContext:UIGraphicsGetCurrentContext()];

    UIImage* ret = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return ret;

}

@end

By this you might be able to say [self.view.window imageRepresentation] in a view controller, and get a full screenshot of your app. This might exclude the statusbar though.

EDIT:

And may I add. If you have an UIView with transparent content, and needs an image representation WITH the underlaying content as well, you can grab an image representation of the container view and crop that image, simply by taking the rect of the subview and converting it to the container views coordinate system.

[view convertRect:self.bounds toView:containerView]

To crop see answer to this question: Cropping an UIImage

iOS 7 introduced a new method that allows you to draw a view hierarchy into the current graphics context. This can be used to get an UIImage very fast.

Implemented as category method on UIView:

- (UIImage *)pb_takeSnapshot {
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, [UIScreen mainScreen].scale);

    [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

It is considerably faster then the existing renderInContext: method.

UPDATE FOR SWIFT: An extension that does the same:

extension UIView {

    func pb_takeSnapshot() -> UIImage {
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, UIScreen.mainScreen().scale);

        self.drawViewHierarchyInRect(self.bounds, afterScreenUpdates: true)

        // old style: self.layer.renderInContext(UIGraphicsGetCurrentContext())

        let image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        return image;
    }
}

I combined the answers to single function which will be running for any iOS versions, even for retina or non-retains devices.

- (UIImage *)screenShot {
    if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)])
        UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, [UIScreen mainScreen].scale);
    else
        UIGraphicsBeginImageContext(self.view.bounds.size);

    #ifdef __IPHONE_7_0
        #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
            [self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:YES];
        #endif
    #else
            [self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
    #endif

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

For me setting the InterpolationQuality went a long way.

CGContextSetInterpolationQuality(ctx, kCGInterpolationNone);

If you are snapshotting very detailed images this solution may not be acceptable. If you are snapshotting text you will hardly notice the difference.

This cut down the time to take the snap shot significantly as well as making an image that consumed far less memory.

This is still beneficial with the drawViewHierarchyInRect:afterScreenUpdates: method.

What you’re asking for as an alternative is to read the GPU (since the screen is composited from any number of translucent views), which is an inherently slow operation too.

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