SDWebImage緩存學(xué)習(xí)

常用的第三方庫:

關(guān)于圖片的下載第三方庫赋除,目前有三個選擇:

  • SDWebImage是使用最廣泛的荆针,目前的項目中在用航背,而且還是直接源碼方式,連CocoaPod都沒用
  • YYImage據(jù)說可以替代SDWebImage今魔,曾經(jīng)看過YYModelYYCache涡贱,真的有意愿選擇
  • AFNetworking,這個主要用來做網(wǎng)絡(luò)庫激挪,圖片的下載很少用垄分。SDWebImage作者也說AFNetworking值得用,采用系統(tǒng)的NSURLCache實現(xiàn)緩存豺瘤。
  • SDWebImageYYImage都用了自己的緩存蚕泽,并且會把圖片解碼操作提前做了须妻,可以加快UITableView的顯示,但缺點是內(nèi)存占用會偏高司倚。緩存和UIImage編解碼是這兩個庫的特色皿伺。
  • SDWebImage項目中在用鸵鸥,先看他的實現(xiàn)方式妒穴,其他的等以后有機(jī)會再學(xué)

讀取內(nèi)存緩存

// 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很好理解杰赛,應(yīng)該就是直接給UIImageView用的乏屯。根據(jù)后面代碼閱讀辰晕,這里的不是普通的UIImage,應(yīng)該是考慮了乘數(shù)因子scale和解碼之后的UIImage扎唾,是可以直接在屏幕上顯示的胸遇。
  • 這里的NSData *diskData 是特指gif動圖的數(shù)據(jù)。gif動圖是不考慮解碼的逗威。
@property (strong, nonatomic, nonnull) NSCache *memCache;

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

軟件緩存直接使用系統(tǒng)的NSCache實現(xiàn),比較簡單

- (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,這些數(shù)據(jù)存在磁盤某個目錄下登馒,文件名是keymd5值,而這個key是圖片的url
如果是普通圖片济欢,內(nèi)存中Image可以直接用,默認(rèn)的話是解碼過的半等,可以直接在屏幕上顯示

讀取磁盤緩存

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中估計是為了省內(nèi)存
  • 這里的diskData切距,從磁盤的某處(urlMD5值為文件名,帶擴(kuò)展名)葡幸,讀取NSData格式的數(shù)據(jù)床蜘。
  • self.config.shouldCacheImagesInMemory默認(rèn)值是YES肤粱,就是說從磁盤上讀出來數(shù)據(jù)同時也會在內(nèi)存中保存一份。下次就直接在內(nèi)存緩存中命中了蛮穿,不需要再到緩慢的磁盤緩存中讀取庶骄。
  • 大小cost是像素,要考慮乘數(shù)因子scale
NSUInteger SDCacheCostForImage(UIImage *image) {
    return image.size.height * image.size.width * image.scale * image.scale;
}```
* 這里的`diskImage`和普通的`UIImage`不同践磅,考慮了乘數(shù)因子`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`格式的數(shù)據(jù)府适,和前面用的都是同一個函數(shù)
* 將`NSData`格式的數(shù)據(jù)轉(zhuǎn)化為`UIImage`
* `UIImage`考慮乘數(shù)因子`scale`
* `self.config.shouldDecompressImages`默認(rèn)是`YES`羔飞,`UIImage`需要經(jīng)過解碼卡儒,成為能直接在屏幕上顯示的`UIImage`缀磕。
不經(jīng)過解碼的普通`UIImage`,會在主線程進(jìn)行解碼后再顯示在屏幕上滞项,造成`CPU`占用率過高。平時關(guān)系不大仁热,在`UITableView`快速滑動,并且圖片數(shù)據(jù)量較大的時候,會有卡頓現(xiàn)象發(fā)生。
解碼之后的`UIImage`措伐,可以直接在屏幕上顯示,但是數(shù)據(jù)量很大泳唠,所以高清圖在低端機(jī)上內(nèi)存暴漲镶奉,還發(fā)燙亿蒸,就是這個原因。下面的有篇文章說的就是這個,解決方案就是把`self.config.shouldDecompressImages`設(shè)置為`NO`邻眷,這樣就解決內(nèi)存占用過大的問題了阿宅。
另外往湿,對于`JPEG`圖蟀俊,`iPhone4s`以后的機(jī)子都有硬件編解碼的,所以為了減少內(nèi)存占用,也可以考慮把這個開關(guān)關(guān)閉。
**是省CPU時間乍炉,還是省內(nèi)存困檩?**默認(rèn)選擇了省cpu時間喉誊,減少卡頓現(xiàn)象邀摆,提升體驗

#### `NSData`轉(zhuǎn)換為`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`的一個類別
* 默認(rèn)是`JPEG`格式
* 第一個字節(jié)代表了圖片的格式

#### 普通的`UIImage`轉(zhuǎn)換為像素`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`動圖和普通圖片的區(qū)別`(image.images).count > 0`
* 乘數(shù)因子`scale`默認(rèn)為1栋盹,根據(jù)圖片名稱中的`@2x.` 以及` @3x.`來判斷是2倍圖還是3倍圖
* `gif`動圖有包含的`images`和持續(xù)時間`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;
    

    }
    }

* 這個過程是很耗內(nèi)存的,所以用一個`@autoreleasepool`進(jìn)行內(nèi)存管理
* 把`pixel`化的`UIImage`轉(zhuǎn)化為`bitmap`的`context`
* 利用這個`context`畫一個`UIImage`
* 這個`UIImage`可以直接在屏幕上顯示了敷矫,不需要`CPU`或者硬件(`JPEG`)解碼了例获,加快了顯示,避免了`UITableView`快速滑動過程中的“卡頓”現(xiàn)象

## 下載完成后存緩存
  • (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();
    }
    }
    }

* 先在內(nèi)存中保存一份
* 是否保存到磁盤由參數(shù)`toDisk` 控制曹仗,比如設(shè)置了`SDWebImageCacheMemoryOnly`的話榨汤,就不保存到磁盤了
* 保存到磁盤過程在一個串行隊列`self.ioQueue`中執(zhí)行,由`dispatch_async`開辟一個子線程來完成怎茫。
* `image`和`key`(`url`)參數(shù)是必須的收壕,不然不會保存。不管是`gif`動圖或者普通的`png遭居、jpeg`圖啼器,`image`參數(shù)都是有的
* `NSData *`格式的`imageData`參數(shù)是可以為空的,如果不為空俱萍,那么就是`gif`動圖的數(shù)據(jù)端壳,直接存磁盤了。
如果為空枪蘑,那么就把`image`參數(shù)轉(zhuǎn)換為`NSData *`之后存到磁盤
* 下面這段代碼寫的比較差损谦;也可能是個`bug`,不理解為什么會這些寫

if (!data && image) {
SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data];
data = [image sd_imageDataAsFormat:imageFormatFromData];
}```
data只有nil的情況岳颇,才會進(jìn)入照捡,所以sd_imageFormatForImageData的入?yún)⑹谴_定的nil,沒有必要給data话侧。imageFormatFromData是確定的SDImageFormatUndefined
另外image一進(jìn)入函數(shù)的時候就查過栗精,到這里肯定不是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轉(zhuǎn)換而來悲立;
  • 默認(rèn)是JPEG類型
  • 如果是.png圖片鹿寨,或者有Alpha信息,那么是PNG格式
  • 由于調(diào)用者傳入的參數(shù)都是SDImageFormatUndefined薪夕,所以就簡化為有Alpha信息就是PNG格式數(shù)據(jù)脚草,其他都是JPEG格式數(shù)據(jù)。
  • 磁盤上的數(shù)據(jù)是沒有解碼的圖片數(shù)據(jù)原献,體積比較辛罂;內(nèi)存中的是解碼過的圖片數(shù)據(jù)姑隅,體積比較大写隶。

下載后解碼

#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進(jìn)行下載,并沒有用downloadTask
  • 下載的數(shù)據(jù)保存在self.imageData中粤策,類型是NSData *
    @property (strong, nonatomic, nullable) NSMutableData *imageData;
  • 通過函數(shù)sd_imageWithDataNSData *格式的數(shù)據(jù)轉(zhuǎn)換為普通的UIImage *
  • 通過函數(shù)scaledImageForKey將普通的UIImage *轉(zhuǎn)換為考慮了乘數(shù)因子scale的像素UIImage *
  • 通過函數(shù)decodedImageWithImage進(jìn)行解碼樟澜,生成能在屏幕上直接顯示的UIImage *

設(shè)置磁盤緩存最大值

// 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;
            }
        }
    }
}
  • 默認(rèn)情況下self.config.maxCacheSize = 0;不會調(diào)整磁盤緩存的大小
  • 默認(rèn)緩存的有效時間是1周,在一周之內(nèi)訪問過的圖片都緩存下來的叮盘。過期的文件會被刪除秩贰。
    static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
  • 如果有必要,可以設(shè)置self.config.maxCacheSize為一個合理值柔吼,減少對手機(jī)磁盤的占用毒费。每當(dāng)超過這個設(shè)定值,將會刪除一些緩存文件愈魏,直到總?cè)萘啃∮谶@個設(shè)定值得一半觅玻。
  • 這個控制的是磁盤緩存的文件,對于由于解碼而造成的內(nèi)存超標(biāo)培漏,這個參數(shù)不起作用溪厘。

清除內(nèi)存

當(dāng)出現(xiàn)內(nèi)存告警時,會清緩存(內(nèi)存緩存)牌柄。解碼導(dǎo)致內(nèi)存占用大畸悬,用空間換時間,使界面顯示更流暢珊佣,不“卡頓”這個是有的蹋宦。
不過由于內(nèi)存占用過大而導(dǎo)致崩潰,應(yīng)該不至于吧?

#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];

關(guān)于大圖的壓縮解碼

SDWebImageDownloaderScaleDownLargeImages以及SDWebImageScaleDownLargeImages默認(rèn)是不設(shè)置的咒锻。如果設(shè)置冷冗,就會調(diào)用decodedAndScaledDownImageWithImage進(jìn)行壓縮解碼

/*
 * 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,那么就需要壓縮须板,否則就不需要碰镜。
    如果不需要壓縮,那么就調(diào)用decodedImageWithImage進(jìn)行無壓縮的解碼
  • 壓縮后的目標(biāo)是把大小壓縮到60M以內(nèi)
    static const CGFloat kDestImageSizeMB = 60.0f;
    比如原先100M的位圖(解碼后的格式)壓縮后得到的位圖在60M以內(nèi)
  • 壓縮不是一次完成的习瑰,而是一塊一塊完成的,每次的大小是20M
    static const CGFloat kSourceImageTileSizeMB = 20.0f;
  • 他的方法是寬度保持不變秽荤,然后高度y一塊一塊往下移動
  • 往下移多少次呢甜奄?高度除一下儡陨,有余數(shù)加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++;
}
  • 又想起了網(wǎng)上說的解碼導(dǎo)致內(nèi)存暴漲蒜哀,對于高清圖隘膘,關(guān)閉解碼彬碱。這個壓縮解碼瓦糕,不正是為了解決高清圖解碼后占用內(nèi)存太多的問題嗎喂柒?
    所以除了網(wǎng)上說的關(guān)閉解碼功能辣卒,也可以嘗試一下打開這個壓縮解碼功能拧晕,也就是設(shè)置SDWebImageScaleDownLargeImages

參考文章

SDWebImage

iOS圖片加載框架-SDWebImage解讀

使用SDWebImage和YYImage下載高分辨率圖紊扬,導(dǎo)致內(nèi)存暴增的解決辦法

iOS 處理圖片的一些小 Tip

移動端圖片格式調(diào)研

SDWebImage源碼解讀_之SDWebImageDecoder

CGBitmapContextCreate參數(shù)詳解

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蜒茄,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子餐屎,更是在濱河造成了極大的恐慌檀葛,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腹缩,死亡現(xiàn)場離奇詭異屿聋,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)藏鹊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門润讥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人盘寡,你說我怎么就攤上這事楚殿。” “怎么了宴抚?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵勒魔,是天一觀的道長。 經(jīng)常有香客問我菇曲,道長冠绢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任常潮,我火速辦了婚禮弟胀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己孵户,他們只是感情好萧朝,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著夏哭,像睡著了一般检柬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上竖配,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天何址,我揣著相機(jī)與錄音,去河邊找鬼进胯。 笑死用爪,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的胁镐。 我是一名探鬼主播偎血,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼盯漂!你這毒婦竟也來了颇玷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤宠能,失蹤者是張志新(化名)和其女友劉穎亚隙,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體违崇,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡阿弃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了羞延。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渣淳。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖伴箩,靈堂內(nèi)的尸體忽然破棺而出入愧,到底是詐尸還是另有隱情,我是刑警寧澤嗤谚,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布棺蛛,位于F島的核電站,受9級特大地震影響巩步,放射性物質(zhì)發(fā)生泄漏旁赊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一椅野、第九天 我趴在偏房一處隱蔽的房頂上張望终畅。 院中可真熱鬧籍胯,春花似錦、人聲如沸离福。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽妖爷。三九已至蝶涩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間絮识,已是汗流浹背子寓。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留笋除,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓炸裆,卻偏偏與公主長得像垃它,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子烹看,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容