图片渲染性能优化

  • 谁吃掉我们的CPU: 方法CA::Render::create_image_from_provider
  • 图片预解码可以大幅提高ScrollView流畅度
  • 解码方式:UIGraphic,CGContextWithAlpha,CGContextWithoutAlpha,ImageIO
  • 单线程ImageIO性能最优,多线程中ImageIO并不比其他方式占优
  • 图片为什么要解码:一般下载或者从磁盘获取的图片是PNG或JPG,这是经过编码压缩后的图片数据,不是位图,要把它们渲染到屏幕前就需要进行解码转成位图数据,而这个解码操作比较耗时。iOS默认是在主线程解码,所以SDWebImage将这个过程放到子线程了。同时因为位图体积很大,所以磁盘缓存不会直接缓存位图数据,而是编码压缩后的PNG或JPG数据。

图片加载的流程

  • 使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
  • 将生成的 UIImage 赋值给 UIImageView ;
  • 一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
  • 在主线程的下一个 run loop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
    1. 分配内存缓冲区用于管理文件 IO 和解压缩操作;
    2. 将文件数据从磁盘读到内存中;
    3. 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
    4. 最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。

常用的解码就是对图片进行重新绘制,得到一张新的解压缩后的位图。其中,用到的最核心的函数是 CGBitmapContextCreate :

/* Create a bitmap context. The context draws into a bitmap which is `width'
   pixels wide and `height' pixels high. The number of components for each
   pixel is specified by `space', which may also specify a destination color
   profile. The number of bits for each component of a pixel is specified by
   `bitsPerComponent'. The number of bytes per pixel is equal to
   `(bitsPerComponent * number of components + 7)/8'. Each row of the bitmap
   consists of `bytesPerRow' bytes, which must be at least `width * bytes
   per pixel' bytes; in addition, `bytesPerRow' must be an integer multiple
   of the number of bytes per pixel. `data', if non-NULL, points to a block
   of memory at least `bytesPerRow * height' bytes. If `data' is NULL, the
   data for context is allocated automatically and freed when the context is
   deallocated. `bitmapInfo' specifies whether the bitmap should contain an
   alpha channel and how it's to be generated, along with whether the
   components are floating-point or integer. */

CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
    size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
    CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
    CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
  • 位图是一个像素数组,而像素格式则是用来描述每个像素的组成格式,它包括以下信息:
  1. Bits per component :一个像素中每个独立的颜色分量使用的 bit 数。
  2. Bits per pixel :一个像素使用的总 bit 数。
  3. Bytes per row :位图中的每一行使用的字节数。
  4. Color and Color Spaces:色彩空间。如0,0,1,颜色空间则是用来说明如何解析这些值的,在RGB中代表蓝色。
  5. Color Spaces and Bitmap Layout:像素格式是用来描述每个像素的组成格式的,比如每个像素使用的总 bit 数。而要想确保 Quartz 能够正确地解析这些 bit 所代表的含义,我们还需要提供位图的布局信息 CGBitmapInfo :。包括3方面布局信息:alpha 的信息、颜色分量是否为浮点数、像素格式的字节顺序。当图片不包含 alpha 的时候使用 kCGImageAlphaNoneSkipFirst ,否则使用 kCGImageAlphaPremultipliedFirst 。字节顺序应该使用 32 位的主机字节顺序 kCGBitmapByteOrder32Host (不管当前设备采用的是小端模式还是大端模式,字节顺序始终与其保持一致)。
  6. CGImageByteOrderInfo:像素字节顺序。提供2方面信息:小端模式还是大端模式;数据以 16 位还是 32 位为单位。
  • 函数中参数的含义

  1. data :如果不为 NULL ,那么它应该指向一块大小至少为 bytesPerRow * height 字节的内存;如果 为 NULL ,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL 即可;
  2. width 和 height :位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可;
  3. bitsPerComponent :像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可;
  4. bytesPerRow :位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节。有意思的是,当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化,更多信息可以查看 what is byte alignment (cache line alignment) for Core Animation? Why it matters? 和 Why is my image’s Bytes per Row more than its Bytes per Pixel times its Width? 。
  5. space :就是我们前面提到的颜色空间,一般使用 RGB 即可。
  6. bitmapInfo :就是我们前面提到的位图的布局信息。
  • SDWebImage中图片解码实现如下

+ (UIImage *)decodedImageWithImage:(UIImage *)image {
    // while downloading huge amount of images
    // autorelease the bitmap context
    // and all vars to help system to free memory
    // when there are memory warning.
    // on iOS7, do not forget to call
    // [[SDImageCache sharedImageCache] clearMemory];
    
    if (image == nil) { // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
        return nil;
    }
    
    @autoreleasepool{
        // do not decode animated images
        if (image.images != nil) {
            return image;
        }
        
        CGImageRef imageRef = image.CGImage;
        
        CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
        BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
                         alpha == kCGImageAlphaLast ||
                         alpha == kCGImageAlphaPremultipliedFirst ||
                         alpha == kCGImageAlphaPremultipliedLast);
        if (anyAlpha) {
            return image;
        }
        
        // current
        CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
        CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);
        
        BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||
                                      imageColorSpaceModel == kCGColorSpaceModelMonochrome ||
                                      imageColorSpaceModel == kCGColorSpaceModelCMYK ||
                                      imageColorSpaceModel == kCGColorSpaceModelIndexed);
        if (unsupportedColorSpace) {
            colorspaceRef = CGColorSpaceCreateDeviceRGB();
        }
        
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        NSUInteger bytesPerPixel = 4;
        NSUInteger bytesPerRow = bytesPerPixel * width;
        NSUInteger bitsPerComponent = 8;


        // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
        // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
        // to create bitmap graphics contexts without alpha info.
        // 创建一个位图上下文
        CGContextRef context = CGBitmapContextCreate(NULL,
                                                     width,
                                                     height,
                                                     bitsPerComponent,
                                                     bytesPerRow,
                                                     colorspaceRef,
                                                     kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
        
        // Draw the image into the context and retrieve the new bitmap image without alpha
        // 将原始位图绘制到上下文中
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
        // 创建一张新的解压缩后的位图
        CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
        UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
                                                         scale:image.scale
                                                   orientation:image.imageOrientation];
        
        if (unsupportedColorSpace) {
            CGColorSpaceRelease(colorspaceRef);
        }
        
        CGContextRelease(context);
        CGImageRelease(imageRefWithoutAlpha);
        
        return imageWithoutAlpha;
    }
}
  • YYKit、SDWebImage、FLAnimatedImage性能对比

  1. 参数设置


    图片渲染性能优化_第1张图片
    image
  2. Github Demo

参考:iOS中的imageIO与image解码

ImageIO

  • ImageIO对外开放的对象有CGImageSourceRef、CGImageDestinationRef,不对外开放的对象有CGImageMetadataRef。CoreGraphics中经常与ImageIO打交道的对象有CGImageRef和CGDataProvider。
  • 从CFDataRef到UIImage代码如下:
NSString *resource = [[NSBundle mainBundle] pathForResource:@"xxx" ofType:@"png"];
NSData *data = [NSData dataWithContentsOfFile:resource options:0 error:nil];
   
CFDataRef dataRef = (__bridge CFDataRef)data;
// CGImageSourceRef跟读取图像数据有关
CGImageSourceRef source = CGImageSourceCreateWithData(dataRef, nil);
// CGImageSourceCreateImageAtIndex:调用了_cg_png_read_info和CGImageMetadataCreateMutable,在构建CGImageRef时,读取了图片的基础数据和元数据,基础数据中包括Image的header chunk,比如png的IHDR(即文件头数据块)。元数据是由CGImageMetadataRef来抽象的。并且没有读取图片的其他数据,更没有做解码的动作。
CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil);
UIImage *image = [UIImage imageWithCGImage:cgImage];

如果调用CGImageSourceCopyPropertiesAtIndex,CGImageSourceCopyPropertiesAtIndex的内部函数调用了CGImageMetadataRef。说明:CGImageMetadataRef抽象出图片中EXIF、IPTC、XMP格式的元数据插入字段,而若想获得CGImageMetadataRef必须要通过CGImageSourceRef。

  • 将CGImageSourceRef改由CGDataProviderRef创建
CFDataRef dataRef = (__bridge CFDataRef)data;
CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData(dataRef);
CGImageSourceRef source = CGImageSourceCreateWithDataProvider(dataProvider, nil);
// 测试:由CGImageRef获取CGDataProviderRef和由CGDataProviderRef创建CGImageRef
CGDataProviderRef newDataProvider = CGImageGetDataProvider(cgImage);

size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
CGColorSpaceRef space = CGImageGetColorSpace(cgImage);
size_t bitsPerComponent = CGImageGetBitsPerComponent(cgImage);
size_t bitsPerPixel = CGImageGetBitsPerPixel(cgImage);
size_t bytesPerRow = CGImageGetBytesPerRow(cgImage);
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(cgImage);
    
CGDataProviderRef newDataProvider = CGImageGetDataProvider(cgImage);
CGImageRef newImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, space, bitmapInfo, newDataProvider, NULL, false, kCGRenderingIntentDefault);
  • CGImageDestinationRef将图片数据写入目的地,并且负责做图片编码或者说图片压缩。
CFMutableDataRef buffer = CFDataCreateMutable(kCFAllocatorDefault, 0);
CGImageDestinationRef destination = CGImageDestinationCreateWithData(buffer, kUTTypePNG, 1, NULL);
CGImageDestinationAddImage(destination, cgImage, nil);
CGImageDestinationFinalize(destination);
  • ==结论==
  1. CGImageSourceRef抽象了对读图像数据的通道,读取图像要通过它,它自己本身不读取图像的任何数据,在你调用CGImageSourceCopyPropertiesAtIndex的时候会才去读取图像元数据。
  2. CGImageMetadataRef抽象出图片中EXIF、IPTC、XMP格式的元数据,通过CGImageSourceRef获取。
  3. CGImageRef抽象了图像的基本数据和元数据,创建的时候会通过CGImageSourceRef去读取图像的基础数据和元数据,但没有读取图像的其他数据,没有做图片解码的动作。
  4. CGDataProviderRef没有得出有用信息。
  5. CGImageDestinationRef抽象写入图像数据的通道,写入图像要通过它,在写入图片的时候还负责图片的编码。

Image解码

由上可知,从CFDataRef直到创建出UIImage,没有调用过对图像解码的函数,只读取了一些图像基础数据和元数据。如果不手动设置Image只会等到在屏幕上渲染时再解码。

  • 如果在画布上渲染图片,图片一定是会被解码的
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
    
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
    alphaInfo == kCGImageAlphaPremultipliedFirst ||
    alphaInfo == kCGImageAlphaLast ||
    alphaInfo == kCGImageAlphaFirst) {
    hasAlpha = YES;
}
    
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); 
CGImageRef newImage = CGBitmapContextCreateImage(context);
  • Image解码发生在CGDataProviderCopyData函数内部调用ImageProviderCopyImageBlockSetCallback设置的callback或者copyImageBlock函数,根据不同的图片格式调用的不同的方法中。

Image的初始化

  • imageWithData:通过CGImageSourceRef访问图像数据,创建CGImageRef。
  • imageWithContentsOfFile:文件通过mmap到内存然后通过CGImageSourceRef访问图像数据,创建CGImageRef。
  • imageNamed:先从Bundle里找到资源路径,然后同样也是将文件mmap到内存,再通过CGImageSourceRef访问图像数据,创建CGImageRef。
    图片渲染性能优化_第2张图片
    image

优化

图像

  • 使用网络图片时候并不能直接使用,需先从各种格式解码到内存后才能绘制,且解码是一个相当负责的过程,相当耗时。iOS推荐使用PNG图片(一些很大的背景图片可以考虑JPG或者JPEG),是因为通常PNG图片虽然加载到内存会更慢(图片通常会更大),但是PNG的图片的解码算法更为简单,耗时更少。耗时操作可以考虑异步线程来处理。
  • 如果想显示图片到比原始尺寸小的容器中,那么一次性在后台线程重新绘制到正确的尺寸会比每次显示的时候都做缩放会更有效。
  • 缓存本质上就是用空间(内存)来换性能:imageNamed会将所有的图片缓存到内存,自带缓存不会在对象销毁直接清除,但是占用内存较大;imageWithContentOfFile方法占用内存较少,但没有用自带缓存,每次使用同一个图片都需要重新加载解码一遍。所以如果图片较小,并且频繁使用的图片,使用imageNamed来加载图片(按钮图片/主页图片/占位图);如果图片较大,并且使用次数较少,使用 imageWithContentOfFile:来加载(相册/新特性页面)。
  • imageNamed加载图片之后会立刻进行解码,并由系统缓存图片解码后的数据(即刻占用内存);imageWithContentsOfFile 加载图片后,不会进行解码(直至渲染时才会占用内存)。
  • 分辨率超大的图片处理:《iOS 核心动画高级技巧》中有到 CATiledLayer 将大图分解成小片然后将它们单独按需载入。

图层

  • 避免视图中出现混合层,实验中opaque似乎不影响,alpha会影响。

  • UILabel如果显示中文,就算是设置了backgroundColor仍然在查看混合图层的时候,还是标红的。设置masksToBounds = YES即可解决(UILabel内容是中文时, label的实际渲染区域要大于label的size, 就是因为外围有一圈透明, 才会有图层混合),Demo式例。

  • 设置圆角masksToBounds就会导致离屏渲染,所以不要设置圆角,如果一定要使用圆角,可以使用UIGraphic去切圆角。

  • 图片的使用尽量避免缩放,一定要缩放同样考虑使用UIGraphic去画。

  • 在表格视图中为了减少图层数量可以直接,启用栅格化和离屏渲染。但是一定要使用instruments工具分析一下,是否有必要,但是如果开启栅格化就必须设置分辨率,否则边缘会有毛刺。

    //手动启用离屏渲染
    self.layer.drawsAsynchronously = true // 该属性对传入-drawLayer:inContext:的CGContext进行改动,允许CGContext延缓绘制命令的执行以至于不阻塞用户交互。适用于图层内容需要反复重绘的情况。
    //手动启用栅格化
    self.layer.shouldRasterize = true
    //启用栅格化必须设置设备的分辨率,否则可能会出现毛刺
    self.layer.rasterizationScale = UIScreen.main.scale
    
  • 包含文本的视图UILabel使用的时候,尽量避免修改修改frame,修改frame会导致文本重绘。

  • 如果图层不会被频繁重绘,可以对离屏渲染的图层使用栅格化,作为一种优化方式。

  • 表格控件不要动态创建控件,创建丰富的控件,在显示的时候根据数据隐藏或者显示。

补充:

光栅化:双刃剑
  1. TableViewCell的重绘是很频繁的(因为Cell的复用),如果Cell的内容不断变化,则Cell需要不断重绘,如果此时设置了cell.layer可光栅化。则会造成大量的offscreen渲染,降低图形性能。
  2. 当然,合理利用的话,是能够得到不少性能的提高的,因为使用shouldRasterize后layer会缓存为Bitmap位图,对一些添加了shawdow等效果的耗费资源较多的静态内容进行缓存,能够得到性能的提升。
离屏渲染
  1. 指的是在图像在绘制到当前屏幕前,需要先进行一次渲染,之后才绘制到当前屏幕。
  2. 离屏渲染会消耗大量资源:GPU需要另外alloc一块内存(CPU)来进行渲染(就是使用CPU进行渲染),渲染完毕后在绘制到当前屏幕,而且对于显卡来说,onscreen到offscreen的上下文环境切换是非常昂贵的(这个过程的消耗会比较昂贵,涉及到OpenGL的pipeline跟barrier)。
  3. 使用CPU造成离屏渲染操作包括:drawRect方法(如没有自定义绘制的任务就不要在子类中写一个空的drawRect方法,因为只要实现了该方法,就会为视图分配一个寄宿图,这个寄宿图的像素尺寸等于视图大小乘以 contentsScale的值,造成资源浪费);使用Core Graphics。

此过程如下:首先分配一块内存,然后进行渲染操作生成一份bitmap位图,整个渲染过程会在你的应用中同步的进行,接着再将位图打包发送到iOS里一个单独的进程--render server,理想情况下,render server将内容交给GPU直接显示到屏幕上。

  1. 使用GPU造成离屏渲染操作包括:设置cornerRadius, masks, shadows,edge antialiasing(view的缩放的时候,layer.border.width随着view的放大,会出现锯齿化的问题),layer.edgeAntialiasingMask等;开启光栅化:设置layer.shouldRasterize = YES;
  2. 解决办法:
使用Instruments
  1. 混合层检查:Color Blended layers。标示混合的图层会为红色,不透明的图层为绿色,通常我们希望绿色的区域越多越好。
  2. 开启光栅化是否命中缓存:Color Hits Green and Misses Red。设置viewlayer的shouldRasterize为YES,那些成功被缓存的layer会标注为绿色,反之为红色。
  3. Color copied images:表示那些被Core Animation拷贝的图片。这主要是因为该图片的色彩格式不能被GPU直接处理,需要在CPU这边做转换,假如在主线层做这个操作对性能会有一定的影响。
  4. Color misaligned images:被缩放的图片会被标记为黄色,像素不对齐则会标注为紫色。
  5. 离屏渲染:Color offscreen-rendered yellow。黄色越多则离屏渲染的越多。

UIView的alpha、hidden和opaque属性之间的关系和区别
注意几点:

  • 设置backgroundColor的alpha值影响当前UIView的背景,不会影响subview。
  • opaque默认为YES,如果alpha < 1,那么应该设置opaque设置为NO;但是如果view对应的alpha < 1,opaque设置为YES,产生的后果是不可预料的。

绘图

图片渲染性能优化_第3张图片
image
  • Core Graphics Framework是一套基于C的API框架,使用了Quartz作为绘图引擎。它提供了低级别、轻量级、高保真度的2D渲染。该框架可以用于基于路径的 绘图、变换、颜色管理、脱屏渲染,模板、渐变、遮蔽、图像数据管理、图像的创建、遮罩以及PDF文档的创建、显示和分析。

  • iOS支持两套图形API族:Core Graphics/QuartZ 2D 和OpenGL ES/OpenGL。

    1. Core Graphics是一个绘图专用的API族,它经常被称为QuartZ或QuartZ 2D。Core Graphics是iOS上所有绘图功能的基石,包括UIKit。QuartZ 2D是苹果公司开发的一套API,它是Core Graphics Framework的一部分。
    2. OpenGL ES是跨平台的图形API,属于OpenGL的一个简化版本
  • 注意:OpenGL ES是应用程序编程接口,该接口描述了方法、结构、函数应具有的行为以及应该如何被使用的语义。也就是说它只定义了一套规范,具体的实现由设备制造商根据 规范去做。因为制造 商可以自由的实现Open
    GL ES,所以不同系统实现的OpenGL ES也存在着巨大的性能差异。

  • Core Graphics API所有的操作都在一个上下文中进行。所以在绘图之前需要获取该上下文并传入执行渲染的函数中。如果你正在渲染一副在内存中的图片,此时就需要传入图片 所属的上下文。获得一个图形上下文是我们完成绘图任务的第一步,你可以将图形上下文理解为一块画布。如果你没有得到这块画布,那么你就无法完成任何绘图操 作。

  • 两种最为常用的获取上下文方法:

    1. 调用UIGraphicsBeginImageContextWithOptions函数;
    2. 利用cocoa为你生成的图形上下文。当你子类化了一个UIView并实现了自己的drawRect:方法后,一旦drawRect:方法被调用,Cocoa就会为你创建一个图形上下文,此时你对图形上下文的所有绘图操作都会显示在UIView上。
    3. drawRect: inContext:。
  • 使用UIKit,你只能在当前上下文中绘图,所以如果你当前处于 UIGraphicsBeginImageContextWithOptions函数或drawRect:方法中,你就可以直接使用UIKit提供的方法 进行绘图。如果你持有一个context:参数,那么使用UIKit提供的方法之前,必须将该上下文参数转化为当前上下文。幸运的是,调用 UIGraphicsPushContext 函数可以方便的将context:参数转化为当前上下文,记住最后别忘了调用UIGraphicsPopContext函数恢复上下文环境。

  • 如果你当前处于 UIGraphicsBeginImageContextWithOptions函数或drawRect:方法中,并没有引用一个上下文。为了使用 Core Graphics,你可以调用UIGraphicsGetCurrentContext函数获得当前的图形上下文。

  • 两大绘图框架的支持以及三种获得图形上下文的方法(drawRect:、drawRect: inContext:、UIGraphicsBeginImageContextWithOptions),那么就有6种绘图的形式。
    1、 在UIView的子类方法drawRect:中绘制一个蓝色圆,使用UIKit在Cocoa为我们提供的当前上下文中完成绘图任务。

- (void)drawRect:(CGRect)rect {  
    UIBezierPath *p = [UIBezierPathbezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
    [[UIColor blueColor] setFill];  
    [p fill];  
}

2、 使用Core Graphics实现绘制蓝色圆。

- (void)drawRect:(CGRect)rect {  
    CGContextRef con = UIGraphicsGetCurrentContext();
    CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
    CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
    CGContextFillPath(con);  
}

3、 我将在UIView子类的drawLayer:inContext:方法中实现绘图任务。 drawLayer:inContext:方法是一个绘制图层内容的代理方法。为了能够调用drawLayer:inContext:方法,我们需要设定 图层的代理对象。但要注意,不应该将UIView对象设置为显示层的委托对象,这是因为UIView对象已经是隐式层的代理对象,再将它设置为另一个层的 委托对象就会出问题。轻量级的做法是:编写负责绘图形的代理类。在MyView.h文件中声明如下代码:

@interface MyLayerDelegate : NSObject  
@end

@implementation MyLayerDelegate  
- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)ctx {
    UIGraphicsPushContext(ctx);    
    UIBezierPath* p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
    [[UIColor blueColor] setFill];
    [p fill];    
    UIGraphicsPopContext();  
}  
@end

@interface MyView () {
    MyLayerDelegate *_layerDeleagete;  
}  
@end

// 使用
MyView *myView = [[MyView alloc] initWithFrame: CGRectMake(0, 0, 320, 480)];  
CALayer *myLayer = [CALayer layer];  
_layerDelegate = [[MyLayerDelegate alloc] init];  
myLayer.delegate = _layerDelegate;  
[myView.layer addSublayer:myLayer];  
[myView setNeedsDisplay]; // 调用此方法,drawLayer: inContext:方法才会被调用

4、 使用Core Graphics在drawLayer:inContext:方法中实现同样操作,代码如下:

- (void)drawLayer:(CALayer*)lay inContext:(CGContextRef)con { 
    CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
    CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
    CGContextFillPath(con);  
}

5、 使用UIKit实现:

// 第一个参数表示所要创建的图片的尺寸;第二个参 数用来指定所生成图片的背景是否为不透明,如上我们使用YES而不是NO,则我们得到的图片背景将会是黑色,显然这不是我想要的;第三个参数指定生成图片 的缩放因子,这个缩放因子与UIImage的scale属性所指的含义是一致的。传入0则表示让图片的缩放因子根据屏幕的分辨率而变化,所以我们得到的图 片不管是在单分辨率还是视网膜屏上看起来都会很好。
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO, 0);
UIBezierPath* p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)];  
[[UIColor blueColor] setFill];  
[p fill];  
UIImage *im = UIGraphicsGetImageFromCurrentImageContext(); 
UIGraphicsEndImageContext();

6、 使用Core Graphics实现:

UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO, 0);
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
CGContextFillPath(con);  
UIImage *im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

参考:https://my.oschina.net/u/248165/blog/224309


UIGraphic解码?不知道是否是下面的操作

- (UIImage *)imageWithAspectScaleSize:(CGSize)newSize {
    // 创建一个基于位图的上下文
    UIGraphicsBeginImageContext(newSize);
    CGFloat widthScale = newSize.width / self.size.width;
    CGFloat heightScale = newSize.height / self.size.height;
    CGFloat scale = MIN(widthScale, heightScale);
    CGSize drawSize = CGSizeMake(self.size.width * scale, self.size.width * scale);
    [self drawInRect:CGRectMake((newSize.width - drawSize.width) / 2, (newSize.height - drawSize.height) / 2, drawSize.width, drawSize.height)];
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsPopContext();
    
    return newImage;
}
  • UIGraphicsBeginImageContext 与CGBitmapContextCreate 的区别
    UIGraphicsBeginImageContext is a UIKit wrapper that sits on top of CGBitmapContextCreate and reduces its functionality。也就是说UIGraphicsBeginImageContext高级一点,CGBitmapContextCreate底层一点,功能灵活点

CGContextWithAlpha解码?


CGContextWithoutAlpha解码?


ImageIO解码

  • 步骤

    1. CGImageSourceCreateWithData(data) 创建 ImageSource。
    2. CGImageSourceCreateImageAtIndex(source) 创建一个未解码的 CGImage。
    3. CGImageGetDataProvider(image) 获取这个图片的数据源。
    4. CGDataProviderCopyData(provider) 从数据源获取直接解码的数据。
      ImageIO 解码发生在最后一步,这样获得的数据是没有经过颜色类型转换的原生数据(比如灰度图像)。
  • YYKit中方法

CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFTypeRef)data, NULL);
CGImageRef image = CGImageSourceCreateImageAtIndex(source, 0, (CFDictionaryRef)@{(id)kCGImageSourceShouldCache:@(NO)});
CGImageRef decoded = YYCGImageCreateDecodedCopy(image, YES);
CFRelease(decoded);
CFRelease(image);
CFRelease(source);
CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
    if (!imageRef) return NULL;
    size_t width = CGImageGetWidth(imageRef);
    size_t height = CGImageGetHeight(imageRef);
    if (width == 0 || height == 0) return NULL;
    
    if (decodeForDisplay) { //decode with redraw (may lose some precision)
        // 解码和重绘:可能会丢失一些精度
        CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
        BOOL hasAlpha = NO;
        if (alphaInfo == kCGImageAlphaPremultipliedLast ||
            alphaInfo == kCGImageAlphaPremultipliedFirst ||
            alphaInfo == kCGImageAlphaLast ||
            alphaInfo == kCGImageAlphaFirst) {
            hasAlpha = YES;
        }
        // BGRA8888 (premultiplied) or BGRX8888
        // same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
        CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
        if (!context) return NULL;
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode
        CGImageRef newImage = CGBitmapContextCreateImage(context);
        CFRelease(context);
        return newImage;
        
    } else {
        // 直接解码
        CGColorSpaceRef space = CGImageGetColorSpace(imageRef);
        size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
        size_t bitsPerPixel = CGImageGetBitsPerPixel(imageRef);
        size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);
        CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
        if (bytesPerRow == 0 || width == 0 || height == 0) return NULL;
        
        CGDataProviderRef dataProvider = CGImageGetDataProvider(imageRef);
        if (!dataProvider) return NULL;
        //  这里进行的解码:CGDataProviderCopyData 内部会调用 ImageProviderCopyImageBlockSetCallback 和 copyImageBlock,得到的 CFDataRef 是解码过的像素数组。
        CFDataRef data = CGDataProviderCopyData(dataProvider);
        if (!data) return NULL;
        
        CGDataProviderRef newProvider = CGDataProviderCreateWithCFData(data);
        CFRelease(data);
        if (!newProvider) return NULL;
        
        CGImageRef newImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, space, bitmapInfo, newProvider, NULL, false, kCGRenderingIntentDefault);
        CFRelease(newProvider);
        return newImage;
    }
}

ImageIO编码

  • YYKit中方法
NSString *fileName = [NSString stringWithFormat:@"%@%@_imageio",imageName, imageSize];
NSString *filePath = [[NSBundle mainBundle] pathForResource:fileName ofType:@"png"];
NSData *data = filePath ? [NSData dataWithContentsOfFile:filePath] : nil;
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFTypeRef)data, NULL);
CGImageRef image = CGImageSourceCreateImageAtIndex(source, 0, (CFDictionaryRef)@{(id)kCGImageSourceShouldCache:@(NO)});
CGImageRef decoded = YYCGImageCreateDecodedCopy(image, YES);

CFMutableDataRef outData = NULL;
long length = 0;

if (outData) CFRelease(outData);
outData = CFDataCreateMutable(CFAllocatorGetDefault(), 0);
CGImageDestinationRef dest = CGImageDestinationCreateWithData(outData, (CFStringRef)uti, 1, NULL);
NSDictionary *options = @{(id)kCGImageDestinationLossyCompressionQuality : quality };
CGImageDestinationAddImage(dest, decoded, (CFDictionaryRef)options);
CGImageDestinationFinalize(dest);
length = CFDataGetLength(outData);
CFRelease(dest);

你可能感兴趣的:(图片渲染性能优化)