iOS: Was ist der schnellste und performanteste Weg, um einen Screenshot programmgesteuert zu erstellen?

77

In meiner iPad-App möchte ich einen Screenshot eines UIView machen, der einen großen Teil des Bildschirms einnimmt. Leider sind die Unteransichten ziemlich tief verschachtelt, so dass es zu lange dauert, den Screenshot zu erstellen und anschließend eine Seite zu animieren, die sich kräuselt.

Gibt es einen schnelleren Weg als den "üblichen"?

UIGraphicsBeginImageContext(self.bounds.size);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *resultingImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

Wenn möglich, möchte ich vermeiden, meine Ansicht zwischenzuspeichern oder neu zu strukturieren.

Swalkner
quelle
6
Vergessen Sie nicht, UIGraphicsEndImageContext aufzurufen, wenn Sie fertig sind.
ZunTzu

Antworten:

111

Ich habe eine bessere Methode gefunden, die nach Möglichkeit die Snapshot-API verwendet.

Ich hoffe, es hilft.

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!
}

Möchten Sie mehr über iOS 7-Snapshots erfahren?

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;
}
3lvis
quelle
2
Ist diese Lösung besser als die vom Originalplakat angebotene Lösung? Meine eigenen Tests legen nahe, dass es genau das gleiche ist. Insgesamt würde ich mich für die ursprüngliche Lösung entscheiden, da der Code so viel einfacher ist.
Greg Maletic
@ GregMaletic: Ja, die andere Lösung sieht einfacher aus, funktioniert aber über UIView. Diese funktioniert über UIWindow, sodass sie vollständiger ist.
3lvis
Ich kann immer noch nicht verstehen, warum diese Lösung schneller ist. Die meisten iOS-Apps enthalten nur ein Fenster. Ist nicht nur [self.window.layer renderInContext: context] in Ordnung?
Muhammad Hassan Nasr
1
Ich glaube nicht, dass das funktioniert. Die Leistungsprobleme von renderInContext sind gut dokumentiert, und das Aufrufen auf der Fensterebene wird dies nicht beheben.
Adam
1
In meinen Tests ist renderInContext auch viel besser als drawViewHierarchyInRect, iOS 11.2
Nikolay Dimitrov
18

BEARBEITEN 3. Oktober 2013 Aktualisiert, um die neue superschnelle Methode drawViewHierarchyInRect: afterScreenUpdates: in iOS 7 zu unterstützen.


Nein. CALayers renderInContext: ist meines Wissens der einzige Weg, dies zu tun. Sie können eine UIView-Kategorie wie diese erstellen, um sich die Zukunft zu erleichtern:

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

Auf diese Weise können Sie möglicherweise [self.view.window imageRepresentation]in einem Ansichts-Controller sagen , dass Sie einen vollständigen Screenshot Ihrer App erhalten. Dies könnte jedoch die Statusleiste ausschließen.

BEARBEITEN:

Und darf ich hinzufügen. Wenn Sie eine UIView mit transparentem Inhalt haben und auch eine Bilddarstellung mit dem darunter liegenden Inhalt benötigen, können Sie eine Bilddarstellung der Containeransicht abrufen und dieses Bild zuschneiden, indem Sie einfach das Rechteck der Unteransicht nehmen und es in den Container konvertieren Ansichten Koordinatensystem.

[view convertRect:self.bounds toView:containerView]

Zum Zuschneiden siehe Antwort auf diese Frage: Zuschneiden eines UIImage

Trenskow
quelle
Vielen Dank; Ich verwende gerade eine Kategorie. aber ich suche nach einem performanteren Weg, um den Screenshot zu machen ...: /
swalkner
@EDIT: Das mache ich - ich bekomme die Bilddarstellung des Containers. Aber das hilft mir nicht bei meinem Leistungsproblem ...
Swalkner
Das Gleiche gilt für mich ... gibt es keinen Weg, ohne alles neu zu rendern?
Christian Schnorr
Es ist richtig, dass iOS interne Bilddarstellungen verwendet, um das Rendern zu beschleunigen. Nur Ansichten, die sich ändern, werden erneut gerendert. Wenn Sie jedoch fragen, wie Sie die interne Bilddarstellung erhalten, ohne sie neu zeichnen zu müssen, halte ich dies nicht für möglich. Wie oben erwähnt, befindet sich dieses Image wahrscheinlich in der GPU und ist höchstwahrscheinlich über öffentliche APIs nicht zugänglich.
Trenskow
Ich musste verwenden afterScreenUpdates:YES, aber ansonsten funktioniert es großartig.
EthanB
10

In iOS 7 wurde eine neue Methode eingeführt, mit der Sie eine Ansichtshierarchie in den aktuellen Grafikkontext zeichnen können. Dies kann verwendet werden, um eine UIImagesehr schnelle zu bekommen .

Implementiert als Kategoriemethode für UIView:

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

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

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

Es ist erheblich schneller als die bestehende renderInContext:Methode.

UPDATE FÜR SWIFT : Eine Erweiterung, die dasselbe tut:

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;
    }
}
Klaas
quelle
Haben Sie getestet, dass es tatsächlich schneller ist? Meine Tests führten zu sehr geringen Leistungsverbesserungen, selbst wenn afterScreenUpdates auf NO gesetzt war.
Maxpower
@maxpower Ich habe die Ausführung zeitlich festgelegt und eine Geschwindigkeitssteigerung von mehr als 50% erzielt. Mit dem alten renderInContext: Es dauerte ungefähr 0,18 Sekunden und damit 0,063 Sekunden. Ich glaube, Ihre Ergebnisse variieren je nach CPU in Ihrem Gerät.
Ist es nur ich oder self.drawViewHierarchyInRect(self.bounds, afterScreenUpdates: true)verursacht es für einen Moment einen seltsamen Anzeigefehler, während es ausgeführt wird? Ich habe nicht das gleiche Problem mit self.layer.renderInContext(UIGraphicsGetCurrentContext()).
Captain COOLGUY
3

Ich habe die Antworten auf einzelne Funktionen kombiniert, die für alle iOS-Versionen ausgeführt werden können, auch für Netzhaut- oder Nicht-Retain-Geräte.

- (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;
}
Hemang
quelle
2

Für mich war das Einstellen der InterpolationQuality ein langer Weg.

CGContextSetInterpolationQuality(ctx, kCGInterpolationNone);

Wenn Sie sehr detaillierte Bilder aufnehmen, ist diese Lösung möglicherweise nicht akzeptabel. Wenn Sie Schnappschüsse erstellen, werden Sie den Unterschied kaum bemerken.

Dies verkürzte die Zeit für die Aufnahme erheblich und machte ein Bild, das viel weniger Speicherplatz beanspruchte.

Dies ist bei der Methode drawViewHierarchyInRect: afterScreenUpdates: weiterhin von Vorteil.

maximale Kraft
quelle
Können Sie mir genau sagen, welche Unterschiede Sie sehen? Ich sehe einen leichten Zeitanstieg.
DaveMac
Leider kann ich nicht. Ich habe keinen Zugriff mehr auf das Projekt. Jobs geändert. Aber ich kann sagen, dass die Ansicht, die auf dem Bildschirm aufgenommen wurde, wahrscheinlich 50 + - 10 Ansichten in absteigender Hierarchie hatte. Ich kann auch sagen, dass etwa 1/4 - 1/3 der Ansichten
Bildansichten
1
Wenn ich weiter recherchiere, sehe ich nur dann einen Unterschied beim Festlegen der Interpolation, wenn Sie die Größe der Ansicht ändern, wenn Sie sie rendern oder in einen kleineren Kontext rendern.
DaveMac
Ich gehe davon aus, dass dies hauptsächlich vom jeweiligen Kontext abhängt. Mindestens eine andere Person hat signifikante Ergebnisse daraus gesehen. Siehe den Kommentar zu dieser Antwort. stackoverflow.com/questions/11435210/…
maxpower
1

Als Alternative möchten Sie die GPU lesen (da der Bildschirm aus einer beliebigen Anzahl durchscheinender Ansichten besteht), was ebenfalls von Natur aus langsam ist.

David Dunham
quelle
Es gibt also keine schnellere Lösung?
Swalkner
es ist nicht auf ios, da gpu und cpu den gleichen ram teilen
nevyn