SDWebImage源码阅读(四)SDWebImageDecoder
一般我们都是使用:
1 + (nullable UIImage *)imageNamed:(NSString *)name; // load from main bundle
和:
1 + (nullable UIImage *)imageWithContentsOfFile:(NSString *)path;
两种方式加载图片,它们两个的区别在SDWebImage源码阅读前的准备(三)UIImage.h 里面的 “(六):加载和创建UIImage 的类方法和实例方法:”部分有详细的介绍。
为什么要对图片进行解码?难道不能直接使用上面的两种加载方式直接进行加载显示吗,答案是可以,而且大概我们编码都是使用上面的两种方式直接在主线程加载图片然后显示在UIImageView上,并且并没有发现什么问题。那为什么SDWebImage 还要费劲去进行解码图片呢,其实我们自己不解码图片我们也是可以直接使用的(其实是系统为我们进行了解码的操作),一般下载的图片或者我们手动拖进主bundle 的图片都是PNG 或者JPG 其他格式的图片,这些图片都是经过编码压缩后的图片数据,并不是控件可以直接显示的位图,如果我们直接使用 "+ (nullable UIImage *)imageNamed:(NSString *)name" 来加载图片,系统默认会在主线程立即进行图片的解码工作,这个过程就是把图片数据解码成可供控件直接显示的位图数据,由于这个解码操作比较耗时,并且默认是在主线程进行,所以当在主线程调用了大量的 "+ (nullable UIImage *)imageNamed:(NSString *)name" 方法后就会产生卡顿。(同时由于位图体积较大,所以在磁盘缓存中不会直接缓存位图数据,而是编码压缩过的PNG 活着JPG 数据)
为了解决这个问题,我们有两种功能比较简单的处理方法:
1.使用 "+ (nullable UIImage *)imageWithContentsOfFile:(NSString *)path" 获取到图片
2.自己解码图片,并且把解码过程放在子线程
下面首先对图片的知识做一些拓展:
参考链接:https://www.objccn.io/issue-21-2/
http://honglu.me/2016/09/02/一张图片引发的深思/
下面首先看 SDWebImageDecoder.h 文件:
1 @interface UIImage (ForceDecode) 2 3 + (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image; 4 5 + (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image; 6 7 @end
其实是一个 UIImage 的 ForceDecode 分类,并定义了两个类方法,一个是解码图像,另一个是压缩图像。
接下来看 SDWebImageDecoder.m 的文件实现:
首先是条件编译:
1 #if SD_UIKIT || SD_WATCH 2 3 xxx... 4 5 #elif SD_MAC 6 + (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image { 7 return image; 8 } 9 10 + (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image { 11 return image; 12 } 13 #endif
如果是 MAC 开发平台,两个方法都是直接返回 image 参数。
如果是 iOS/TV/WATCH 开发平台: + (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image
1 static const size_t kBytesPerPixel = 4; 2 static const size_t kBitsPerComponent = 8; 3 4 + (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image { 5 if (![UIImage shouldDecodeImage:image]) { 6 return image; 7 } 8 9 // autorelease the bitmap context and all vars to help system to free memory when there are memory warning. 10 // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory]; 11 @autoreleasepool{ 12 13 CGImageRef imageRef = image.CGImage; 14 CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef]; 15 16 size_t width = CGImageGetWidth(imageRef); 17 size_t height = CGImageGetHeight(imageRef); 18 size_t bytesPerRow = kBytesPerPixel * width; 19 20 // kCGImageAlphaNone is not supported in CGBitmapContextCreate. 21 // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast 22 // to create bitmap graphics contexts without alpha info. 23 CGContextRef context = CGBitmapContextCreate(NULL, 24 width, 25 height, 26 kBitsPerComponent, 27 bytesPerRow, 28 colorspaceRef, 29 kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast); 30 if (context == NULL) { 31 return image; 32 } 33 34 // Draw the image into the context and retrieve the new bitmap image without alpha 35 CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); 36 CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context); 37 UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha 38 scale:image.scale 39 orientation:image.imageOrientation]; 40 41 CGContextRelease(context); 42 CGImageRelease(imageRefWithoutAlpha); 43 44 return imageWithoutAlpha; 45 } 46 }
两个静态不可变的类型是 size_t 的变量:
1 static const size_t kBytesPerPixel = 4;
kBytesPerPixel 按命名英语直译过来是每个像素占内存多少字节(Byte),赋值为4,表示每个像素占4个字节。(图像在iOS 设备上是以像素为单位显示的)
1 static const size_t kBitsPerComponent = 8;
kBitsPerComponent 按命名英语直译过来是每个组件占多少位(Bit),这个不好理解,举个例子,比如RGBA,其中 R (红色)G(绿色)B(蓝色)A(透明度)总共4个组件,每个像素由这4个组件组成,且该变量被赋值为8,所以一个 RGBA 像素就是8 * 4 = 32 Bits。
知道了 kBitsPerComponent 和每个像素有多少组件组成就能计算 kBytesPerPixel 了。计算公式是: (bitsPerComponent * number of components + 7)/ 8。
[UIImage shouldDecodeImage:image]
判断要不要解码,并不是所有的image 都要解码,看 + (BOOL)shouldDecodeImage:(nullable UIImage *)image 函数实现:
1 + (BOOL)shouldDecodeImage:(nullable UIImage *)image { 2 // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error 3 if (image == nil) { 4 return NO; 5 } 6 7 // do not decode animated images 8 if (image.images != nil) { 9 return NO; 10 } 11 12 CGImageRef imageRef = image.CGImage; 13 14 CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef); 15 BOOL anyAlpha = (alpha == kCGImageAlphaFirst || 16 alpha == kCGImageAlphaLast || 17 alpha == kCGImageAlphaPremultipliedFirst || 18 alpha == kCGImageAlphaPremultipliedLast); 19 // do not decode images with alpha 20 if (anyAlpha) { 21 return NO; 22 } 23 24 return YES; 25 }
1.如果 image 等于 nil 返回 NO。 // 防止 "CGBitmapContextCreateImage: invalid context 0X0" 的错误
2.如果 image 是动效图片,即 image.images 不等于 nil,返回 NO。// 不要解码动画图像
3.通过 CGImageGetAlphaInfo(image.CGImage) 获取 CGImageAlphaInfo alpha,如果 alpha 等于
1 kCGImageAlphaPremultipliedLast, /* For example, premultiplied RGBA */ 2 kCGImageAlphaPremultipliedFirst, /* For example, premultiplied ARGB */ 3 kCGImageAlphaLast, /* For example, non-premultiplied RGBA */ 4 kCGImageAlphaFirst, /* For example, non-premultiplied ARGB */
任一个枚举值,即带有 A(透明度因素),返回 NO。// 不要用alpha 解码图像
注释翻译:
// 当有内存警告的时候,自动释放 bitmap context 和所有的 vars 释放系统内存
// 在iOS 7 ,别忘了调用 [[SDImageCache sharedImageCache] clearMemory];
CGImageRef imageref
1 CGImageRef imageRef = image.CGImage;
CGColorSpaceRef colorspaceRef
1 CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef];
通过 imageRef 获得颜色空间(CGColorSpaceRef)colorspaceRef,获得没有 alpha 通道的colorspaceRef,下面看 + (CGColorSpaceRef)colorSpaceForImageRef:(CGImageRef)imageRef 函数实现:
1 + (CGColorSpaceRef)colorSpaceForImageRef:(CGImageRef)imageRef { 2 // current 3 CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef)); 4 CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef); 5 6 BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown || 7 imageColorSpaceModel == kCGColorSpaceModelMonochrome || 8 imageColorSpaceModel == kCGColorSpaceModelCMYK || 9 imageColorSpaceModel == kCGColorSpaceModelIndexed); 10 if (unsupportedColorSpace) { 11 colorspaceRef = CGColorSpaceCreateDeviceRGB(); 12 CFAutorelease(colorspaceRef); 13 } 14 return colorspaceRef; 15 }
CGImageGetColorSpace(imageRef)
1 /* Return the color space of `image', or NULL if `image' is an image 2 mask. */ 3 4 CG_EXTERN CGColorSpaceRef __nullable CGImageGetColorSpace(CGImageRef cg_nullable image) 5 CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
CGColorSpaceGetModel(CGImageGetColorSpace(imageRef))
1 /* Return the color space model of `space'. */ 2 3 CG_EXTERN CGColorSpaceModel CGColorSpaceGetModel(CGColorSpaceRef cg_nullable space) 4 CG_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);
CGColorSpaceCreateDeviceRGB()
这里很重要,判断如果有 alpha 则创建一个 DeviceRGB 颜色的空间。
1 BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown || 2 imageColorSpaceModel == kCGColorSpaceModelMonochrome || 3 imageColorSpaceModel == kCGColorSpaceModelCMYK || 4 imageColorSpaceModel == kCGColorSpaceModelIndexed); 5 if (unsupportedColorSpace) { 6 colorspaceRef = CGColorSpaceCreateDeviceRGB(); 7 CFAutorelease(colorspaceRef); 8 }
这三个函数都是 CoreGraphics 框架里面的。
CGImageGetWidth(imageRef)
1 size_t width = CGImageGetWidth(imageRef);
1 /* Return the width of `image'. */ 2 3 CG_EXTERN size_t CGImageGetWidth(CGImageRef cg_nullable image) 4 CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
CGImageGetHeight(imageRef)
1 size_t height = CGImageGetHeight(imageRef);
1 /* Return the height of `image'. */ 2 3 CG_EXTERN size_t CGImageGetHeight(CGImageRef cg_nullable image) 4 CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
bytesPerRow
1 size_t bytesPerRow = kBytesPerPixel * width;
表示每行占内存多少个字节,计算方法是行宽乘以每个像素在内存占几个字节。
// kCGImageAlphaNone 表示不支持 CGBitmapContextCreate
// 因为这里的原始图像没有 alpha 信息,使用 kCGImageAlphaNoneSkipLast
// 创建bitmap graphics contexts 没有 alpha 信息
1 CGContextRef context = CGBitmapContextCreate(NULL, 2 width, 3 height, 4 kBitsPerComponent, 5 bytesPerRow, 6 colorspaceRef, 7 kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
1 /* Create a bitmap context. The context draws into a bitmap which is `width' 2 pixels wide and `height' pixels high. The number of components for each 3 pixel is specified by `space', which may also specify a destination color 4 profile. The number of bits for each component of a pixel is specified by 5 `bitsPerComponent'. The number of bytes per pixel is equal to 6 `(bitsPerComponent * number of components + 7)/8'. Each row of the bitmap 7 consists of `bytesPerRow' bytes, which must be at least `width * bytes 8 per pixel' bytes; in addition, `bytesPerRow' must be an integer multiple 9 of the number of bytes per pixel. `data', if non-NULL, points to a block 10 of memory at least `bytesPerRow * height' bytes. If `data' is NULL, the 11 data for context is allocated automatically and freed when the context is 12 deallocated. `bitmapInfo' specifies whether the bitmap should contain an 13 alpha channel and how it's to be generated, along with whether the 14 components are floating-point or integer. */ 15 16 CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data, 17 size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow, 18 CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo) 19 CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
创建没有透明因素的 bitmap graphics contexts (主要在参数 colorspaceRef,不包含 alpha 通道,和
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast )
注意:
这里创建的 contexts 是没有透明因素的。在 UI 渲染的时候,实际上是把多个图层按像素叠加计算的过程,需要对每一个像素进行 RGBA 的叠加计算(R、G、B 分别都要乘以 A)。当某个 layer 的是不透明的,也就是 opaque 为 YES 时,GPU 可以直接忽略掉其下方的图层。这也是调用 CGBitmapContextCreate 时 bitmapInfo 参数设置为忽略掉 alpha 通道的原因。
// 将图像绘制到上下文中,并在没有alpha 信息的情况下检索新位图图像
绘制图像
1 // Draw the image into the context and retrieve the new bitmap image without alpha 2 CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); 3 CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context); 4 UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha 5 scale:image.scale 6 orientation:image.imageOrientation]; 7 8 CGContextRelease(context); 9 CGImageRelease(imageRefWithoutAlpha);
1.CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef)
1 /** Image functions. **/ 2 3 /* Draw `image' in the rectangular area specified by `rect' in the context 4 `c'. The image is scaled, if necessary, to fit into `rect'. */ 5 6 CG_EXTERN void CGContextDrawImage(CGContextRef cg_nullable c, CGRect rect, 7 CGImageRef cg_nullable image) 8 CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
2.CGBitmapContextCreateImage(context)
1 /* Return an image containing a snapshot of the bitmap context `context'. If 2 context is not a bitmap context, or if the image cannot be created for 3 any reason, this function returns NULL. This is a "copy" operation --- 4 subsequent changes to context will not affect the contents of the 5 returned image. 6 7 Note that in some cases the copy will actually follow "copy-on-write" 8 semantics, so that the actual physical copy of the bits will only occur 9 if the underlying data in the bitmap context is modified. As a 10 consequence, you may wish to use the resulting image and release it 11 before performing more drawing into the bitmap context; in this way, the 12 actual physical copy of the data may be avoided. */ 13 14 CG_EXTERN CGImageRef __nullable CGBitmapContextCreateImage( 15 CGContextRef cg_nullable context) 16 CG_AVAILABLE_STARTING(__MAC_10_4, __IPHONE_2_0);
3.根据返回的 CGImageRef 初始化一个 UIImage。
4.CGContextRelease(context)
1 /* Equivalent to `CFRelease(c)'. */ 2 3 CG_EXTERN void CGContextRelease(CGContextRef cg_nullable c) 4 CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
5.CGImageRelease(imageRefWithoutAlpha)
1 /* Equivalent to `CFRelease(image)'. */ 2 3 CG_EXTERN void CGImageRelease(CGImageRef cg_nullable image) 4 CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
6.return imageWithoutAlpha;
下面看压缩图像的方法:+ (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image, 看方法实现之前先看下几个静态不可变 CGFloat 变量:
1 /* 2 * Defines the maximum size in MB of the decoded image when the flag `SDWebImageScaleDownLargeImages` is set 3 * Suggested value for iPad1 and iPhone 3GS: 60. 4 * Suggested value for iPad2 and iPhone 4: 120. 5 * Suggested value for iPhone 3G and iPod 2 and earlier devices: 30. 6 */ 7 static const CGFloat kDestImageSizeMB = 60.0f; 8 9 /* 10 * Defines the maximum size in MB of a tile used to decode image when the flag `SDWebImageScaleDownLargeImages` is set 11 * Suggested value for iPad1 and iPhone 3GS: 20. 12 * Suggested value for iPad2 and iPhone 4: 40. 13 * Suggested value for iPhone 3G and iPod 2 and earlier devices: 10. 14 */ 15 static const CGFloat kSourceImageTileSizeMB = 20.0f; 16 17 static const CGFloat kBytesPerMB = 1024.0f * 1024.0f; 18 static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel; 19 static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB; 20 static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB; 21 22 static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet.
kDestImageSizeMB 表示需要压缩图像源的大小界限,并且赋值是60 默认的单位是MB。
当然要压缩一张图像的时候,首先要把图像源文件的大小和 kDestImageSizeMB 进行比较,如果源文件大小超过了 kDestImageSizeMB,那么该图像需要被压缩。
SDWebImage 的建议:
* Suggested value for iPad1 and iPhone 3GS: 60.
* Suggested value for iPad2 and iPhone 4: 120.
* Suggested value for iPhone 3G and iPod 2 and earlier devices: 30.
kSourceImageTileSizeMb 表示用于分割源图像方块的大小,这个方块将会被用来分割原图,并且赋值为20 默认单位是MB。
SDWebImage 的建议:
* Suggested value for iPad1 and iPhone 3GS: 20.
* Suggested value for iPad2 and iPhone 4: 40.
* Suggested value for iPhone 3G and iPod 2 and earlier devices: 10.
kBytesPerMB 表示1 MB 有多少字节,相信对此都特别熟悉(1 *1024 * 1024)。
1 static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
kPixelsPerMB 表示1 MB 有多少像素,算法是:1 MB 有多少字节除以 1 个像素占多少字节(4 字节),即:kBytesPerMB / kBytesPerPixel == (1024 * 1024 /4 = 262144 个像素)。
1 static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;
kDestTotalPixels 表示图像需要压缩时的图像像素的界限,算法是:最大支持的压缩图像源有多少 MB 乘以1 MB 有多少像素。即: kDestImageSizeMB * kPixelsPerMB == (60 * 262144 = 15728640 个像素)。
1 static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB;
kTileTotalPixels 表示用于分割图像源的方块的总像素,算法是:原图方块有多少 MB 乘以1 MB 有多少像素。即:kSourceImageTileSizeMB * kPixelsPerMB == (20 * 262144 =5242880 个像素)。
1 static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB;
kDestSeemOverlap 表示重叠像素大小,并赋值为2。
1 static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet.
好了,下面开始看 + (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image 函数实现:
1 + (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image { 2 if (![UIImage shouldDecodeImage:image]) { 3 return image; 4 } 5 6 if (![UIImage shouldScaleDownImage:image]) { 7 return [UIImage decodedImageWithImage:image]; 8 } 9 10 CGContextRef destContext; 11 12 // autorelease the bitmap context and all vars to help system to free memory when there are memory warning. 13 // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory]; 14 @autoreleasepool { 15 CGImageRef sourceImageRef = image.CGImage; 16 17 CGSize sourceResolution = CGSizeZero; 18 sourceResolution.width = CGImageGetWidth(sourceImageRef); 19 sourceResolution.height = CGImageGetHeight(sourceImageRef); 20 float sourceTotalPixels = sourceResolution.width * sourceResolution.height; 21 // Determine the scale ratio to apply to the input image 22 // that results in an output image of the defined size. 23 // see kDestImageSizeMB, and how it relates to destTotalPixels. 24 float imageScale = kDestTotalPixels / sourceTotalPixels; 25 CGSize destResolution = CGSizeZero; 26 destResolution.width = (int)(sourceResolution.width*imageScale); 27 destResolution.height = (int)(sourceResolution.height*imageScale); 28 29 // current color space 30 CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:sourceImageRef]; 31 32 size_t bytesPerRow = kBytesPerPixel * destResolution.width; 33 34 // Allocate enough pixel data to hold the output image. 35 void* destBitmapData = malloc( bytesPerRow * destResolution.height ); 36 if (destBitmapData == NULL) { 37 return image; 38 } 39 40 // kCGImageAlphaNone is not supported in CGBitmapContextCreate. 41 // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast 42 // to create bitmap graphics contexts without alpha info. 43 destContext = CGBitmapContextCreate(destBitmapData, 44 destResolution.width, 45 destResolution.height, 46 kBitsPerComponent, 47 bytesPerRow, 48 colorspaceRef, 49 kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast); 50 51 if (destContext == NULL) { 52 free(destBitmapData); 53 return image; 54 } 55 CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh); 56 57 // Now define the size of the rectangle to be used for the 58 // incremental blits from the input image to the output image. 59 // we use a source tile width equal to the width of the source 60 // image due to the way that iOS retrieves image data from disk. 61 // iOS must decode an image from disk in full width 'bands', even 62 // if current graphics context is clipped to a subrect within that 63 // band. Therefore we fully utilize all of the pixel data that results 64 // from a decoding opertion by achnoring our tile size to the full 65 // width of the input image. 66 CGRect sourceTile = CGRectZero; 67 sourceTile.size.width = sourceResolution.width; 68 // The source tile height is dynamic. Since we specified the size 69 // of the source tile in MB, see how many rows of pixels high it 70 // can be given the input image width. 71 sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width ); 72 sourceTile.origin.x = 0.0f; 73 // The output tile is the same proportions as the input tile, but 74 // scaled to image scale. 75 CGRect destTile; 76 destTile.size.width = destResolution.width; 77 destTile.size.height = sourceTile.size.height * imageScale; 78 destTile.origin.x = 0.0f; 79 // The source seem overlap is proportionate to the destination seem overlap. 80 // this is the amount of pixels to overlap each tile as we assemble the ouput image. 81 float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height); 82 CGImageRef sourceTileImageRef; 83 // calculate the number of read/write operations required to assemble the 84 // output image. 85 int iterations = (int)( sourceResolution.height / sourceTile.size.height ); 86 // If tile height doesn't divide the image height evenly, add another iteration 87 // to account for the remaining pixels. 88 int remainder = (int)sourceResolution.height % (int)sourceTile.size.height; 89 if(remainder) { 90 iterations++; 91 } 92 // Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations. 93 float sourceTileHeightMinusOverlap = sourceTile.size.height; 94 sourceTile.size.height += sourceSeemOverlap; 95 destTile.size.height += kDestSeemOverlap; 96 for( int y = 0; y < iterations; ++y ) { 97 @autoreleasepool { 98 sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap; 99 destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap); 100 sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile ); 101 if( y == iterations - 1 && remainder ) { 102 float dify = destTile.size.height; 103 destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale; 104 dify -= destTile.size.height; 105 destTile.origin.y += dify; 106 } 107 CGContextDrawImage( destContext, destTile, sourceTileImageRef ); 108 CGImageRelease( sourceTileImageRef ); 109 } 110 } 111 112 CGImageRef destImageRef = CGBitmapContextCreateImage(destContext); 113 CGContextRelease(destContext); 114 if (destImageRef == NULL) { 115 return image; 116 } 117 UIImage *destImage = [UIImage imageWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation]; 118 CGImageRelease(destImageRef); 119 if (destImage == nil) { 120 return image; 121 } 122 return destImage; 123 } 124 }
如何把一个很大的原图压缩成指定的大小?
首先定义了一个静态不可变的 CGFloat 变量 kSourceImageTileSizeMB 并赋值为20,表示一个固定大小为20 MB的宽度和传进来的 UIImage 图像的宽度相等的图像方块(方块的高度可以根据大小为20 MB 乘以1 MB 多少像素得出总的像素数再除以宽度得出),然后把传进来的 UIImage 图像按照方块的高度进行分割,最后把分割的每个方块的数据画到一个目标画布上,最终得到一个目标图像。
1.[UIImage shouldDecodeImage:image]
1 if (![UIImage shouldDecodeImage:image]) { 2 return image; 3 } 4 5 6 + (BOOL)shouldDecodeImage:(nullable UIImage *)image { 7 // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error 8 if (image == nil) { 9 return NO; 10 } 11 12 // do not decode animated images 13 if (image.images != nil) { 14 return NO; 15 } 16 17 CGImageRef imageRef = image.CGImage; 18 19 CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef); 20 BOOL anyAlpha = (alpha == kCGImageAlphaFirst || 21 alpha == kCGImageAlphaLast || 22 alpha == kCGImageAlphaPremultipliedFirst || 23 alpha == kCGImageAlphaPremultipliedLast); 24 // do not decode images with alpha 25 if (anyAlpha) { 26 return NO; 27 } 28 29 return YES; 30 }
检测图像能不能解码。
2.[UIImage shouldScaleDownImage:image]
1 if (![UIImage shouldScaleDownImage:image]) { 2 return [UIImage decodedImageWithImage:image]; 3 } 4 5 + (BOOL)shouldScaleDownImage:(nonnull UIImage *)image { 6 BOOL shouldScaleDown = YES; 7 8 CGImageRef sourceImageRef = image.CGImage; 9 CGSize sourceResolution = CGSizeZero; 10 sourceResolution.width = CGImageGetWidth(sourceImageRef); 11 sourceResolution.height = CGImageGetHeight(sourceImageRef); 12 float sourceTotalPixels = sourceResolution.width * sourceResolution.height; 13 float imageScale = kDestTotalPixels / sourceTotalPixels; 14 if (imageScale < 1) { 15 shouldScaleDown = YES; 16 } else { 17 shouldScaleDown = NO; 18 } 19 20 return shouldScaleDown; 21 }
检测图像应不应该去压缩,判断标准是传入的UIImage 图像的像素数是否大于 kDestTotalPixels (图像需要压缩时的图像像素的界限)。
3.destContext
1 CGContextRef destContext; 2 3 typedef struct CGContext *CGContextRef;
4.@autoreleasepool { }
1 // autorelease the bitmap context and all vars to help system to free memory when there are memory warning. 2 // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory]; 3 4 // 当有内存警告的时候,自动释放 bitmap context 和所有的 vars 释放系统内存 5 // 在iOS 7 ,别忘了调用 [[SDImageCache sharedImageCache] clearMemory];
5.sourceImageRef
1 CGImageRef sourceImageRef = image.CGImage;
6.sourceResolution/sourceTotalPixels/imageScale
1 CGSize sourceResolution = CGSizeZero; 2 sourceResolution.width = CGImageGetWidth(sourceImageRef); 3 sourceResolution.height = CGImageGetHeight(sourceImageRef); 4 float sourceTotalPixels = sourceResolution.width * sourceResolution.height; 5 // Determine the scale ratio to apply to the input image 6 // that results in an output image of the defined size. 7 // see kDestImageSizeMB, and how it relates to destTotalPixels. 8 float imageScale = kDestTotalPixels / sourceTotalPixels;
根据 sourceImageRef 获得源图像的宽和高,计算出源图像的总像素数 sourceTotalPixels,根据 kDestTotalPixels / sourceTotalPixels 计算出源图像的压缩比例 imageScale。
7.destResolution
1 CGSize destResolution = CGSizeZero; 2 destResolution.width = (int)(sourceResolution.width*imageScale); 3 destResolution.height = (int)(sourceResolution.height*imageScale);
使用 imageScale 分别乘以 sourceResolution.width 和 sourceResolution.height 获得源图像压缩后获得的目标图像的宽和高。
8.colorspaceRef
1 // current color space 2 CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:sourceImageRef]; 3 4 + (CGColorSpaceRef)colorSpaceForImageRef:(CGImageRef)imageRef { 5 // current 6 CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef)); 7 CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef); 8 9 BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown || 10 imageColorSpaceModel == kCGColorSpaceModelMonochrome || 11 imageColorSpaceModel == kCGColorSpaceModelCMYK || 12 imageColorSpaceModel == kCGColorSpaceModelIndexed); 13 if (unsupportedColorSpace) { 14 colorspaceRef = CGColorSpaceCreateDeviceRGB(); 15 CFAutorelease(colorspaceRef); 16 } 17 return colorspaceRef; 18 }
获得当前的颜色空间 colorspaceRef。
9.destBitmapData
1 size_t bytesPerRow = kBytesPerPixel * destResolution.width; 2 3 // Allocate enough pixel data to hold the output image. 4 void* destBitmapData = malloc( bytesPerRow * destResolution.height ); 5 if (destBitmapData == NULL) { 6 return image; 7 }
先根据压缩后得到的目标图像的宽度乘以每个像素占多少内存字节得到目标图像的每一行的占用的内存字节 bytesPerRow,然后创建申请目标图像的内存空间 destBitmapData(目标图像的每一行占的内存大小乘以目标图像的高度)。
10.CGBitmapContextCreate
1 // kCGImageAlphaNone is not supported in CGBitmapContextCreate. 2 // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast 3 // to create bitmap graphics contexts without alpha info. 4 destContext = CGBitmapContextCreate(destBitmapData, 5 destResolution.width, 6 destResolution.height, 7 kBitsPerComponent, 8 bytesPerRow, 9 colorspaceRef, 10 kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast); 11 12 if (destContext == NULL) { 13 free(destBitmapData); 14 return image; 15 }
1 /* Create a bitmap context. The context draws into a bitmap which is `width' 2 pixels wide and `height' pixels high. The number of components for each 3 pixel is specified by `space', which may also specify a destination color 4 profile. The number of bits for each component of a pixel is specified by 5 `bitsPerComponent'. The number of bytes per pixel is equal to 6 `(bitsPerComponent * number of components + 7)/8'. Each row of the bitmap 7 consists of `bytesPerRow' bytes, which must be at least `width * bytes 8 per pixel' bytes; in addition, `bytesPerRow' must be an integer multiple 9 of the number of bytes per pixel. `data', if non-NULL, points to a block 10 of memory at least `bytesPerRow * height' bytes. If `data' is NULL, the 11 data for context is allocated automatically and freed when the context is 12 deallocated. `bitmapInfo' specifies whether the bitmap should contain an 13 alpha channel and how it's to be generated, along with whether the 14 components are floating-point or integer. */ 15 16 CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data, 17 size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow, 18 CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo) 19 CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
创建目标图像上下文 destContext,CGBitmapContextCreate(...) 方法传入的参数都是上面定义的变量。
11.CGContextSetInterpolationQuality
1 CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh); 1 /* Set the interpolation quality of `context' to `quality'. */ 2 3 CG_EXTERN void CGContextSetInterpolationQuality(CGContextRef cg_nullable c, 4 CGInterpolationQuality quality) 5 CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0); 1 /* Interpolation quality. */ 2 3 typedef CF_ENUM (int32_t, CGInterpolationQuality) { 4 kCGInterpolationDefault = 0, /* Let the context decide. */ 5 kCGInterpolationNone = 1, /* Never interpolate. */ 6 kCGInterpolationLow = 2, /* Low quality, fast interpolation. */ 7 kCGInterpolationMedium = 4, /* Medium quality, slower than kCGInterpolationLow. */ 8 kCGInterpolationHigh = 3 /* Highest quality, slower than kCGInterpolationMedium. */ 9 };
设置压缩后的图像的质量。
12.sourceTile
1 // Now define the size of the rectangle to be used for the 2 // incremental blits from the input image to the output image. 3 // we use a source tile width equal to the width of the source 4 // image due to the way that iOS retrieves image data from disk. 5 // iOS must decode an image from disk in full width 'bands', even 6 // if current graphics context is clipped to a subrect within that 7 // band. Therefore we fully utilize all of the pixel data that results 8 // from a decoding opertion by achnoring our tile size to the full 9 // width of the input image. 10 CGRect sourceTile = CGRectZero; 11 sourceTile.size.width = sourceResolution.width; 12 // The source tile height is dynamic. Since we specified the size 13 // of the source tile in MB, see how many rows of pixels high it 14 // can be given the input image width. 15 sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width ); 16 sourceTile.origin.x = 0.0f;
定义一个 CGRect 变量 sourceTile 表示用来分割源图像的方块的 rect,宽度和源图像的宽度一样,高度根据固定的方块的占内存的大小(20 MB)算出方块的总的像素数再除以宽度得出,x 赋值为 0。
13.destTile
1 // The output tile is the same proportions as the input tile, but 2 // scaled to image scale. 3 CGRect destTile; 4 destTile.size.width = destResolution.width; 5 destTile.size.height = sourceTile.size.height * imageScale; 6 destTile.origin.x = 0.0f;
定义一个 CGRect 变量 destTile 表示压缩后的分割目标图像的方块的 rect,宽度和源图像一样,高度是 sourceTile 的高度乘以imageScale (压缩比例),x 赋值为 0。
14.sourceSeemOverlap
1 // The source seem overlap is proportionate to the destination seem overlap. 2 // this is the amount of pixels to overlap each tile as we assemble the ouput image. 3 float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
计算源图像与压缩后目标图像重叠的像素大小。
15.sourceTileImageRef
1 CGImageRef sourceTileImageRef;
16.iterations/remainder
1 // calculate the number of read/write operations required to assemble the 2 // output image. 3 int iterations = (int)( sourceResolution.height / sourceTile.size.height ); 4 // If tile height doesn't divide the image height evenly, add another iteration 5 // to account for the remaining pixels. 6 int remainder = (int)sourceResolution.height % (int)sourceTile.size.height; 7 if(remainder) { 8 iterations++; 9 }
源图像的高度除以分割源图像的方块的高度得出源图像被分割成多少个方块并赋值给 iterations,再做取余运算取得分割的最大的整数。
17.分割图片并写入新图
1 // Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations. 2 float sourceTileHeightMinusOverlap = sourceTile.size.height; 3 sourceTile.size.height += sourceSeemOverlap; 4 destTile.size.height += kDestSeemOverlap; 5 for( int y = 0; y < iterations; ++y ) { 6 @autoreleasepool { 7 sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap; 8 destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap); 9 sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile ); 10 if( y == iterations - 1 && remainder ) { 11 float dify = destTile.size.height; 12 destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale; 13 dify -= destTile.size.height; 14 destTile.origin.y += dify; 15 } 16 CGContextDrawImage( destContext, destTile, sourceTileImageRef ); 17 CGImageRelease( sourceTileImageRef ); 18 } 19 }
17.1 定义一个 float 变量 sourceTitleHeightMinusOverlap 存放那个用来分割源图像,大小为 20 MB 的方块的高度。
17.2 用于切割源图像大小为 20 MB 的方块的高度加上源图像与源图像分割方块的像素重叠数。
17.3 目标图像的分割方块的高度加上 kDestSeemOverlap(像素重叠数赋值为 2)。
17.4 进行for 循环,y 从0开始,到小于源图像被分割的块数。
17.5 sourceTile 和 destTile 都是宽度和高度固定的,x 值为 0,只有 y 值随着循环的 y 值在变化,sourceTile 的 y 值在递增,destTile 的 y 值在递减。
17.6 然后循环的从源图像的 sourceImageRef 根据大小为 20 MB 的分割块的不同 CGRect 的矩形区域内获取 sourceTileImageRef。
17.7 然后把 sourceTileImageRef 写入和源图像分割块相对应的目标图像的分割块的矩形区域的 CGContextRef。
17.8 另外每一次都要 CGImageRelease(sourceTileImageRef)。
18.destImage
1 CGImageRef destImageRef = CGBitmapContextCreateImage(destContext); 2 CGContextRelease(destContext); 3 if (destImageRef == NULL) { 4 return image; 5 } 6 UIImage *destImage = [UIImage imageWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation]; 7 CGImageRelease(destImageRef); 8 if (destImage == nil) { 9 return image; 10 } 11 return destImage;
CGBitmapContextCreateImage(destContext) 获得目标图像的 CGImageRef 并做参数创建 destImage,返回destImage。
参考链接:http://www.jianshu.com/p/9322acb7a7b1
END