深入理解SDWebImage-扩展

在应用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];
深入理解SDWebImage-扩展_第1张图片
SDWebImageProgressiveLoad

场景三:设置了SDWebImageRefreshCached,缓存图片如何更新?

现在提供两种方法来解决这个问题 - 旧版本SDWebImage:
首先我们需要探讨一下实现的原理:一种是NSURLCache(NSURL缓存的get请求),一种是SD中自己定义的SDImageCache进行缓存。在SDWebImage中我们可以查看SDWebImageRefreshCached是如何定义的 - 如果设置了该类型,缓存策略依据NSURLCache而不是SDImageCache,所以可以通过NSURLCache进行缓存了;
但是图片的更新还需要的服务器的配合才能实现,服务器如何设置那?图片的更新与否取决于你服务器的cache-control设置,如果没有cache-control设置,那么客户端就享受不了自动更新的功能。首先了解一下cache-control,
终端中输入命令: curl [url] --head


深入理解SDWebImage-扩展_第2张图片
cache-control

发现有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进行解压缩的过程

雷纯锋的blog
图片的解压缩和渲染过程

场景五: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 

@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数组,按顺序做转换
 */
@interface SDImagePipelineTransformer : NSObject 

/**
 */
@property (nonatomic, copy, readonly, nonnull) NSArray> *transformers;

- (nonnull instancetype)init NS_UNAVAILABLE;
+ (nonnull instancetype)transformerWithTransformers:(nonnull NSArray> *)transformers;

@end

/**
 添加圆角
 */
@interface SDImageRoundCornerTransformer: NSObject 

@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 

@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 

@property (nonatomic, assign, readonly) CGRect rect;

- (nonnull instancetype)init NS_UNAVAILABLE;
+ (nonnull instancetype)transformerWithRect:(CGRect)rect;

@end

/**
 翻转
 */
@interface SDImageFlippingTransformer : NSObject 

@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 

@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 

@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 

@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 

@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开发、大前端开发、跨平台技术分享。


深入理解SDWebImage-扩展_第3张图片
iOS开发之家

你可能感兴趣的:(深入理解SDWebImage-扩展)