SDWebImage缓存学习

常用的第三方库:

关于图片的下载第三方库,目前有三个选择:

  • SDWebImage是使用最广泛的,目前的项目中在用,而且还是直接源码方式,连CocoaPod都没用
  • YYImage据说可以替代SDWebImage,曾经看过YYModelYYCache,真的有意愿选择
  • AFNetworking,这个主要用来做网络库,图片的下载很少用。SDWebImage作者也说AFNetworking值得用,采用系统的NSURLCache实现缓存。
  • SDWebImageYYImage都用了自己的缓存,并且会把图片解码操作提前做了,可以加快UITableView的显示,但缺点是内存占用会偏高。缓存和UIImage编解码是这两个库的特色。
  • SDWebImage项目中在用,先看他的实现方式,其他的等以后有机会再学

读取内存缓存

// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
    NSData *diskData = nil;
    if ([image isGIF]) {
        diskData = [self diskImageDataBySearchingAllPathsForKey:key];
    }
    if (doneBlock) {
        doneBlock(image, diskData, SDImageCacheTypeMemory);
    }
    return nil;
}
  • UIImage *image很好理解,应该就是直接给UIImageView用的。根据后面代码阅读,这里的不是普通的UIImage,应该是考虑了乘数因子scale和解码之后的UIImage,是可以直接在屏幕上显示的。
  • 这里的NSData *diskData 是特指gif动图的数据。gif动图是不考虑解码的。
@property (strong, nonatomic, nonnull) NSCache *memCache;

- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
    return [self.memCache objectForKey:key];
}

软件缓存直接使用系统的NSCache实现,比较简单

- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
    NSString *defaultPath = [self defaultCachePathForKey:key];
    NSData *data = [NSData dataWithContentsOfFile:defaultPath];
    if (data) {
        return data;
    }

    return nil;
}

- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key {
    return [self cachePathForKey:key inPath:self.diskCachePath];
}

- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path {
    NSString *filename = [self cachedFileNameForKey:key];
    return [path stringByAppendingPathComponent:filename];
}

- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
    const char *str = key.UTF8String;
    if (str == NULL) {
        str = "";
    }
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                          r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                          r[11], r[12], r[13], r[14], r[15], [key.pathExtension isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", key.pathExtension]];

    return filename;
}

如果是Gif动图,不能直接用Image,会去磁盘上读NSData,这些数据存在磁盘某个目录下,文件名是keymd5值,而这个key是图片的url
如果是普通图片,内存中Image可以直接用,默认的话是解码过的,可以直接在屏幕上显示

读取磁盘缓存

dispatch_async(self.ioQueue, ^{
    @autoreleasepool {
        NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        UIImage *diskImage = [self diskImageForKey:key];
        if (diskImage && self.config.shouldCacheImagesInMemory) {
            NSUInteger cost = SDCacheCostForImage(diskImage);
            [self.memCache setObject:diskImage forKey:key cost:cost];
        }

        if (doneBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
            });
        }
    }
});
  • self.ioQueue是一个串行队列,放在一个@autoreleasepool中估计是为了省内存
  • 这里的diskData,从磁盘的某处(urlMD5值为文件名,带扩展名),读取NSData格式的数据。
  • self.config.shouldCacheImagesInMemory默认值是YES,就是说从磁盘上读出来数据同时也会在内存中保存一份。下次就直接在内存缓存中命中了,不需要再到缓慢的磁盘缓存中读取。
  • 大小cost是像素,要考虑乘数因子scale
NSUInteger SDCacheCostForImage(UIImage *image) {
    return image.size.height * image.size.width * image.scale * image.scale;
}```
* 这里的`diskImage`和普通的`UIImage`不同,考虑了乘数因子`scale`和解码之后,是可以直接在屏幕上显示的。

#### 读取过程
  • (nullable UIImage *)diskImageForKey:(nullable NSString *)key {
    NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
    if (data) {
    UIImage *image = [UIImage sd_imageWithData:data];
    image = [self scaledImageForKey:key image:image];
    if (self.config.shouldDecompressImages) {
    image = [UIImage decodedImageWithImage:image];
    }
    return image;
    }
    else {
    return nil;
    }
    }
* 从磁盘读取`NSData`格式的数据,和前面用的都是同一个函数
* 将`NSData`格式的数据转化为`UIImage`
* `UIImage`考虑乘数因子`scale`
* `self.config.shouldDecompressImages`默认是`YES`,`UIImage`需要经过解码,成为能直接在屏幕上显示的`UIImage`。
不经过解码的普通`UIImage`,会在主线程进行解码后再显示在屏幕上,造成`CPU`占用率过高。平时关系不大,在`UITableView`快速滑动,并且图片数据量较大的时候,会有卡顿现象发生。
解码之后的`UIImage`,可以直接在屏幕上显示,但是数据量很大,所以高清图在低端机上内存暴涨,还发烫,就是这个原因。下面的有篇文章说的就是这个,解决方案就是把`self.config.shouldDecompressImages`设置为`NO`,这样就解决内存占用过大的问题了。
另外,对于`JPEG`图,`iPhone4s`以后的机子都有硬件编解码的,所以为了减少内存占用,也可以考虑把这个开关关闭。
**是省CPU时间,还是省内存?**默认选择了省cpu时间,减少卡顿现象,提升体验

#### `NSData`转换为`UIImage`
  • (nullable UIImage *)sd_imageWithData:(nullable NSData *)data {
    if (!data) {
    return nil;
    }

    UIImage *image;
    SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:data];
    if (imageFormat == SDImageFormatGIF) {
    image = [UIImage sd_animatedGIFWithData:data];
    } else if (imageFormat == SDImageFormatWebP) {
    image = [UIImage sd_imageWithWebPData:data];
    } else {
    image = [[UIImage alloc] initWithData:data];
    UIImageOrientation orientation = [self sd_imageOrientationFromImageData:data];
    if (orientation != UIImageOrientationUp) {
    image = [UIImage imageWithCGImage:image.CGImage
    scale:image.scale
    orientation:orientation];
    }
    }
    return image;
    }

* 这是`UIImage`的一个类别`category`
* `gif`动图和`webp`格式有特殊的生成方式
* 这里还考虑了图片的方向,如果不是朝上,图片生成方式还不一样

#### 判断图片的格式

typedef NS_ENUM(NSInteger, SDImageFormat) {
SDImageFormatUndefined = -1,
SDImageFormatJPEG = 0,
SDImageFormatPNG,
SDImageFormatGIF,
SDImageFormatTIFF,
SDImageFormatWebP
};

  • (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
    if (!data) {
    return SDImageFormatUndefined;
    }

    uint8_t c;
    [data getBytes:&c length:1];
    switch (c) {
    case 0xFF:
    return SDImageFormatJPEG;
    case 0x89:
    return SDImageFormatPNG;
    case 0x47:
    return SDImageFormatGIF;
    case 0x49:
    case 0x4D:
    return SDImageFormatTIFF;
    case 0x52:
    // R as RIFF for WEBP
    if (data.length < 12) {
    return SDImageFormatUndefined;
    }

          NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
          if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
              return SDImageFormatWebP;
          }
    

    }
    return SDImageFormatUndefined;
    }

* 这是`NSData`的一个类别
* 默认是`JPEG`格式
* 第一个字节代表了图片的格式

#### 普通的`UIImage`转换为像素`UIImage`
  • (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image {
    return SDScaledImageForKey(key, image);
    }

inline UIImage *SDScaledImageForKey(NSString * _Nullable key, UIImage * _Nullable image) {
if (!image) {
return nil;
}
if ((image.images).count > 0) {
NSMutableArray<UIImage *> *scaledImages = [NSMutableArray array];

    for (UIImage *tempImage in image.images) {
        [scaledImages addObject:SDScaledImageForKey(key, tempImage)];
    }

    return [UIImage animatedImageWithImages:scaledImages duration:image.duration];
} else {
    if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
        CGFloat scale = 1;
        if (key.length >= 8) {
            NSRange range = [key rangeOfString:@"@2x."];
            if (range.location != NSNotFound) {
                scale = 2.0;
            }
            
            range = [key rangeOfString:@"@3x."];
            if (range.location != NSNotFound) {
                scale = 3.0;
            }
        }

        UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
        image = scaledImage;
    }
    return image;
}

}

* `gif`动图和普通图片的区别`(image.images).count > 0`
* 乘数因子`scale`默认为1,根据图片名称中的`@2x.` 以及` @3x.`来判断是2倍图还是3倍图
* `gif`动图有包含的`images`和持续时间`duration`两个重要特征

#### 图片解码
  • (BOOL)shouldDecodeImage:(nullable UIImage *)image {
    // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
    if (image == nil) {
    return NO;
    }

    // do not decode animated images
    if (image.images != nil) {
    return NO;
    }

    CGImageRef imageRef = image.CGImage;

    CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
    BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
    alpha == kCGImageAlphaLast ||
    alpha == kCGImageAlphaPremultipliedFirst ||
    alpha == kCGImageAlphaPremultipliedLast);
    // do not decode images with alpha
    if (anyAlpha) {
    return NO;
    }

    return YES;
    }

* `gif`动图不用解码`image.images != nil`和`(image.images).count > 0`是一个意思
* 有透明度信息的`Alpha`的图片不用解码

static const size_t kBytesPerPixel = 4;
static const size_t kBitsPerComponent = 8;

  • (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image {
    if (![UIImage 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];
    @autoreleasepool{

      CGImageRef imageRef = image.CGImage;
      CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef];
      
      size_t width = CGImageGetWidth(imageRef);
      size_t height = CGImageGetHeight(imageRef);
      size_t bytesPerRow = kBytesPerPixel * width;
    
      // 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,
                                                   kBitsPerComponent,
                                                   bytesPerRow,
                                                   colorspaceRef,
                                                   kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
      if (context == NULL) {
          return image;
      }
      
      // 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];
      
      CGContextRelease(context);
      CGImageRelease(imageRefWithoutAlpha);
      
      return imageWithoutAlpha;
    

    }
    }

* 这个过程是很耗内存的,所以用一个`@autoreleasepool`进行内存管理
* 把`pixel`化的`UIImage`转化为`bitmap`的`context`
* 利用这个`context`画一个`UIImage`
* 这个`UIImage`可以直接在屏幕上显示了,不需要`CPU`或者硬件(`JPEG`)解码了,加快了显示,避免了`UITableView`快速滑动过程中的“卡顿”现象

## 下载完成后存缓存
  • (void)storeImage:(nullable UIImage *)image
    imageData:(nullable NSData *)imageData
    forKey:(nullable NSString *)key
    toDisk:(BOOL)toDisk
    completion:(nullable SDWebImageNoParamsBlock)completionBlock {
    if (!image || !key) {
    if (completionBlock) {
    completionBlock();
    }
    return;
    }
    // if memory cache is enabled
    if (self.config.shouldCacheImagesInMemory) {
    NSUInteger cost = SDCacheCostForImage(image);
    [self.memCache setObject:image forKey:key cost:cost];
    }

    if (toDisk) {
    dispatch_async(self.ioQueue, ^{
    NSData *data = imageData;

          if (!data && image) {
              SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data];
              data = [image sd_imageDataAsFormat:imageFormatFromData];
          }
          
          [self storeImageDataToDisk:data forKey:key];
          if (completionBlock) {
              dispatch_async(dispatch_get_main_queue(), ^{
                  completionBlock();
              });
          }
      });
    

    } else {
    if (completionBlock) {
    completionBlock();
    }
    }
    }

* 先在内存中保存一份
* 是否保存到磁盘由参数`toDisk` 控制,比如设置了`SDWebImageCacheMemoryOnly`的话,就不保存到磁盘了
* 保存到磁盘过程在一个串行队列`self.ioQueue`中执行,由`dispatch_async`开辟一个子线程来完成。
* `image`和`key`(`url`)参数是必须的,不然不会保存。不管是`gif`动图或者普通的`png、jpeg`图,`image`参数都是有的
* `NSData *`格式的`imageData`参数是可以为空的,如果不为空,那么就是`gif`动图的数据,直接存磁盘了。
如果为空,那么就把`image`参数转换为`NSData *`之后存到磁盘
* 下面这段代码写的比较差;也可能是个`bug`,不理解为什么会这些写

if (!data && image) {
SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data];
data = [image sd_imageDataAsFormat:imageFormatFromData];
}```
data只有nil的情况,才会进入,所以sd_imageFormatForImageData的入参是确定的nil,没有必要给dataimageFormatFromData是确定的SDImageFormatUndefined
另外image一进入函数的时候就查过,到这里肯定不是nil,没有必要再查一下

- (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat {
    NSData *imageData = nil;
    if (self) {
        int alphaInfo = CGImageGetAlphaInfo(self.CGImage);
        BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                          alphaInfo == kCGImageAlphaNoneSkipFirst ||
                          alphaInfo == kCGImageAlphaNoneSkipLast);
        
        BOOL usePNG = hasAlpha;
        
        // the imageFormat param has priority here. But if the format is undefined, we relly on the alpha channel
        if (imageFormat != SDImageFormatUndefined) {
            usePNG = (imageFormat == SDImageFormatPNG);
        }
        
        if (usePNG) {
            imageData = UIImagePNGRepresentation(self);
        } else {
            imageData = UIImageJPEGRepresentation(self, (CGFloat)1.0);
        }
    }
    return imageData;
}
  • 这是UIImage的一个类别category
  • 存入磁盘的NSData *格式由PNG或者JPEG转换而来;
  • 默认是JPEG类型
  • 如果是.png图片,或者有Alpha信息,那么是PNG格式
  • 由于调用者传入的参数都是SDImageFormatUndefined,所以就简化为有Alpha信息就是PNG格式数据,其他都是JPEG格式数据。
  • 磁盘上的数据是没有解码的图片数据,体积比较小;内存中的是解码过的图片数据,体积比较大。

下载后解码

#pragma mark NSURLSessionTaskDelegate

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    @synchronized(self) {
        self.dataTask = nil;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
            if (!error) {
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
            }
        });
    }
    
    if (error) {
        [self callCompletionBlocksWithError:error];
    } else {
        if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
            /**
             *  See #1608 and #1623 - apparently, there is a race condition on `NSURLCache` that causes a crash
             *  Limited the calls to `cachedResponseForRequest:` only for cases where we should ignore the cached response
             *    and images for which responseFromCached is YES (only the ones that cannot be cached).
             *  Note: responseFromCached is set to NO inside `willCacheResponse:`. This method doesn't get called for large images or images behind authentication
             */
            if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached && [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]) {
                // hack
                [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
            } else if (self.imageData) {
                UIImage *image = [UIImage sd_imageWithData:self.imageData];
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                image = [self scaledImageForKey:key image:image];
                
                // Do not force decoding animated GIFs
                if (!image.images) {
                    if (self.shouldDecompressImages) {
                        if (self.options & SDWebImageDownloaderScaleDownLargeImages) {
                            image = [UIImage decodedAndScaledDownImageWithImage:image];
                            [self.imageData setData:UIImagePNGRepresentation(image)];
                        } else {
                            image = [UIImage decodedImageWithImage:image];
                        }
                    }
                }
                if (CGSizeEqualToSize(image.size, CGSizeZero)) {
                    [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
                } else {
                    [self callCompletionBlocksWithImage:image imageData:self.imageData error:nil finished:YES];
                }
            } else {
                [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
            }
        }
    }
    [self done];
}
  • 采用NSURLSessiondataTask进行下载,并没有用downloadTask
  • 下载的数据保存在self.imageData中,类型是NSData *
    @property (strong, nonatomic, nullable) NSMutableData *imageData;
  • 通过函数sd_imageWithDataNSData *格式的数据转换为普通的UIImage *
  • 通过函数scaledImageForKey将普通的UIImage *转换为考虑了乘数因子scale的像素UIImage *
  • 通过函数decodedImageWithImage进行解码,生成能在屏幕上直接显示的UIImage *

设置磁盘缓存最大值

// If our remaining disk cache exceeds a configured maximum size, perform a second
// size-based cleanup pass.  We delete the oldest files first.
if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
    // Target half of our maximum cache size for this cleanup pass.
    const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;

    // Sort the remaining cache files by their last modification time (oldest first).
    NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                             usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                 return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                             }];

    // Delete files until we fall below our desired cache size.
    for (NSURL *fileURL in sortedFiles) {
        if ([_fileManager removeItemAtURL:fileURL error:nil]) {
            NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;

            if (currentCacheSize < desiredCacheSize) {
                break;
            }
        }
    }
}
  • 默认情况下self.config.maxCacheSize = 0;不会调整磁盘缓存的大小
  • 默认缓存的有效时间是1周,在一周之内访问过的图片都缓存下来的。过期的文件会被删除。
    static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
  • 如果有必要,可以设置self.config.maxCacheSize为一个合理值,减少对手机磁盘的占用。每当超过这个设定值,将会删除一些缓存文件,直到总容量小于这个设定值得一半。
  • 这个控制的是磁盘缓存的文件,对于由于解码而造成的内存超标,这个参数不起作用。

清除内存

当出现内存告警时,会清缓存(内存缓存)。解码导致内存占用大,用空间换时间,使界面显示更流畅,不“卡顿”这个是有的。
不过由于内存占用过大而导致崩溃,应该不至于吧?

#pragma mark - Cache clean Ops

- (void)clearMemory {
    [self.memCache removeAllObjects];
}
// Subscribe to app events
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(clearMemory)
                                             name:UIApplicationDidReceiveMemoryWarningNotification
                                           object:nil];

关于大图的压缩解码

SDWebImageDownloaderScaleDownLargeImages以及SDWebImageScaleDownLargeImages默认是不设置的。如果设置,就会调用decodedAndScaledDownImageWithImage进行压缩解码

/*
 * 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.
 */
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.
 */
static const CGFloat kSourceImageTileSizeMB = 20.0f;

static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;
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 *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image {
    if (![UIImage shouldDecodeImage:image]) {
        return image;
    }
    
    if (![UIImage shouldScaleDownImage:image]) {
        return [UIImage decodedImageWithImage: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 {
        CGImageRef sourceImageRef = image.CGImage;
        
        CGSize sourceResolution = CGSizeZero;
        sourceResolution.width = CGImageGetWidth(sourceImageRef);
        sourceResolution.height = CGImageGetHeight(sourceImageRef);
        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.
        float imageScale = kDestTotalPixels / sourceTotalPixels;
        CGSize destResolution = CGSizeZero;
        destResolution.width = (int)(sourceResolution.width*imageScale);
        destResolution.height = (int)(sourceResolution.height*imageScale);
        
        // current color space
        CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:sourceImageRef];
        
        size_t bytesPerRow = kBytesPerPixel * destResolution.width;
        
        // Allocate enough pixel data to hold the output image.
        void* destBitmapData = malloc( bytesPerRow * destResolution.height );
        if (destBitmapData == NULL) {
            return image;
        }
        
        // 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(destBitmapData,
                                            destResolution.width,
                                            destResolution.height,
                                            kBitsPerComponent,
                                            bytesPerRow,
                                            colorspaceRef,
                                            kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
        
        if (destContext == NULL) {
            free(destBitmapData);
            return image;
        }
        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.
        // 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 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.
        CGRect sourceTile = CGRectZero;
        sourceTile.size.width = sourceResolution.width;
        // 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 );
        sourceTile.origin.x = 0.0f;
        // 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.
        float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
        CGImageRef sourceTileImageRef;
        // calculate the number of read/write operations required to assemble the
        // output image.
        int iterations = (int)( sourceResolution.height / sourceTile.size.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 sourceTileHeightMinusOverlap = sourceTile.size.height;
        sourceTile.size.height += sourceSeemOverlap;
        destTile.size.height += kDestSeemOverlap;
        for( int y = 0; y < iterations; ++y ) {
            @autoreleasepool {
                sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
                destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
                sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
                if( y == iterations - 1 && remainder ) {
                    float dify = destTile.size.height;
                    destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
                    dify -= destTile.size.height;
                    destTile.origin.y += dify;
                }
                CGContextDrawImage( destContext, destTile, sourceTileImageRef );
                CGImageRelease( sourceTileImageRef );
            }
        }
        
        CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
        CGContextRelease(destContext);
        if (destImageRef == NULL) {
            return image;
        }
        UIImage *destImage = [UIImage imageWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
        CGImageRelease(destImageRef);
        if (destImage == nil) {
            return image;
        }
        return destImage;
    }
}
  • shouldDecodeImage判断是否需要解码,空的,有alpha信息的,GIF动图都不会解码
  • shouldScaleDownImage判断是否需要压缩;如果解压后大小超过60M,那么就需要压缩,否则就不需要。
    如果不需要压缩,那么就调用decodedImageWithImage进行无压缩的解码
  • 压缩后的目标是把大小压缩到60M以内
    static const CGFloat kDestImageSizeMB = 60.0f;
    比如原先100M的位图(解码后的格式)压缩后得到的位图在60M以内
  • 压缩不是一次完成的,而是一块一块完成的,每次的大小是20M
    static const CGFloat kSourceImageTileSizeMB = 20.0f;
  • 他的方法是宽度保持不变,然后高度y一块一块往下移动
  • 往下移多少次呢?高度除一下,有余数加1
// calculate the number of read/write operations required to assemble the
// output image.
int iterations = (int)( sourceResolution.height / sourceTile.size.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++;
}
  • 又想起了网上说的解码导致内存暴涨,对于高清图,关闭解码。这个压缩解码,不正是为了解决高清图解码后占用内存太多的问题吗?
    所以除了网上说的关闭解码功能,也可以尝试一下打开这个压缩解码功能,也就是设置SDWebImageScaleDownLargeImages

参考文章

SDWebImage

iOS图片加载框架-SDWebImage解读

使用SDWebImage和YYImage下载高分辨率图,导致内存暴增的解决办法

iOS 处理图片的一些小 Tip

移动端图片格式调研

SDWebImage源码解读_之SDWebImageDecoder

CGBitmapContextCreate参数详解

推荐阅读更多精彩内容