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

时间:2011-11-07 11:14:47

标签: iphone ios uiimage quartz-2d drawrect

给定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的框架和clipToBounds可以产生更好的结果(灵活性较低)。

5 个答案:

答案 0 :(得分:78)

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


信任Apple以获取常规UI内容

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

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

如果您需要进行3D转换,您仍然可以将masksToBoundsCALayer属性一起使用,但使用UIView会给您带来的额外性能通常不会太大。

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


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

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

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

无论如何,如果你需要极端优化数百个带有微调像素着色器的动画精灵,你应该直接使用OpenGL,因为UIImageView缺乏很多优化选项较低的水平。无论如何,至少为了优化UI的东西,要比Apple更好,这是非常难的。


为什么你的方法比CGImage慢?

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

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

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

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


    关于限制

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

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

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


    关于未来

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

答案 1 :(得分:5)

如果您不仅可以设置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;
}

答案 2 :(得分:3)

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);

答案 3 :(得分:1)

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

答案 4 :(得分:0)

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

// 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); 
}