【IOS开发基础系列】SDWebImageDownloader专题

1 机制原理

        SDWebImage是一个很厉害的图片缓存的框架。既ASIHttp+AsyncImage之后,我一直使用AFNetworking集成的UIImageView+AFNetworking.h,但后者对于图片的缓存实际应用的是NSURLCache自带的cache机制。而NSURLCache每次都要把缓存的raw  data 再转化为UIImage,就带来了数据处理和内存方面的更多操作。具体的比较在这里。

        SDWebImage提供了如下三个category来进行缓存。

    • MKAnnotationView(WebCache)

    • UIButton(WebCache)

    • UIImageView(WebCache)

        以最为常用的UIImageView为例:

    1、UIImageView+WebCache: setImageWithURL: placeholderImage: options: 

    先显示 placeholderImage,同时由SDWebImageManager 根据 URL 来在本地查找图片。

    2、SDWebImageManager: downloadWithURL: delegate: options: userInfo:

    SDWebImageManager是将UIImageView+WebCache同SDImageCache链接起来的类,     SDImageCache:queryDiskCacheForKey:delegate:userInfo:

    用来从缓存根据CacheKey查找图片是否已经在缓存中

    3、如果内存中已经有图片缓存, SDWebImageManager会回调SDImageCacheDelegate: imageCache: didFindImage: forKey: userInfo:

    4、而 UIImageView+WebCache 则回调SDWebImageManagerDelegate:  webImageManager: didFinishWithImage: 来显示图片。

    5、如果内存中没有图片缓存,那么生成 NSInvocationOperation 添加到队列,从硬盘查找图片是否已被下载缓存。

    6、根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。

    7、如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。

    8、如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调imageCache:didNotFindImageForKey:userInfo:。

    9、共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。

    10、图片下载由 NSURLConnection 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。

    11、connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。

    12、connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。

    13、图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。

    14、在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder: didFinishDecodingImage: userInfo: 回调给 SDWebImageDownloader。

    15、imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。

    16、通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。

    17、将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。

    18、写文件到硬盘在单独 NSInvocationOperation 中完成,避免拖慢主线程。

    19、如果是在iOS上运行,SDImageCache 在初始化的时候会注册notification 到 UIApplicationDidReceiveMemoryWarningNotification以及 UIApplicationWillTerminateNotification,在内存警告的时候清理内存图片缓存,应用结束的时候清理过期图片。

    20、SDWebImagePrefetcher 可以预先下载图片,方便后续使用。


2 开发技巧

2.1 常见问题

2.1.1 下载大量图片导致内存告警

2.1.1.1 问题原因

    1、CGBitmapContextCreateImage绘制的图片会造成内存无法释放,应该换用CGDataProviderCreateWithCFData;

    2、加载大量图片时,SD会将图片进行解压(加快渲染速度,但是内存会增大差不多一倍),然后将解压后的Image数据缓存在内存中,从而导致内存暴涨;


以下代码具有内存泄露问题:

    // 原始方案

    UIGraphicsBeginImageContextWithOptions(imageSize,YES, 0);

    [image drawInRect: imageRect];

    UIImage *imgData = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();


    return imgData;


//   改进方案1

//        CGImageRef imgRef =CGImageCreateWithImageInRect(image.CGImage,CGRectMake(0,0,image.size.width,image.size.height));

//   

//       UIGraphicsBeginImageContextWithOptions(imageSize, YES, 0);

//        CGContextRef context = UIGraphicsGetCurrentContext();

//        CGContextDrawImage(context, imageRect, imgRef);

//    //   [image drawInRect: imageRect];

//        UIImage *imgData = UIGraphicsGetImageFromCurrentImageContext();

//        UIGraphicsEndImageContext();

//        CGImageRelease(imgRef);

//        UIImage *data = [self verticallyFlipImage: imgData];

//       

//        return data;


    //方案二,内存有释放,挂机

//   UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0);   

//    CGContextRef context =UIGraphicsGetCurrentContext();

//    CGRect rect = CGRectMake(0, 0,imageSize.width * [UIScreen mainScreen].scale, imageSize.height * [UIScreenmainScreen].scale);

//    // draw alpha-mask

////    CGContextSetBlendMode(context,kCGBlendModeNormal);

//    CGContextDrawImage(context, rect,image.CGImage);

//    // draw tint color, preserving alpha valuesof original image

////    CGContextSetBlendMode(context,kCGBlendModeSourceIn);

//

//    CGContextFillRect(context, rect);

//   

//    //Set the original greyscale template asthe overlay of the new image

//    UIImage *imgData = [selfverticallyFlipImage:image];

//    [imgData drawInRect:imageRect];

//    UIImage *colouredImage =UIGraphicsGetImageFromCurrentImageContext();

//    UIGraphicsEndImageContext();

//    colouredImage = [selfverticallyFlipImage:colouredImage];

//    CGContextRelease(context);

//   

//    return colouredImage;


    //方案三,内存没释放

//    CGFloat targetWidth = imageSize.width *[UIScreen mainScreen].scale;

//    CGFloat targetHeight = imageSize.height *[UIScreen mainScreen].scale;

//    CGImageRef imageRef = [image CGImage]; 

//    CGBitmapInfo bitmapInfo =CGImageGetBitmapInfo(imageRef);  

//    CGColorSpaceRef colorSpaceInfo =CGImageGetColorSpace(imageRef);

//    CGContextRef bitmapContext;

//    bitmapContext = CGBitmapContextCreate(NULL,targetWidth, targetHeight,CGImageGetBitsPerComponent(imageRef),CGImageGetBytesPerRow(imageRef),colorSpaceInfo, bitmapInfo);

//    CGContextDrawImage(bitmapContext,CGRectMake(0, 0, targetWidth, targetHeight), imageRef);

//    CGImageRef imgref =CGBitmapContextCreateImage(bitmapContext);

//    UIImage* newImage = [UIImage imageWithCGImage: imgref];

//    CGColorSpaceRelease(colorSpaceInfo);

//    CGContextRelease(bitmapContext);

//    CGImageRelease(imgref);

//   

//   return newImage;


2.1.1.2 方案一:修改源代码,入缓存前做数据压缩

http://my.oschina.net/u/1244672/blog/510379

        SDWebImage有一个SDWebImageDownloaderOperation类来执行下载操作的。里面有个下载完成的方法:

- (void) connectionDidFinishLoading: (NSURLConnection*)aConnection {

    SDWebImageDownloaderCompletedBlockcompletionBlock = self.completedBlock;

    @synchronized(self) {

        CFRunLoopStop(CFRunLoopGetCurrent());

        self.thread = nil;

        self.connection= nil;

        [[NSNotificationCenter defaultCenter] postNotificationName: SDWebImageDownloadStopNotification object: nil];

    }


    if(![[NSURLCache sharedURLCache] cachedResponseForRequest: _request]) {

        responseFromCached= NO;

    }


    if(completionBlock)

    {

        if(self.options & SDWebImageDownloaderIgnoreCachedResponse &&responseFromCached) {

            completionBlock(nil, nil, nil, YES);

        }

        else {

            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) {

                image =[UIImage decodedImageWithImage: image];

            }

            if(CGSizeEqualToSize(image.size, CGSizeZero)) {

                completionBlock(nil, nil, [NSError errorWithDomain: @"SDWebImageErrorDomain" code: 0 userInfo: @{NSLocalizedDescriptionKey : @"Downloaded image has 0pixels"}], YES);

            }

            else {

                completionBlock(image, self.imageData, nil, YES);

            }

        }

    }

    self.completionBlock= nil;

    [self done];

}


其中,UIImage *image = [UIImage sd_imageWithData: self.imageData]; 就是将data转换成image。

再看看sd_imageWithData:这个方法:

+ (UIImage*) sd_imageWithData: (NSData *)data {

    UIImage *image;

    NSString *imageContentType = [NSData sd_contentTypeForImageData: data];

    if ([imageContentType isEqualToString: @"image/gif"]) {

        image =[UIImage sd_animatedGIFWithData: data];

    }

#ifdef SD_WEBP

    else if([imageContentType isEqualToString: @"image/webp"])

    {

        image =[UIImage sd_imageWithWebPData: data];

    }

#endif

    else {

        image = [[UIImage alloc] initWithData: data];

        UIImageOrientationorientation = [self sd_imageOrientationFromImageData: data];

        if(orientation != UIImageOrientationUp) {

            image =[UIImage imageWithCGImage: image.CGImage scale: image.scale orientation: orientation];

        }

    }


    return image;

}

        这个方法在UIImage+MultiFormat里面,是UIImage的一个类别处理。这句话很重要image =[[UIImage alloc] initWithData:data]; SDWebImage把下载下来的data直接转成image,然后没做等比缩放直接存起来使用。所以,我们只需要在这边做处理即可:

        UIImage+MultiFormat添加一个方法:

+ (UIImage *) compressImageWith: (UIImage *)image

{

    float imageWidth = image.size.width;

    float imageHeight = image.size.height;

    float width =640;

    float height =image.size.height / (image.size.width/width);

    float widthScale = imageWidth / width;

    float heightScale = imageHeight / height;


    // 创建一个bitmap的context并把它设置成为当前正在使用的context

    UIGraphicsBeginImageContext(CGSizeMake(width, height));


    if (widthScale> heightScale) {

        [image drawInRect: CGRectMake(0, 0, imageWidth / heightScale , height)];

    }

    else {

        [image drawInRect: CGRectMake(0, 0, width , imageHeight / widthScale)];

    }


    // 从当前context中创建一个改变大小后的图片

    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();

    // 使当前的context出堆栈

    UIGraphicsEndImageContext();


    return newImage;

}


        然后在image =[[UIImage alloc] initWithData: data];下面调用以下:

    if(data.length/1024 > 1024) {

        image = [self compressImageWith: image];

    }


        当data大于1M的时候做压缩处理。革命尚未成功,还需要一步处理。在SDWebImageDownloaderOperation的connectionDidFinishLoading方法里面的:

        UIImage *image= [UIImage sd_imageWithData: self.imageData];


//将等比压缩过的image在赋在转成data赋给self.imageData

NSData *data = UIImageJPEGRepresentation(image, 1);

self.imageData = [NSMutableData dataWithData: data];


2.1.1.3 方案二:设置全局缓存大小

http://www.myexception.cn/swift/2033029.html

    1、首先在appdelegate方法didFinishLaunchingWithOptions

SDImageCache.sharedImageCache().maxCacheSize=1024*1024*8设置一下最大的缓存大小。

    2、在appdelegate applicationDidReceiveMemoryWarning里加入

SDImageCache.sharedImageCache().clearMemory()

SDWebImageManager.sharedManager().cancelAll()


2.1.1.4 方案三:定时清理内存缓存

http://www.bubuko.com/infodetail-956863.html

        经过尝试,发现了一个最简单的完美解决该问题的方法

【IOS开发基础系列】SDWebImageDownloader专题_第1张图片

        在使用SDWebImage加载较多图片造成内存警告时,定期调用

 [[SDImageCache sharedImageCache] setValue: nil forKey: @"memCache"];


2.1.1.5 方案四(不推荐):修复SD库代码,不做解压,直接返回压缩的原图

【IOS开发基础系列】SDWebImageDownloader专题_第2张图片

2.1.1.6 方案五(推荐):使用CGDataProviderRef进行图形解压重绘

iOS开发中界面展示大图片时UIImage的性能有关问题

http://www.myexception.cn/operating-system/578931.html


#import "SDWebImageDecoder.h"


@implementation UIImage (ForceDecode)


+ (UIImage*) decodedImageWithImage: (UIImage*)image {

    if (image.images) {

       // Do not decode animated images

       return image;

    }


    //仅仅作为临时应付方案

    //    return image;


    UIImage *decompressedImage;


    @autoreleasepool{

        //核心代码,可以解决内存未释放问题

        NSData *data = UIImageJPEGRepresentation(image, 1);

        CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);

        CGImageRefimageRef = CGImageCreateWithPNGDataProvider(dataProvider, NULL, NO,

kCGRenderingIntentDefault);


    //    CGImageRef imageRef = image.CGImage;

    CGSizeimageSize = CGSizeMake(CGImageGetWidth(imageRef),CGImageGetHeight(imageRef));

    CGRect imageRect = (CGRect){.origin = CGPointZero, .size=imageSize};

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

    CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);


    intinfoMask = (bitmapInfo & kCGBitmapAlphaInfoMask);

    BOOL anyNonAlpha = (infoMask == kCGImageAlphaNone || infoMask ==kCGImageAlphaNoneSkipFirst || infoMask ==kCGImageAlphaNoneSkipLast);


    // CGBitmapContextCreate doesn't support kCGImageAlphaNone with RGB.

    // https://developer.apple.com/library/mac/#qa/qa1037/_index.html

    if(infoMask == kCGImageAlphaNone&& CGColorSpaceGetNumberOfComponents(colorSpace)

> 1) {

        // Unset the old alpha info.

        bitmapInfo &= ~kCGBitmapAlphaInfoMask;


        // Set noneSkipFirst.

        bitmapInfo |= kCGImageAlphaNoneSkipFirst;

    }

     // Some PNGs tell us they have alpha but only 3 components. Odd.

    else if(!anyNonAlpha && CGColorSpaceGetNumberOfComponents(colorSpace)

== 3) {

        // Unset the old alpha info.

        bitmapInfo &= ~kCGBitmapAlphaInfoMask;

        bitmapInfo |= kCGImageAlphaPremultipliedFirst;

    }


    // It calculates the bytes-per-row based on the bitsPerComponent and width arguments.

    CGContextRef context = CGBitmapContextCreate(NULL, imageSize.width, imageSize.height,  CGImageGetBitsPerComponent(imageRef), 0, colorSpace, bitmapInfo);

    CGColorSpaceRelease(colorSpace);


    // If failed, return undecompressed image

    if(!context) 

        return image;


    CGContextDrawImage(context,imageRect, imageRef);

    CGImageRef decompressedImageRef = CGBitmapContextCreateImage(context);

    CGContextRelease(context);

    decompressedImage = [UIImage imageWithCGImage: decompressedImageRef scale: image.scale orientation: image.imageOrientation];

    CGImageRelease(decompressedImageRef);

}


//    CVPixelBufferRef pixelBuffer;

//   CreateCGImageFromCVPixelBuffer(pixelBuffer,&decompressedImageRef);

//    CGImage *cgImage =CGBitmapContextCreateImage(context);

//    CFDataRef dataRef =CGDataProviderCopyData(CGImageGetDataProvider(cgImage));

//    CGImageRelease(cgImage);

//    image->imageRef = dataRef;

//    image->image =CFDataGetBytePtr(dataRef);


    return decompressedImage;

}

【IOS开发基础系列】SDWebImageDownloader专题_第3张图片

3 参考链接

(GOOD)iOS开发中界面展示大图片时UIImage的性能有关问题

http://www.myexception.cn/operating-system/578931.html


(Good)iPhone - UIImage Leak, CGBitmapContextCreateImage Leak

http://stackoverflow.com/questions/1427478/iphone-uiimage-leak-cgbitmapcontextcreateimage-leak


Another iPhone - CGBitmapContextCreateImage Leak

http://stackoverflow.com/questions/1434714/another-iphone-cgbitmapcontextcreateimage-leak


UIGraphicsBeginImageContext vs CGBitmapContextCreate

http://stackoverflow.com/questions/4683448/uigraphicsbeginimagecontext-vs-cgbitmapcontextcreate


iPhone - CGBitmapContextCreateImage Leak, Anyone else withthis problem?

http://stackoverflow.com/questions/1431566/iphone-cgbitmapcontextcreateimage-leak-anyone-else-with-this-problem


Build and Analyze false positive on leak detection?

http://stackoverflow.com/questions/8438249/build-and-analyze-false-positive-on-leak-detection


iPhone - Multiple CGBitmapContextCreateImage Calls -ObjectAlloc climbing

http://stackoverflow.com/questions/1436465/iphone-multiple-cgbitmapcontextcreateimage-calls-objectalloc-climbing


(Good)ios开发图片处理,内存泄露

http://www.oschina.net/question/736524_69802


主题: CGBitmapContextCreateImage(bitmap)内存泄露问题处理

http://www.cocoachina.com/bbs/read.php?tid=31835


iOS异步图片加载优化与常用开源库分析

http://luoyibu.com/2015/05/12/iOS异步图片加载优化与常用开源库分析/


主题:图片处理开源函数ImageProcessing  CGDataProviderCreateWithData Bug修复

http://www.cocoachina.com/bbs/read.php?tid=116149


CGDataProviderCreateWithData对内存数据的释放

http://www.taofengping.com/2012/11/04/cgdataprovidercreatewithdata_memory_release/#.VmpqgoSitZE


IOS7.x下UIGraphicsGetImageFromCurrentImageContext引发内存暴涨,导致应用被结束掉

http://blog.163.com/l1_jun/blog/static/1438638820155593641529/


在iOS中与CGContextRef的内存泄漏

http://www.itstrike.cn/Question/55b86ce7-dfba-4548-a103-22dc5317420a.html


Quartz 2D (ProgrammingWithQuartz) note

http://renxiangzyq.iteye.com/blog/1188025


使用AFNetworking,SDWebimage和OHHTTPStubs

http://blog.shiqichan.com/using-afnetworking-sdwebimage-and-ohhttpstubs/


SDWebImage缓存图片的机制(转)

http://blog.csdn.net/zhun36/article/details/8900327


近来一个swift项目用uicollectionview 用sdwebimage 加载图片,发生内存猛增,直接闪退的情况,简单说一下解决方案

http://www.myexception.cn/swift/2033029.html


关于SDWebImage加载高清图片导致app崩溃的问题

http://www.bubuko.com/infodetail-956863.html


SDWebImage加载大图导致的内存警告问题

http://blog.csdn.net/richer1997/article/details/43481959


解决MWPhotoBrowser中的SDWebImage加载大图导致的内存警告问题

http://my.oschina.net/u/1244672/blog/510379


使用SDWebImage加载大量图片后造成内存泄露的解决办法

http://www.bubuko.com/infodetail-985746.html

你可能感兴趣的:(【IOS开发基础系列】SDWebImageDownloader专题)