1、简单介绍html
研究了下SDWebImage的源码,借鉴了别人的一些资料,感受逐渐的明白的一些原理,如今就来记录下。算法
在咱们使用 UIImage 的时候,建立的图片一般不会直接加载到内存,而是在渲染的时候默认在主线程上再进行解码并加载到内存。这就会致使 UIImage 在渲染的时候效率上不是那么高效。为了提升效率因此在SDWebImage中就采起在子线程中进行解码图片。安全
这里再介绍下为何建立图像的时候是须要解码的由于通常下载的图片或者是咱们手动拖进工程的图片都是PNG 或者JPEG或者是其余格式的图片,这些图片都是通过编码压缩后的图片数据,并非咱们的控件能够直接显示的位图,若是咱们直接使用加载渲染图片到手机上的时候,系统默认会在主线程当即进行图片的解码工做,这个过程就是把图片数据解码成能够供给控件直接显示的位图数据,因为这个解码操做比较耗时,而且默认是在主线程进行,因此若是加载过多的图片的话确定是会发生卡顿现象的。app
2、源码分析框架
首先介绍的是根据data来解码成一个UIImage对象的方法ide
- (UIImage *)decodedImageWithData:(NSData *)data { if (!data) { return nil; } UIImage *image = [[UIImage alloc] initWithData:data]; //若是是MAC端就直接返回image #if SD_MAC return image; #else if (!image) { return nil; } //否则的话就要去获取数据中图片的方向 UIImageOrientation orientation = [[self class] sd_imageOrientationFromImageData:data]; //若是图片的方向不是默认向上的话就要去根据其图片信息的方向来从新建立图片 if (orientation != UIImageOrientationUp) { image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:orientation]; } return image; #endif }
这里再介绍下关于获取图片方向的sd_imageOrientationFromImageData方法,这里面其实就是根据imageData去建立CGImageSourceRef而后去读取其图像的属性函数
+ (UIImageOrientation)sd_imageOrientationFromImageData:(nonnull NSData *)imageData { UIImageOrientation result = UIImageOrientationUp; //建立从Core Foundation 数据对象中读取的图像源 /** 参数1: 参数2:指定额外建立option字典。咱们能够在options字典中包含的键来建立图像源。 好比说 kCGImageSourceTypeIdentifierHint kCGImageSourceShouldAllowFloat kCGImageSourceShouldCache kCGImageSourceCreateThumbnailFromImageIfAbsent kCGImageSourceCreateThumbnailFromImageAlways kCGImageSourceThumbnailMaxPixelSize kCGImageSourceCreateThumbnailWithTransform */ CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL); if (imageSource) { //调用CGImageSourceCopyPropertiesAtIndex的时候会才去读取图像元数据 //返回图像源中指定位置的图像的属性。 /** 参数1:一个图像的来源 参数2:你想要得到的属性的索引。该指数是从零开始的。index参数设置获取第几张图像 参数3:能够用来请求其余选项的字典。 返回包含与图像相关联的属性的字典。请参见CGImageProperties,以得到能够在字典中使用的属性列表。 CGImageProperties引用定义了表明图像I/O框架使用的图像特征的常量。 */ CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); //判断属性存不存在,若是不存在就用默认的UIImageOrientationUp方向 if (properties) { //typedef const void *CFTypeRef; CFTypeRef val; NSInteger exifOrientation; //返回与给定键关联的值,这里就是返回方向键值所对应的内容 val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation); //若是其存在的话,就去获取 if (val) { //将CFNumber对象转换为指定类型的值 /** 参数1:要检查的CFNumber对象。 参数2:指定要返回的数据类型的常量。请参阅CFNumberType以得到可能的值列表。 参数3:返回的时候包含数字的值 */ CFNumberGetValue(val, kCFNumberNSIntegerType, &exifOrientation); //转换exif中信息的方向到iOS里面的方向 result = [SDWebImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation]; } // else - if it's not set it remains at up CFRelease((CFTypeRef) properties); } else { //NSLog(@"NO PROPERTIES, FAIL"); } //释放这个图像源 CFRelease(imageSource); } //返回结果 return result; }
这里再介绍的介绍下imageOrientationFromEXIFOrientation的方法,这个方法就是转换一个EXIF信息中图像方向到iOS中的方向说白了就是从NSInteger转换为UIImageOrientationUp这样的枚举值。源码分析
// Convert an EXIF image orientation to an iOS one. + (UIImageOrientation)imageOrientationFromEXIFOrientation:(NSInteger)exifOrientation { // CGImagePropertyOrientation is available on iOS 8 above. Currently kept for compatibility UIImageOrientation imageOrientation = UIImageOrientationUp; switch (exifOrientation) { case 1: imageOrientation = UIImageOrientationUp; break; case 3: imageOrientation = UIImageOrientationDown; break; case 8: imageOrientation = UIImageOrientationLeft; break; case 6: imageOrientation = UIImageOrientationRight; break; case 2: imageOrientation = UIImageOrientationUpMirrored; break; case 4: imageOrientation = UIImageOrientationDownMirrored; break; case 5: imageOrientation = UIImageOrientationLeftMirrored; break; case 7: imageOrientation = UIImageOrientationRightMirrored; break; default: break; } return imageOrientation; }
接下来开始讲下解码图像,在这里面其实刚开始先判断能不能解码图片,这个方法是这样的优化
+ (BOOL)shouldDecodeImage:(nullable UIImage *)image { // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error //若是图片都为空,那确定返回的是NO if (image == nil) { return NO; } //不能编码动画图片 // do not decode animated images if (image.images != nil) { return NO; } CGImageRef imageRef = image.CGImage; BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef); //不支持解码含有透明度的图片 // do not decode images with alpha if (hasAlpha) { return NO; } return YES; }
回到这个方法,其实主要的过程就是CGBitmapContextCreate建立一个位图上下文→CGContextDrawImage绘制原始位图到上下文→CGBitmapContextCreateImage建立解码后的新位图。动画
- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image { if (![[self class] shouldDecodeImage:image]) { return image; } // 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]; //新建自动释放池,将bitmap context和临时变量都添加到池中在方法末尾自动释放以防止内存警告 @autoreleasepool{ //获取传入的UIImage对应的CGImageRef(位图) CGImageRef imageRef = image.CGImage; //获取彩色空间 CGColorSpaceRef colorspaceRef = [[self class] colorSpaceForImageRef:imageRef]; //获取高和宽 size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); // 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. //初始化bitmap graphics context 上下文 /* 参数1:指向要呈现绘图的内存中目标的指针。这个内存块的大小至少应该是(bytesPerRow*height)字节。 若是但愿此函数为位图分配内存,则传递NULL。这将使您没必要管理本身的内存,从而减小内存泄漏问题。 参数2:所需宽度,以像素为单位 参数3:所需高度 参数4:用于内存中一个像素的每一个组件的比特数 参数5:位图中每一行使用的内存字节数。若是数据参数为NULL,传递值为0,则会自动计算值。 参数6:颜色空间 参数7:指定位图是否应该包含一个alpha通道、alpha通道在一个像素中的相对位置,以及关于像素组件是浮点数仍是整数值的信息。 指定alpha通道信息的常量使用CGImageAlphaInfo类型声明,能够安全地传递给该参数。 您还能够传递与CGBitmapInfo类型相关联的其余常量。 例如,如何指定颜色空间、每一个像素的位元、每一个像素的位元以及位图信息,请参阅图形上下文。 */ //kCGBitmapByteOrderDefault 是默认模式,对于iPhone 来讲,采用的是小端模式 CGContextRef context = CGBitmapContextCreate(NULL, width, height, kBitsPerComponent, 0, colorspaceRef, kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast); //若是上下文为NULL,就返回image if (context == NULL) { return image; } // Draw the image into the context and retrieve the new bitmap image without alpha /** 这里建立的contexts是没有透明因素的。在UI渲染的时候,其实是把多个图层按像素叠加计算的过程,须要对每个像素进行 RGBA 的叠加计算。 当某个 layer 的是不透明的,也就是 opaque 为 YES 时,GPU 能够直接忽略掉其下方的图层,这就减小了不少工做量。 这也是调用 CGBitmapContextCreate 时 bitmapInfo 参数设置为忽略掉 alpha 通道的缘由。并且这里主要针对的就是解码图片成位图 */ //将CGImageRef对象画到上面生成的上下文中,且将alpha通道移除 CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); //使用上下文建立位图 CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context); //从位图建立UIImage对象,返回含有指定的方向和scale的图片 UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation]; //释放CG对象 CGContextRelease(context); CGImageRelease(imageRefWithoutAlpha); return imageWithoutAlpha; } }
下面再介绍下关于SDWebImage中的图片压缩的算法,其实简单来讲就是将图像矩阵按照规则分割成小型子矩阵进行压缩,而后插值拼接,并且这个算法也是借鉴苹果的,官方Demo连接:https://developer.apple.com/library/content/samplecode/LargeImageDownsizing/Introduction/Intro.html 关于这个算法,苹果的定义就是此代码示例演示了一种支持在有限的内存环境中显示超大图像的方法,方法是将磁盘上的大图像转换为内存中较小的图像。这在原始图像太大而没法按照要显示的要求放入内存的状况下颇有用 我目前也只能理解个大概,还有些细节方面还没想到,它是怎么进行优化的,代码都有注释。如今先简单的介绍下回用到的宏吧。
// 每一个像素占4个字节大小 共32位 static const size_t kBytesPerPixel = 4; //每一个通道由8位组成 static const size_t kBitsPerComponent = 8; /* * Defines the maximum size in MB of the decoded image when the flag `SDWebImageScaleDownLargeImages` is set * 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. 该参数用于设置内存占用的最大字节数。默认为60MB,下面给出了一些旧设备的参考数值。若是图片大小大于该值,则将图片以该数值为目标进行压缩。 */ static const CGFloat kDestImageSizeMB = 60.0f; /* * Defines the maximum size in MB of a tile used to decode image when the flag `SDWebImageScaleDownLargeImages` is set * 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. 设置压缩时对于源图像使用到的*块*的最大字节数。原图方块的大小,这个方块将会被用来分割原图,默认设置为20M。 */ static const CGFloat kSourceImageTileSizeMB = 20.0f; //1M有多少字节 static const CGFloat kBytesPerMB = 1024.0f * 1024.0f; //1M有多少像素 262144个像素 static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel; //目标总像素kDestImageSizeMB为60MB static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB; //目标图像的像素点个数 static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB; //目标重叠像素大小 static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet.
关于算法的描述,位图其实简单能够描述为是由像素组成的矩阵,因此下面其实就是把图像当作一个矩阵(或多个矩阵的组合)来进行处理的。
- (nullable UIImage *)sd_decompressedAndScaledDownImageWithImage:(nullable UIImage *)image { //一、判断图片是否支持解码 if (![[self class] shouldDecodeImage:image]) { return image; } //二、判断图片是否支持缩小也就是压缩,总像素要大于15728640才能压缩,也就是kDestTotalPixels的大小 if (![[self class] shouldScaleDownImage:image]) { return [self sd_decompressedImageWithImage:image]; } //声明压缩目标用的上下文 CGContextRef destContext; // 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]; @autoreleasepool { //3. 获取源图像位图 CGImageRef sourceImageRef = image.CGImage; //4. 源图像尺寸,存储在CGSize结构体中 CGSize sourceResolution = CGSizeZero; sourceResolution.width = CGImageGetWidth(sourceImageRef); sourceResolution.height = CGImageGetHeight(sourceImageRef); //5. 计算源图像总的像素点个数 float sourceTotalPixels = sourceResolution.width * sourceResolution.height; // Determine the scale ratio to apply to the input image // that results in an output image of the defined size. // see kDestImageSizeMB, and how it relates to destTotalPixels. //6. 获取原图像和目标图像的比例(以像素点个数为基准),这里是以60MB的像素点为标准了 60MB的总像素要除以原文件的总像素小于1的 float imageScale = kDestTotalPixels / sourceTotalPixels; //7. 使用imagescale计算目标图像的宽高,因此我目标图像的目标就是到60MB CGSize destResolution = CGSizeZero; destResolution.width = (int)(sourceResolution.width*imageScale); destResolution.height = (int)(sourceResolution.height*imageScale); //8. 进行图像绘制前的准备工做 // current color space CGColorSpaceRef colorspaceRef = [[self class] colorSpaceForImageRef:sourceImageRef]; // 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. //建立位图上下文 destContext = CGBitmapContextCreate(NULL, destResolution.width, destResolution.height, kBitsPerComponent, 0, colorspaceRef, kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast); if (destContext == NULL) { return image; } /*9. 设置图像插值的质量为高,设置图形上下文的插值质量水平CGContextSetInterpolationQuality容许上下文以各类保真度水平内插像素。 在这种状况下,kCGInterpolationHigh经过最佳结果*/ CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh); //如今定义矩形的大小,用于增量位块从输入图像到输出图像。 // Now define the size of the rectangle to be used for the // incremental blits from the input image to the output image. //因为iOS从磁盘检索图像数据的方式,咱们使用源图像宽度与源图像的宽度相等 // we use a source tile width equal to the width of the source // image due to the way that iOS retrieves image data from disk. /*iOS必须在全宽度的“波段”中从磁盘上解码图像,即便当前的图形上下文被剪切到band内的一个subrect中。所以,咱们充分利用了全部的像素数据, 这些数据是由解码操做产生的,经过将咱们的平铺大小与输入图像的宽度匹配。 */ // iOS must decode an image from disk in full width 'bands', even // if current graphics context is clipped to a subrect within that // band. Therefore we fully utilize all of the pixel data that results // from a decoding opertion by achnoring our tile size to the full // width of the input image. //10. 定义一个称为*块*的增量矩形(incremental blits,即矩形大小在每一次迭代后都不断增加/减少)用于计算从源图像到目标图像的输出。 CGRect sourceTile = CGRectZero; //源块的宽度等于源图像的宽度,宽度要保持必定 sourceTile.size.width = sourceResolution.width; // 块的高度是动态的,咱们前面指定了源tile的值,也就是kTileTotalPixels目标图像的像素点个数 根据宽度计算动态的高度 // The source tile height is dynamic. Since we specified the size // of the source tile in MB, see how many rows of pixels high it // can be given the input image width. sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width ); // *块*的起始x值老是为0 sourceTile.origin.x = 0.0f; //输出的tile与输入的tile比例相同,但图像按比例缩放。图像按比例缩放就要用到插值运算了 // The output tile is the same proportions as the input tile, but // scaled to image scale. //一样的方式初始化目标图像的块 CGRect destTile; //宽度 = 目标图像的宽度 destTile.size.width = destResolution.width; //高度 = 源图像块的高度 * 缩放比例 destTile.size.height = sourceTile.size.height * imageScale; destTile.origin.x = 0.0f; // The source seem overlap is proportionate to the destination seem overlap. // this is the amount of pixels to overlap each tile as we assemble the ouput image. //十一、计算源图像与压缩后目标图像重叠的像素大小。这里就是按照sourceResolution.height和destResolution.height进行相比 float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height); CGImageRef sourceTileImageRef; //计算组装输出图像所需的读/写操做数 // calculate the number of read/write operations required to assemble the // output image. //源图像的高度除以分割源图像的方块的高度得出源图像被分割成多少个方块并赋值给 iterations,再作取余运算取得分割的最大的整数 int iterations = (int)( sourceResolution.height / sourceTile.size.height ); //十二、若是tile height不均匀地分割图像高度,则添加另外一个迭代来解释剩余的像素。 // If tile height doesn't divide the image height evenly, add another iteration // to account for the remaining pixels. int remainder = (int)sourceResolution.height % (int)sourceTile.size.height; if(remainder) { iterations++; } // Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations. //定义一个 float 变量 sourceTitleHeightMinusOverlap 存放那个用来分割源图像,大小为 20 MB 的方块的高度。 float sourceTileHeightMinusOverlap = sourceTile.size.height; //用于切割源图像大小为 20 MB 的方块的高度加上源图像与源图像分割方块的像素重叠数 sourceTile.size.height += sourceSeemOverlap; //destTile.size.height = sourceTile.size.height * imageScale; //目标图像的分割方块的高度加上 kDestSeemOverlap(像素重叠数赋值为 2) destTile.size.height += kDestSeemOverlap; //1三、进行for 循环,y 从0开始,到小于源图像被分割的块数 for( int y = 0; y < iterations; ++y ) { @autoreleasepool { //sourceTile 和 destTile 都是宽度和高度固定的,x 值为 0,只有 y 值随着循环的 y 值在变化,sourceTile 的 y 值在递增,destTile 的 y 值在递减,只有最最后一次循环中,若是有余数那么size就会发生变化,这是由于最后一次中去取源图像的高度其实 //sourceTileHeightMinusOverlap = sourceTile.size.height; sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap; destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap); /*1四、在用到在一张图片中截取某一部分图片中,用到CGImageRef类中的CGImageCreateWithImageInRect函数 而后循环的从源图像的 sourceImageRef 根据大小为 20 MB 的分割块的不一样 CGRect 的矩形区域内获取 sourceTileImageRef,这里sourceTile的高度是根据kTileTotalPixels / sourceTile.size.width */ sourceTileImageRef = CGImageCreateWithImageInRect(sourceImageRef, sourceTile); //计算剩余的像素,所采用的方法 if( y == iterations - 1 && remainder ) { //destTile.size.height = sourceTile.size.height * imageScale; float dify = destTile.size.height; destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale; dify -= destTile.size.height; destTile.origin.y += dify; } //1五、绘制图像到图形上下文指定的destTile范围中 CGContextDrawImage( destContext, destTile, sourceTileImageRef ); CGImageRelease( sourceTileImageRef ); } } CGImageRef destImageRef = CGBitmapContextCreateImage(destContext); CGContextRelease(destContext); if (destImageRef == NULL) { return image; } //1六、输出图像 UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation]; CGImageRelease(destImageRef); if (destImage == nil) { return image; } return destImage; } }
压缩过程简单流程