在应用SDWebImage过程中,遇到了一些技术问题和细节问题,现在总结一下,并
进行了相关的技术扩展,SDWebImage确实是个值得研究的框架
场景一:当我们在一个页面中加载特别多的九宫格图片,那么当我们滑动页面肯定会造成内存的暴涨,如何处理那?
首先对内存进行监听
//监听内存警告
[[NSNotificationCenter defaultCenter]addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
NSLog(@"内存暴涨");
// 1.取消正在下载的操作
[[SDWebImageManager sharedManager] cancelAll];
// 2.清除内存缓存
[[SDWebImageManager sharedManager].imageCache clearWithCacheType:SDImageCacheTypeAll completion:nil];
}];
加载图片的处理
// SDWebImageLowPriority 当UIScrollView滑动减速时开始加载图片 SDWebImageAvoidDecodeImage由于过多的内存消耗,这个标志可以防止解码图像。
[self.Pic sd_setImageWithURL:self.url placeholderImage:[UIImage imageNamed:@"logo"] options:SDWebImageLowPriority|SDWebImageAvoidDecodeImage];
场景二:如何让下载的图片展示出网页那种从上到下显示的效果?
直接设置SDWebImageProgressiveLoad
[self.image sd_setImageWithURL:self.URL placeholderImage:nil options:SDWebImageProgressiveLoad];
场景三:设置了SDWebImageRefreshCached,缓存图片如何更新?
现在提供两种方法来解决这个问题 - 旧版本SDWebImage:
首先我们需要探讨一下实现的原理:一种是NSURLCache(NSURL缓存的get请求),一种是SD中自己定义的SDImageCache进行缓存。在SDWebImage中我们可以查看SDWebImageRefreshCached是如何定义的 - 如果设置了该类型,缓存策略依据NSURLCache而不是SDImageCache,所以可以通过NSURLCache进行缓存了;
但是图片的更新还需要的服务器的配合才能实现,服务器如何设置那?图片的更新与否取决于你服务器的cache-control设置,如果没有cache-control设置,那么客户端就享受不了自动更新的功能。首先了解一下cache-control,
终端中输入命令: curl [url] --head
发现有Cache-Control,说明是可以的。这其实就是请求照片的过程中,返回来的header信息,这其中还包括一个名为Last-Modified、数据是时间戳的键值对。
首先为查看HTTP协议相关的资料,发现request header中有一个名为if-Modified-Since的key,value就是服务器返回的服务器最后被修改的时间;第一次请求过程中由于并没有携带该request header所以if-Modified-Since为空,第一次请求成功之后,将返回的Last-Modified值做为if-Modified-Since的值传回给服务器。这样后台就会对if-Modified-Since和Last-Modified进行比较,如果客户端图片已经过期,那么返回状态码200、Last-modified和图片内容,客户端重新将Last-modified存储到if-Modified-Since;如果客户端返回的是304 not Modified、则不会返回last-Modified、图片内容,说明图片没有更新,直接拿缓存中数据就行。
回到SDWebImage上,通过查看老的SDWebImageDownloader版本代码发现,它开放了一个headersFilter的block,我们可以在这个block中追加额外的header,所以我们可以在例如AppDelegate didFinishLaunching的地方追加如下代码:
SDWebImageDownloader *imgDownloader = SDWebImageManager.sharedManager.imageDownloader;
imgDownloader.headersFilter = ^NSDictionary *(NSURL *url, NSDictionary *headers) {
NSFileManager *fm = [[NSFileManager alloc] init];
NSString *imgKey = [SDWebImageManager.sharedManager cacheKeyForURL:url];
NSString *imgPath = [SDWebImageManager.sharedManager.imageCache defaultCachePathForKey:imgKey];
NSDictionary *fileAttr = [fm attributesOfItemAtPath:imgPath error:nil];
NSMutableDictionary *mutableHeaders = [headers mutableCopy];
NSDate *lastModifiedDate = nil;
if (fileAttr.count > 0) {
if (fileAttr.count > 0) {
lastModifiedDate = (NSDate *)fileAttr[NSFileModificationDate];
}
}
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
formatter.dateFormat = @"EEE, dd MMM yyyy HH:mm:ss z";
NSString *lastModifiedStr = [formatter stringFromDate:lastModifiedDate];
lastModifiedStr = lastModifiedStr.length > 0 ? lastModifiedStr : @"";
[mutableHeaders setValue:lastModifiedStr forKey:@"If-Modified-Since"];
return mutableHeaders;
复制代码
};
SDWebImage
然后加载图片的地方之前怎么写就怎么写,但是option中一定要加上SDWebImageRefreshCached
另外一种方法:
在SDWebImageManager.m大约167行的地方加上
// remove SDWebImageDownloaderUseNSURLCache flag downloaderOptions &= ~SDWebImageDownloaderUseNSURLCache;
变成了
if (cachedImage && options & SDWebImageRefreshCached) {
// force progressive off if image already cached but forced refreshing
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// remove SDWebImageDownloaderUseNSURLCache flag
downloaderOptions &= ~SDWebImageDownloaderUseNSURLCache;
// ignore image read from NSURLCache if image if cached but force refreshing
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
注意:最新版本中已经解决了这个问题
在之前的版本中,如果服务端更新了图片,虽然设置了SDWebImageRefreshCached,还是拿到的老图片,在最新版5.1.0中已经解决了这个问题 - 应用的SDImageCache,通过每次图片的重新网络请求,和当前的缓存数据做比较,如果不同那么就将新请求到的image通过block返回。
//判断是否更新了,包括SDWebImageDownloaderIgnoreCachedResponse/本地缓存对比
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
//如果和本地缓存相同,那么返回SDWebImageErrorCacheNotModified
self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:nil];
// call completion block with not modified error
[self callCompletionBlocksWithError:self.responseError];
[self done];
//如果没有更新,那么在子线程进图片处理
} else {
// decode the image in coder queue
dispatch_async(self.coderQueue, ^{
@autoreleasepool {
另外也可以应用Cache-control,但是最新版的并没有暴露headersFilter,而是暴露了
/**
* Set a value for a HTTP header to be appended to each download HTTP request.
*
* @param value The value for the header field. Use `nil` value to remove the header field.
* @param field The name of the header field to set.
*/
- (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field;
[[SDWebImageDownloader sharedDownloader] setValue:@"" forHTTPHeaderField:@""];
通过这个方法,我们可以将if-Modified-Since 传入SDWebImageDownloader即可,同样还是需要在option中传入SDWebImageRefreshCached
场景四:图片的解压缩问题?
设置SDWebImageAvoidDecodeImage,这个option到底是如何实现在子线程解压缩图片的那?
图片的加载工作流
将一张图片从磁盘加载到内存然后渲染到屏幕上,这个过程的消耗其实非常的大,会明显降低界面的帧速率,当滚动的时候会加剧这一情况,因为内容变化的太快,需要更快的处理速度才能保持在60FPS的帧速率。
首先考虑一下加载的工作流程:
- [UIImage imageWithContentsOfFile:]使用Image I/O创建CGImageRef内存映射数据。此时,图像尚未解码。
- 返回的数据被返回给UIImageView。
- 隐式CATransaction捕获这些层树修改。
- 在主运行循环的下一次迭代中,Core Animation提交隐式事物,这可能涉及创建已设置为层内容的任何图像的副本。根据图像,复制它涉及一下部分或全部步骤:
- 缓冲区被分配用于管理文件和解压缩操作
- 文件数据从磁盘读入内存
- 压缩的图像数据被解压缩成其未压缩的位图形式,这通常是CPU密集型操作
- 然后Core Animation使用未压缩的位图数据来渲染涂层
扩展:Core Animation不仅能用来做动画,实际上是一个叫做Layer kit这么一个不怎么和动画相关的名字演变来的。Core Animation其实是一个复合引擎,它的指责是尽快的组合屏幕上不同的可视内容,这个内容是被分解成独立的涂层,存储在一个叫图层树的体系之中,这个树形成了UIKit以及在iOS程序中你能在屏幕上看到的一切的基础。
时钟信号:垂直同步信号V-Sync/水平同步信号H-Sync,有这两个信号来按照信号时间,定时进行界面的相应展示
CPU:计算视图frame,图片的解压缩
GPU:纹理绘制,顶点变换,像素点的填充,渲染
当图片过大那么CPU解压就会非常耗时,那么在当前的水平同步信号到来到结束这一段时间内,如果没有解压或者渲染完成,那么到下一个H-Sync信号到来时就会出现拖尾现象 - 卡顿。
位图
如果不进行解压缩,直接渲染是不行的,必须要解压成位图,那么什么是位图那?
UIImage *image = [UIImage imageNamed:@"logo.png"];
CFDataRef mapData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
通过CGDataProviderCopyData获取到的mapData就是位图,可以尝试打印,
发现位图其实就是一个像素数组,有一个获取图片解压后位图大小的公式
图片像素宽 图片像素高4 = 位图大小
事实上,不管是 JPEG 还是 PNG 图片,都是一种压缩的位图图形格式。只不过 PNG 图片是无损压缩,并且支持 alpha 通道,而 JPEG 图片则是有损压缩,可以指定 0-100% 的压缩比。值得一提的是,在苹果的 SDK 中专门提供了两个函数用来生成 PNG 和 JPEG 图片:
// return image as PNG. May return nil if image has no CGImageRef or invalid bitmap format
UIKIT_EXTERN NSData * __nullable UIImagePNGRepresentation(UIImage * __nonnull image);
// return image as JPEG. May return nil if image has no CGImageRef or invalid bitmap format. compression is 0(most)..1(least)
UIKIT_EXTERN NSData * __nullable UIImageJPEGRepresentation(UIImage * __nonnull image, CGFloat compressionQuality);
所以其实我们平时的PNG/JPEG,都是压缩之后的图片,需要对图片进行解压获取图片的位图进行渲染。
解压缩API
默认情况下SDWebImage获取到图片的压缩文件之后,需要用户在UIImageView赋值的同时进行解压缩,但是在SDWebImage中如果设置了SDWebImageAvoidDecodeImage,根本原理是在子线程解压成位图,并进行绘制。用到的主要API就是CGBitmapContextCreate:,
CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
这个函数就是绘制一个位图上下文。
data :如果不为 NULL ,那么它应该指向一块大小至少为 bytesPerRow * height 字节的内存;如果 为 NULL ,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL 即可;
width 和height :位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可;
bitsPerComponent :像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可;
bytesPerRow :位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节。当我们指定 0/NULL 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化
space :就是我们前面提到的颜色空间,一般使用 RGB 即可;
bitmapInfo :位图的布局信息,alpha/颜色分量是否是浮点数/像素格式的字节顺序。如果有alpha那么用kCGImageAlphaPremultipliedFirst,否则用kCGImageAlphaNoneSkipFirst。像素格式(大端小端/16或者32未)使用kCGBitmapByteOrder32Host(关于布局信息的更多信息)
查看SDWebImage中的解压
首先获得图片是否有alpha
//判断是否有alpha
+ (BOOL)CGImageContainsAlpha:(CGImageRef)cgImage {
if (!cgImage) {
return NO;
}
//获取图片的alpha信息
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage);
//kCGImageAlphaNone没有alpha
//kCGImageAlphaNoneSkipFirst在RGB透明通道下,alpha没有在最高有效位
//kCGImageAlphaNoneSkipLast在RGB透明通道下,alpha没有在最低有效位
//这三者都得包括
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
return hasAlpha;
}
然后根据Bitmap构造上下文函数生成bitmap上下文,并对图片进行transform,获取图片上下文
+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation {
if (!cgImage) {
return NULL;
}
//获取图片的像素宽高
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
if (width == 0 || height == 0) return NULL;
size_t newWidth;
size_t newHeight;
//查看当前图片的展示方式是否正确,对width/height进行调整
switch (orientation) {
case kCGImagePropertyOrientationLeft:
case kCGImagePropertyOrientationLeftMirrored:
case kCGImagePropertyOrientationRight:
case kCGImagePropertyOrientationRightMirrored: {
//kCGImagePropertyOrientationRightMirrored这种情况应该交换宽高
newWidth = height;
newHeight = width;
}
break;
default: {
//否则不需要处理
newWidth = width;
newHeight = height;
}
break;
}
//是否有alpha通道
BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
//像素格式中的字节顺序是系统提供的32位主机字节顺序
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
//将像素格式中用位域技术添加alpha信息
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
//获得位图的上下文
//默认的颜色空间是CGColorSpaceCreateDeviceRGB()
//bytesPerRow 每一行的位图大小设置为0,系统进行自动计算并且进行优化
//每一个像素的颜色分量bit数是8
CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
if (!context) {
return NULL;
}
//图片进行反转,保证展示出来的是没有transform的图片
CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight));
CGContextConcatCTM(context, transform);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); // The rect is bounding box of CGImage, don't swap width & height
//获得当前上下文中的位图对应的图片
CGImageRef newImageRef = CGBitmapContextCreateImage(context);
CGContextRelease(context);
return newImageRef;
}
这就是整个的图片在自线程利用CGBitmapContextCreate进行解压缩的过程
场景五:SD中核心方法中context使用问题?
/**
* 通过URL加载图片,如果cache中存在就从cache中获取,否则开始下载
*
* @param url 传入的image的url
* @param options 获取图片的方式
* @param context 获取
* @param progressBlock 获得图片的进度(注意是在子队列中)
* @param completedBlock 完成获取之后的回掉block
* @return 返回一个SDWebImageCombinedOperation对象,用于表示当前的图片获取任务,在这个对象中可以取消获取图片任务
*/
- (nullable SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock;
其中context中对应了很多的业务场景,我们可以自己定义
(1). SDWebImageContextImageTransformer - 处理加载出来的图片,比如翻转圆角等
(2). SDWebImageContextCacheKeyFilter - 指定图片的缓存key
(3). SDWebImageContextCacheSerializer - 转换需要缓存的图片格式
(1)、SDWebImageContextImageTransformer 其对应的是遵守SDImageTransformer协议的类,查看系统方法可以找到具体的图片处理类型:
@protocol SDImageTransformer <NSObject>
@required
/**
@return 在原始缓存中最后添加的自定义cache key
*/
@property (nonatomic, copy, readonly, nonnull) NSString *transformerKey;
/**
调用当前方法实现图片的处理
@param image 处理之后的图片
@param key 原始图片关联的cache key
@return 处理之后的图片
*/
- (nullable UIImage *)transformedImageWithImage:(nonnull UIImage *)image forKey:(nonnull NSString *)key;
@end
#pragma mark - Pipeline
/**
//可以传入一个NSArray<SDImageTransformer>数组,按顺序做转换
*/
@interface SDImagePipelineTransformer : NSObject <SDImageTransformer>
/**
*/
@property (nonatomic, copy, readonly, nonnull) NSArray<id<SDImageTransformer>> *transformers;
- (nonnull instancetype)init NS_UNAVAILABLE;
+ (nonnull instancetype)transformerWithTransformers:(nonnull NSArray<id<SDImageTransformer>> *)transformers;
@end
/**
添加圆角
*/
@interface SDImageRoundCornerTransformer: NSObject <SDImageTransformer>
@property (nonatomic, assign, readonly) CGFloat cornerRadius;
@property (nonatomic, assign, readonly) SDRectCorner corners;
@property (nonatomic, assign, readonly) CGFloat borderWidth;
@property (nonatomic, strong, readonly, nullable) UIColor *borderColor;
- (nonnull instancetype)init NS_UNAVAILABLE;
+ (nonnull instancetype)transformerWithRadius:(CGFloat)cornerRadius corners:(SDRectCorner)corners borderWidth:(CGFloat)borderWidth borderColor:(nullable UIColor *)borderColor;
@end
/**
调整大小
*/
@interface SDImageResizingTransformer : NSObject <SDImageTransformer>
@property (nonatomic, assign, readonly) CGSize size;
@property (nonatomic, assign, readonly) SDImageScaleMode scaleMode;
- (nonnull instancetype)init NS_UNAVAILABLE;
+ (nonnull instancetype)transformerWithSize:(CGSize)size scaleMode:(SDImageScaleMode)scaleMode;
@end
/**
裁剪
*/
@interface SDImageCroppingTransformer : NSObject <SDImageTransformer>
@property (nonatomic, assign, readonly) CGRect rect;
- (nonnull instancetype)init NS_UNAVAILABLE;
+ (nonnull instancetype)transformerWithRect:(CGRect)rect;
@end
/**
翻转
*/
@interface SDImageFlippingTransformer : NSObject <SDImageTransformer>
@property (nonatomic, assign, readonly) BOOL horizontal;
@property (nonatomic, assign, readonly) BOOL vertical;
- (nonnull instancetype)init NS_UNAVAILABLE;
+ (nonnull instancetype)transformerWithHorizontal:(BOOL)horizontal vertical:(BOOL)vertical;
@end
/**
旋转
*/
@interface SDImageRotationTransformer : NSObject <SDImageTransformer>
@property (nonatomic, assign, readonly) CGFloat angle;
@property (nonatomic, assign, readonly) BOOL fitSize;
- (nonnull instancetype)init NS_UNAVAILABLE;
+ (nonnull instancetype)transformerWithAngle:(CGFloat)angle fitSize:(BOOL)fitSize;
@end
#pragma mark - Image Blending
/**
添加色彩
*/
@interface SDImageTintTransformer : NSObject <SDImageTransformer>
@property (nonatomic, strong, readonly, nonnull) UIColor *tintColor;
- (nonnull instancetype)init NS_UNAVAILABLE;
+ (nonnull instancetype)transformerWithColor:(nonnull UIColor *)tintColor;
@end
#pragma mark - Image Effect
/**
添加模糊
*/
@interface SDImageBlurTransformer : NSObject <SDImageTransformer>
@property (nonatomic, assign, readonly) CGFloat blurRadius;
- (nonnull instancetype)init NS_UNAVAILABLE;
+ (nonnull instancetype)transformerWithRadius:(CGFloat)blurRadius;
@end
#if SD_UIKIT || SD_MAC
/**
添加滤镜
*/
@interface SDImageFilterTransformer: NSObject <SDImageTransformer>
@property (nonatomic, strong, readonly, nonnull) CIFilter *filter;
- (nonnull instancetype)init NS_UNAVAILABLE;
+ (nonnull instancetype)transformerWithFilter:(nonnull CIFilter *)filter;
@end
通过这几个类可以对SDWebImageContextImageTransformer中value进行自定义,以达到我们的需要
(2)、SDWebImageContextCacheKeyFilter - 图片的指定缓存key,达到自定义缓存的目的,生成SDWebImageCacheKeyFilter对象。
一种是直接赋值给SDWebImageManager,另一种是放到context中处理。
在UIImageView调用加载图片时,设置下面代码,自定义缓存key。
注意block回调是在global queue中进行的。
设置SDWebImageCacheKeyFilter,在SD内部根据URL缓存数据时,会进入block中,可以对url进行自定义
SDWebImageManager.sharedManager.cacheKeyFilter =[SDWebImageCacheKeyFilter cacheKeyFilterWithBlock:^NSString * _Nullable(NSURL * _Nonnull url) {
url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
return [url absoluteString];
}];
(3)、SDWebImageContextCacheSerializer - 缓存的图片格式 SDWebImageCacheSerializer对象
一种是直接赋值给SDWebImageManager,另一种是放到context中处理。
当webP格式的图片data数据从磁盘读取时,会比普通格式的图片读取更加费时,所以我们下载完webP格式的data数据,将其图片格式变为PNG/JPEG,然后将NSData数据放入磁盘,这样下次读取的时候速度会更快。
在UIImageView调用加载图片时,设置下面代码,自定义缓存图片格式。
注意block回调是在global queue中进行的。
SDWebImageManager.sharedManager.cacheSerializer = [SDWebImageCacheSerializer cacheSerializerWithBlock:^NSData * _Nullable(UIImage * _Nonnull image, NSData * _Nullable data, NSURL * _Nullable imageURL) {
SDImageFormat format = [NSData sd_imageFormatForImageData:data];
switch (format) {
case SDImageFormatWebP:
return image.images ? data : nil;
default:
return data;
}
}];
欢迎关注我的公众号,专注iOS开发、大前端开发、跨平台技术分享。