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
经过尝试,发现了一个最简单的完美解决该问题的方法
在使用SDWebImage加载较多图片造成内存警告时,定期调用
[[SDImageCache sharedImageCache] setValue: nil forKey: @"memCache"];
2.1.1.5 方案四(不推荐):修复SD库代码,不做解压,直接返回压缩的原图
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;
}
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