SDWebImage的图片解码源码阅读

SDWebImage中对下载完的图片在子线程解码后才放到ImageView中显示,这避免了系统在主线程解码而导致的卡顿问题,本文主要解读图片解码时做了哪些事情及如何显示gif动态图和Webp格式图片。上一篇也值得阅读SDWebImage的源码阅读

一、图片解码的过程

在我们设置JPG/PNG图片给UIImageView后,系统在将图片渲染到屏幕上时会默认在主线程完成解码图片重采样两个操作:

解码的原因:显示到屏幕中的图像是位图图像, 而JPG或PNG图片是编码压缩后的图片格式,在显示到屏幕之前,需要解码成位图图像位图图像的大小与图片的宽高像素有关,假设一个3MB的图片,其宽高像素为2048 * 2048的图片,解码后的位图图像大小是16MB(2048 * 2048 * 4)。
图片重采样:图片大小和imageView大小不一致时,系统根据周围像素点按一定权重获得目标图像像素点的值,SDWebImage中下载的图片,即使解码缩放, 图片大小未必和imageView的大小相同,这会引发重采样,我们可以在图片显示前,将图片裁剪成和imageView的大小相同,提升性能。

如果一个页面图片比较多,为了保证流畅度需要手动在子线程完成图片解码和裁剪工作,SDWebImage就是这样做的,下面是解码工作的细节:

  • 1. 网络加载图片成功时会调用这里进行图片处理
UIImage * SDImageLoaderDecodeImageData(NSData * imageData, NSURL * imageURL, SDWebImageOptions options, SDWebImageContext * context) {
    if (!decodeFirstFrame) {
//1. 如果不是设置的只解码第一帧图片,尝试用动态图`SDAnimatedImage`,下面的代码是追踪后简写的
        image = [[SDAnimatedImage alloc] initWithData:imageData scale:scale options:coderOptions];
        [((id<SDAnimatedImage>)image) preloadAllFrames];
    }

    if (!image) {
// 3.如果不是动态图,则使用SDImageCodersManager中包含的解码类对imageData转成image
        image = [[SDImageCodersManager sharedManager] decodedImageWithData:imageData options:coderOptions];
    }
// 4.解码工作
    if (shouldDecode) {
         BOOL shouldScaleDown = options & SDWebImageScaleDownLargeImages;
         if (shouldScaleDown) {// 将原图按照固定大小分割,然后依次绘制到目标画布
             image = [SDImageCoderHelper decodedAndScaledDownImageWithImage:image limitBytes:0];
          } else {// 直接解码到位图生成新图
             image = [SDImageCoderHelper decodedImageWithImage:image];
          }
     }
}
  • 2. SDImageCodersManager类将imageData转为image的细节
- (UIImage *)decodedImageWithData:(NSData *)data options:(SDImageCoderOptions *)options {
    UIImage *image;
// 默认self.coders =[SDImageIOCoder, SDImageGIFCoder, SDImageAPNGCoder]
    NSArray<id<SDImageCoder>> *coders = self.coders;
    for (id<SDImageCoder> coder in coders.reverseObjectEnumerator) {
        if ([coder canDecodeFromData:data]) {
            image = [coder decodedImageWithData:data options:options];
            break;
        }
    }
    return image;
}

// 1.SDImageIOCoder :canDecodeFromData: PNG, JPEG, TIFF; GIF的第一帧;(ios 11 A9以上仿生芯片)的HEIC\HEIF
- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
    UIImage *image = [[UIImage alloc] initWithData:data scale:scale];
    image.sd_imageFormat = [NSData sd_imageFormatForImageData:data];
    return image;
}

// 2.SDImageGIFCoder: 仅支持GIF动态图解码 
- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
    if (decodeFirstFrame || count <= 1) {// 1.设置的只解码第一帧
          animatedImage = [[UIImage alloc] initWithData:data scale:scale];
     } else {
          NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
          for (size_t i = 0; i < count; i++) {
            // 2.一帧一帧转化为UIImage *image,并封装为SDImageFrame *frame
              CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, i, NULL);
              UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp];
              SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration];
              [frames addObject:frame];
          }
          animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
  }
}
// SDImageCoderHelper类中转化为动态图的细节
+ (UIImage *)animatedImageWithFrames:(NSArray<SDImageFrame *> *)frames {
    UIImage *animatedImage;
    NSUInteger durations[frameCount];
    for (size_t i = 0; i < frameCount; i++) {
        durations[i] = frames[i].duration * 1000;
    }
    NSUInteger const gcd = gcdArray(frameCount, durations);
    __block NSUInteger totalDuration = 0;
    NSMutableArray<UIImage *> *animatedImages = [NSMutableArray arrayWithCapacity:frameCount];
    [frames enumerateObjectsUsingBlock:^(SDImageFrame * _Nonnull frame, NSUInteger idx, BOOL * _Nonnull stop) {
        UIImage *image = frame.image;
        NSUInteger duration = frame.duration * 1000;
        totalDuration += duration;
        NSUInteger repeatCount;
        if (gcd) {
            repeatCount = duration / gcd;
        } else {
            repeatCount = 1;
        }
        for (size_t i = 0; i < repeatCount; ++i) {
            [animatedImages addObject:image];
        }
    }];
    
    animatedImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration / 1000.f];
}
// SDImageAPNGCoder: 只支持PNG图片格式的解码(内容是动态图!!): 于GIF的转成UIImage方式是一样的,这里不展示了
  • 3.普通图片解码
+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation {
    // 1. 创建位图上下文
    CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
    bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
    CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
    // 2. 转换图片的方向
    CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight));
    CGContextConcatCTM(context, transform);
    //3. 将图片绘制到上下文中,生成图片
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); 
    CGImageRef newImageRef = CGBitmapContextCreateImage(context);
    CGContextRelease(context);
    return newImageRef;
}
  • 4.超大图的缩放解码:设置了SDWebImageScaleDownLargeImages才会进入到这里
// 先过滤掉不符合解码条件,位图大小不达标的(小于60MB);再将原图按照固定大小分割,然后依次绘制到目标画布上
+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image limitBytes:(NSUInteger)bytes {
    BOOL shouldScaleDown = YES;
    //1.图片的总像素为0,不用缩放
    if (sourceTotalPixels <= 0) {
        return NO;
    }
    // 2. 缩放目标总像素小于1MB拥有到像素,不用缩放
    if (destTotalPixels <= kPixelsPerMB) {
        // Too small to scale down
        return NO;
      }
    // 3. 目标总像素比图片像素点更大,不用缩放
    float imageScale = destTotalPixels / sourceTotalPixels;
    if (imageScale > 1) {
        shouldScaleDown = NO;
    } 
    return shouldScaleDown;
}
+ (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes {
    // 1. 判断如果不需要缩放则直接按普通图片方式解码
      if (![self shouldScaleDownImage:image limitBytes:bytes]) {
          return [self decodedImageWithImage:image];
      }

    CGFloat destTotalPixels = kDestTotalPixels;// 设置的是目标总像素==》60MB拥有的像素
    CGFloat tileTotalPixels = kTileTotalPixels;// 设置的是每一小片20MB拥有的像素点
    @autoreleasepool {
       //1. 图片sourceImageRef拥有的总像素点
      CGFloat sourceTotalPixels = sourceResolution.width * sourceResolution.height;
      //2. 目标总像素与图片总像素比值的平方根作为缩放比例,根据比例算出目标图片宽高
      CGFloat imageScale = sqrt(destTotalPixels / sourceTotalPixels);
      destResolution.width = (int)(sourceResolution.width * imageScale);
      destResolution.height = (int)(sourceResolution.height * imageScale);
      //3. 颜色空间device color space
      CGColorSpaceRef colorspaceRef = [self colorSpaceGetDeviceRGB];
      //4. 创建图形上下文
      CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
      bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
      destContext = CGBitmapContextCreate(NULL,
                                            destResolution.width,
                                            destResolution.height,
                                            kBitsPerComponent,
                                            0,
                                            colorspaceRef,
                                            bitmapInfo);
      //5. 设置图像插值的质量为高,设置图形上下文的插值质量水平允许上下文以各种保真度水平内插像素。
      // 在这种情况下,kCGInterpolationHigh通过最佳结果
      CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
      //6. 计算源图片一小片的宽高
      CGRect sourceTile = CGRectZero;
      sourceTile.size.width = sourceResolution.width;
      sourceTile.size.height = (int)(tileTotalPixels / sourceTile.size.width );
      //7. 计算目标一小片的宽高
      destTile.size.width = destResolution.width;
      destTile.size.height = sourceTile.size.height * imageScale;
      // 8. 计算源图像与压缩后目标图像重叠的像素大小,kDestSeemOverlap=2.f
      float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
      // 9.根据图片总高度/每一小片高度,计算出总共行数iterations
      int iterations = (int)( sourceResolution.height / sourceTile.size.height );
      int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
      if(remainder) {
          iterations++;
      }
      // 10. 根据总行数从源图像中一小块一小块画到目标图形上下文中destContext
      for( int y = 0; y < iterations; ++y ) {
            @autoreleasepool {
                sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
                destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
                // 11. 从源图像中取出一小块
                sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
              // 12.将取出的一小块sourceTileImageRef绘制到上下文destContext的destTile小块中
                CGContextDrawImage( destContext, destTile, sourceTileImageRef );
                CGImageRelease( sourceTileImageRef );
            }
        }
    // 13.从目标上下文生成新图片并返回
      CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
      CGContextRelease(destContext);
      UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
      CGImageRelease(destImageRef);
      return destImage;
    }
}
图片解码可能会带来的问题:

在处理高分辨率大图时直接解码操作会让内存暴增, 所以在处理这个情况可以使用

//SDWebImageOptions选择SDWebImageScaleDownLargeImages,处理网络高分辨率图
[self.imageView sd_setImageWithURL:url placeholderImage:nil options:SDWebImageScaleDownLargeImages];

SDWebImage默认支持的图片解码:

/// SDImageCodersManager类中
- (instancetype)init {
    if (self = [super init]) {
        // initialize with default coders
        _imageCoders = [NSMutableArray arrayWithArray:@[
              [SDImageIOCoder sharedCoder], // PNG, JPEG, TIFF; GIF的第一帧;(ios 11 A9以上仿生芯片)的HEIC\HEIF
              [SDImageGIFCoder sharedCoder], // GIF
              [SDImageAPNGCoder sharedCoder]// APNG
          ]];
        _codersLock = dispatch_semaphore_create(1);
    }
    return self;
}
二、加载的如果是gif动态图时如何实现

加载显示动态图有两个选择:SDAnimatedImageView、另一个库FLAnimatedImage
SDAnimatedImageView:在使用SDWebImage加载GIF,当滑动加载更多的cell时, 内存暴涨。
FLAnimatedImage:是比较轻量级的库,支持可变帧间延时、内存内存表现良好、播放流畅等特点,推荐使用。
FLAnimatedImage源码剖析

三、webp格式图片怎么显示

webp是谷歌创造的一种图片格式,webp图片在无损压缩的情况下,比png要小28%左右,图片质量上跟png差不多, 所以出于性能考虑可能会选择webp图片作为网络图片。
webp格式图片的使用:在github下载YYWebImage
YYWebImage中的WebP.framework导入工程,使用时与普通图片一样,如果是动态的webp图片推荐使用YYAnimatedImageView去显示。
具体做法参考:
iOS SDWebImage加载webp图片
SDWebImage 加载显示 WebP 与性能问题
相关比较好的文章:
SDWebImage源码看图片解码
绘制像素到屏幕的过程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容