繁体   English   中英

在 iOS 中绘制部分图像的最有效方法

[英]Most efficient way to draw part of an image in iOS

给定UIImageCGRect ,绘制与CGRect对应的图像部分(不缩放)最有效的方法(在内存和时间上)是什么?

作为参考,这是我目前的做法:

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGRect frameRect = CGRectMake(frameOrigin.x + rect.origin.x, frameOrigin.y + rect.origin.y, rect.size.width, rect.size.height);    
    CGImageRef imageRef = CGImageCreateWithImageInRect(image_.CGImage, frameRect);
    CGContextTranslateCTM(context, 0, rect.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    CGContextDrawImage(context, rect, imageRef);
    CGImageRelease(imageRef);
}

不幸的是,对于中等大小的图像和高setNeedsDisplay频率,这似乎非常慢。 使用UIImageView的 frame 和clipToBounds会产生更好的结果(灵活性较低)。

我猜你这样做是为了在屏幕上显示图像的一部分,因为你提到了UIImageView 优化问题总是需要专门定义。


信任Apple的常规UI东西

实际上,如果您的目标只是剪切图像的矩形区域(不是太大),那么带有clipsToBounds UIImageView是存档目标的最快/最简单的方法之一。 此外,您不需要发送setNeedsDisplay消息。

或者您可以尝试将UIImageView放在空的UIView并在容器视图中设置剪辑。 使用此技术,您可以通过在2D(缩放,旋转,平移)中设置transform属性来自由变换图像。

如果您需要3D变换,你仍然可以使用CALayermasksToBounds财产,但使用CALayer会给你非常少的额外性能通常不可观。

无论如何,您需要知道所有低级细节才能正确使用它们进行优化。


为什么这是最快的方法之一?

UIView只是CALayer上面的一个薄层,它是在OpenGL之上实现的,它实际上是GPU的直接接口。 这意味着UIKit正在被GPU加速。

因此,如果您正确使用它们(我的意思是,在设计限制内),它将执行与简单的OpenGL实现一样好。 如果您只使用几个图像来显示,那么UIView实现将获得可接受的性能,因为它可以完全加速底层OpenGL (这意味着GPU加速)。

无论如何,如果你需要使用像游戏应用程序那样精细调整的像素着色器对数百个动画精灵进行极端优化 ,你应该直接使用OpenGL,因为CALayer在较低级别缺少很多优化选项。 无论如何,至少为了优化UI的东西,要比Apple好一点是难以置信的。


为什么你的方法比UIImageView慢?

你应该知道的是GPU加速。 在所有最近的计算机中,只有GPU才能实现快速的图形性能。 然后,重点是您使用的方法是否在GPU之上实现。

IMO, CGImage绘图方法不是用GPU实现的。 我想我在Apple的文档中提到了这一点,但我不记得在哪里。 所以我不确定这一点。 无论如何,我相信CGImage是在CPU中实现的,因为,

  1. 它的API看起来像是为CPU设计的,比如位图编辑界面和文本绘图。 它们不适合GPU接口。
  2. 位图上下文接口允许直接内存访问。 这意味着它的后端存储位于CPU内存中。 在统一内存架构(以及Metal API)上可能有些不同,但无论如何, CGImage初始设计意图应该是CPU。
  3. 许多人最近发布了明确提到GPU加速的其他Apple API。 这意味着他们的旧API不是。 如果没有特别提及,它通常在CPU中默认完成。

所以它似乎是在CPU中完成的。 在CPU中完成的图形操作比在GPU中慢很多。

简单地剪切图像和合成图像层对于GPU来说是非常简单和便宜的操作(与CPU相比),因此您可以期望UIKit库将利用它,因为整个UIKit是在OpenGL之上实现的。


关于限制

因为优化是一种关于微观管理的工作,具体数字和小事实非常重要。 中等大小是多少? iOS上的OpenGL通常将最大纹理大小限制为1024x1024像素(在最近的版本中可能更大)。 如果你的图像比这大,它将无法工作,或性能会大大降低(我认为UIImageView针对极限范围内的图像进行了优化)。

如果你需要使用剪辑显示巨大的图像,你必须使用像CATiledLayer这样的另一个优化,这是一个完全不同的故事。

除非你想知道OpenGL的每个细节,否则不要去OpenGL。 它需要充分了解低级图形,至少需要100倍的代码。


关于一些未来

虽然不太可能发生,但CGImage东西(或其他任何东西)不需要仅仅停留在CPU中。 不要忘记检查您正在使用的API的基本技术。 尽管如此,GPU的东西与CPU有很大不同,然后API人员通常会明确地提及它们。

如果您不仅可以设置UIImageView的图像,而且还可以设置在UIImage中显示的左上角偏移量,那么它最终会更快,从精灵地图集创建的图像少得多。 也许这是可能的。

同时,我在我的应用程序中使用的实用程序类中创建了这些有用的函数。 它从另一个UIImage的一部分创建一个UIImage,其中包含使用标准UIImageOrientation值指定的旋转,缩放和翻转选项。

我的应用程序在初始化期间创建了大量的UIImages,这必然需要时间。 但是在选择某个选项卡之前不需要某些图像。 为了给出更快加载的外观,我可以在启动时生成的单独线程中创建它们,然后等到完成后如果选中该选项卡就完成了。

+ (UIImage*)imageByCropping:(UIImage *)imageToCrop toRect:(CGRect)aperture {
    return [ChordCalcController imageByCropping:imageToCrop toRect:aperture withOrientation:UIImageOrientationUp];
}

// Draw a full image into a crop-sized area and offset to produce a cropped, rotated image
+ (UIImage*)imageByCropping:(UIImage *)imageToCrop toRect:(CGRect)aperture withOrientation:(UIImageOrientation)orientation {

            // convert y coordinate to origin bottom-left
    CGFloat orgY = aperture.origin.y + aperture.size.height - imageToCrop.size.height,
            orgX = -aperture.origin.x,
            scaleX = 1.0,
            scaleY = 1.0,
            rot = 0.0;
    CGSize size;

    switch (orientation) {
        case UIImageOrientationRight:
        case UIImageOrientationRightMirrored:
        case UIImageOrientationLeft:
        case UIImageOrientationLeftMirrored:
            size = CGSizeMake(aperture.size.height, aperture.size.width);
            break;
        case UIImageOrientationDown:
        case UIImageOrientationDownMirrored:
        case UIImageOrientationUp:
        case UIImageOrientationUpMirrored:
            size = aperture.size;
            break;
        default:
            assert(NO);
            return nil;
    }


    switch (orientation) {
        case UIImageOrientationRight:
            rot = 1.0 * M_PI / 2.0;
            orgY -= aperture.size.height;
            break;
        case UIImageOrientationRightMirrored:
            rot = 1.0 * M_PI / 2.0;
            scaleY = -1.0;
            break;
        case UIImageOrientationDown:
            scaleX = scaleY = -1.0;
            orgX -= aperture.size.width;
            orgY -= aperture.size.height;
            break;
        case UIImageOrientationDownMirrored:
            orgY -= aperture.size.height;
            scaleY = -1.0;
            break;
        case UIImageOrientationLeft:
            rot = 3.0 * M_PI / 2.0;
            orgX -= aperture.size.height;
            break;
        case UIImageOrientationLeftMirrored:
            rot = 3.0 * M_PI / 2.0;
            orgY -= aperture.size.height;
            orgX -= aperture.size.width;
            scaleY = -1.0;
            break;
        case UIImageOrientationUp:
            break;
        case UIImageOrientationUpMirrored:
            orgX -= aperture.size.width;
            scaleX = -1.0;
            break;
    }

    // set the draw rect to pan the image to the right spot
    CGRect drawRect = CGRectMake(orgX, orgY, imageToCrop.size.width, imageToCrop.size.height);

    // create a context for the new image
    UIGraphicsBeginImageContextWithOptions(size, NO, imageToCrop.scale);
    CGContextRef gc = UIGraphicsGetCurrentContext();

    // apply rotation and scaling
    CGContextRotateCTM(gc, rot);
    CGContextScaleCTM(gc, scaleX, scaleY);

    // draw the image to our clipped context using the offset rect
    CGContextDrawImage(gc, drawRect, imageToCrop.CGImage);

    // pull the image from our cropped context
    UIImage *cropped = UIGraphicsGetImageFromCurrentImageContext();

    // pop the context to get back to the default
    UIGraphicsEndImageContext();

    // Note: this is autoreleased
    return cropped;
}

UIImageView中移动大图像的非常简单的方法如下。

让我们得到大小(100,400)的图像,代表一个图像的4个状态,一个在另一个之下。 我们想在尺寸为(100,100)的方形UIImageView中显示偏移Y = 100的第二张图片。 解决方案是:

UIImageView *iView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
CGRect contentFrame = CGRectMake(0, 0.25, 1, 0.25);
iView.layer.contentsRect = contentFrame;
iView.image = [UIImage imageNamed:@"NAME"];

这里contentFrame是相对于真实UIImage大小的规范化帧。 因此,“0”表示我们从左边界开始可见的图像部分,“0.25”表示我们有垂直偏移100,“1”表示我们想要显示图像的全宽,最后,“0.25”表示我们想要只显示高度的1/4部分图像。

因此,在局部图像坐标中,我们显示以下帧

CGRect visibleAbsoluteFrame = CGRectMake(0*100, 0.25*400, 1*100, 0.25*400)
or CGRectMake(0, 100, 100, 100);

如何使用CGContextClipToRect ,而不是创建一个新图像(因为它分配内存而成本高昂)?

最快的方法是使用图像蒙版:与要遮罩的图像大小相同的图像,但具有某个像素图案,指示渲染时要遮盖的图像部分 - >

// maskImage is used to block off the portion that you do not want rendered
// note that rect is not actually used because the image mask defines the rect that is rendered
-(void) drawRect:(CGRect)rect maskImage:(UIImage*)maskImage {

    UIGraphicsBeginImageContext(image_.size);
    [maskImage drawInRect:image_.bounds];
    maskImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    CGImageRef maskRef = maskImage.CGImage;
    CGImageRef mask = CGImageMaskCreate(CGImageGetWidth(maskRef),
                                    CGImageGetHeight(maskRef),
                                    CGImageGetBitsPerComponent(maskRef),
                                    CGImageGetBitsPerPixel(maskRef),
                                    CGImageGetBytesPerRow(maskRef),
                                    CGImageGetDataProvider(maskRef), NULL, false);

    CGImageRef maskedImageRef = CGImageCreateWithMask([image_ CGImage], mask);
    image_ = [UIImage imageWithCGImage:maskedImageRef scale:1.0f orientation:image_.imageOrientation];

    CGImageRelease(mask);
    CGImageRelease(maskedImageRef); 
}

新框架更好,更易于使用:

      func crop(img: UIImage, with rect: CGRect) -> UIImage? {
            guard let cgImg = img.cgImage else { return nil }
            // Create bitmap image from context using the rect
            if let imageRef: CGImage = cgImg.cropping(to: rect){
                // Create a new image based on the imageRef and rotate back to the original orientation
                let image: UIImage = UIImage(cgImage: imageRef, scale: img.scale, orientation: img.imageOrientation)
                return image
            }
            else{
                return nil
            }
        }

ps:我把@Scott Lahteine 的答案翻译成了Swift,结果真的很奇怪。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM