关于SDWebImage的一些小技巧

SDWebImage分享

网络图片显示大体步骤:

1.生成一个SDWebImageCombinedOperation对象(继承与NSOperation), 添加到operation池中
2.根据URL地址, 用MD5算出散列值存储在本地, 根据此散列值拼接文件路径, 检查沙盒中是否有缓存图片数据, 如果有, 读出放入NSData, 然后转换成UIImage并解码
3.如果找不到图片, 则启动下载流程下载使用NSMutableURLRequest包装的NSOperation, 放入NSOperationQueue并发下载

 dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }

        // Handle single download of simultaneous download request for the same URL
        NSMutableArray *callbacksForURL = self.URLCallbacks[url];
        NSMutableDictionary *callbacks = [NSMutableDictionary new];
        if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
        if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
        [callbacksForURL addObject:callbacks];
        self.URLCallbacks[url] = callbacksForURL;

        if (first) {
            createCallback();
        }
    });

下载图片后的缓存层
下载完成后, 在给imageView对象赋值image的同时, 内部会有一个cache的控制类, 这个类会创建一个串行的GCD队列, 执行文件写入的操作, 而且持有一个NSCache的对象, 保留最近刚下载完成的, 并且已经解压完成的图片对象, 图片在网络层下载完成会会进行decode再往上层传递, 在disk里读出图片也是同理, 所以在两个数据层往UI层传递参数的时候, 这个中间的缓存层都会引用一份已经被decode的imageData, 当下次需要使用时, 不需要再从磁盘中读出并decode出来, 并且这个类会配合内存警告自动清空ram里的图片缓存.

写入磁盘
从磁盘读取数据到内核缓冲区
从内核缓冲区复制到用户空间(内存级别拷贝)
解压缩为位图(耗cpu较高)
如果位图数据不是字节对齐的,CoreAnimation会copy一份位图数据并进行字节对齐
CoreAnimation渲染解压缩过的位图
以上4,5,6,7,8步是在UIImageView的setImage时进行的,所以默认在主线程进行(iOS UI操作必须在主线程执行)

2. 一些优化思路:

同一个URL, 但是图片改变了已被改变:

默认的行为, sd用的是图片的url算出md5散列值存储, 所以在同一url的情况下, 得出的散列值是一样的, 如果图片已经被缓存在disk上, 就会直接读取disk上的图而不会去刷新缓存
因此要使用SDWebImageDownloader单例里一个SDWebImageRefreshCached的策略, 并且在headersFilter中添加http的header
通过查阅HTTP协议相关的资料得知,与服务器返回的Last-Modified相对应的request header里可以加一个名为If-Modified-Since的key,value即是服务器回传的服务端图片最后被修改的时间,第一次图片请求时If-Modified-Since的值为空,第二次及以后的客户端请求会把服务器回传的Last-Modified值作为If-Modified-Since的值传给服务器,这样服务器每次接收到图片请求时就将If-Modified-Since与Last-Modified进行比较,如果客户端图片已陈旧那么返回状态码200、Last-Modified、图片内容,客户端存储Last-Modified和图片;如果客户端图片是最新的那么返回304 Not Modified、不会返回Last-Modified、图片内容。

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;
};

2.1 关于异步图片下载:

fastImageCache主要针对于从磁盘文件读取并展示图片的极端优化,所以并没有集成异步图片下载的功能。这里主要来看看SDWebImage(AFNetWorking的基本类似)的实现方案:

tableView中,异步图片下载任务的管理:
我们知道,tableViewCell是有重用机制的,也就是说,内存中只有当前可见的cell数目的实例,滑动的时候,新显示cell会重用被滑出的cell对象。这样就存在一个问题:

一般情况下在我们会在cellForRow方法里面设置cell的图片数据源,也就是说如果一个cell的imageview对象开启了一个下载任务,这个时候该cell对象发生了重用,新的image数据源会开启另外的一个下载任务,由于他们关联的imageview对象实际上是同一个cell实例的imageview对象,就会发生2个下载任务回调给同一个imageview对象。这个时候就有必要做一些处理,避免回调发生时,错误的image数据源刷新了UI。

SDWebImage提供的UIImageView扩展的解决方案:

imageView对象会关联一个下载列表(列表是给AnimationImages用的,这个时候会下载多张图片),当tableview滑动,imageView重设数据源(url)时,会cancel掉下载列表中所有的任务,然后开启一个新的下载任务。这样子就保证了只有当前可见的cell对象的imageView对象关联的下载任务能够回调,不会发生image错乱。

iOS异步任务一般有3种实现方式:
NSOperation
GCD
NSThread
SDWebImage通过自定义NSOperation来抽象下载任务的并将下载时的数据逻辑处理统一放到operation中,然后结合了GCD来做一些主线程与子线程的切换逻辑。

tableview滑动下的下载处理

从sdweb处理下载的逻辑可以知道, sd是以6个线程为最大并发数去处理下载queue, 这个跟tableview的行为其实有一定的背离. 因为当用户滑动tableview时 (假设你的cell里需要下载一个图片), 这是会不停地将下载的operation对象加入到队列中, 当用户快速滑动到列表的底部然后停下时, 需要等待队列前面大量的任务完成后才会开始当前cell的图片下载, 这时就需要LIFO机制了.
sd实现LIFO机制的方式十分巧妙, 使用的仍然是同一个operation并发队列并往里添加下载的operation

if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
    // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
    [wself.lastAddedOperation addDependency:operation];
    wself.lastAddedOperation = operation;
}

在一个并发队列里, 最后进入的operation会对这个即将进入的operation添加依赖, 这样在这个新的operation的state转为finish之前, 这个lastOperation都不会被执行.

2.2 关于图片解压缩:

通用的解压缩方案
主体的思路是在子线程,将原始的图片渲染成一张的新的可以字节显示的图片,来获取一个解压缩过的图片。
基本上比较流行的一些开源库都先后支持了在异步线程完成图片的解压缩,并对解压缩过后的图片进行缓存。

这么做的优点是在setImage的时候系统省去了解码decode的步骤,缺点就是图片占用的空间变大。

下面的代码是SDWebImage的解决方案:

+ (UIImage *)decodedImageWithImage:(UIImage *)image {
    if (image.images) {
        // Do not decode animated images
        return image;
    }

    CGImageRef imageRef = image.CGImage;
    CGSize imageSize = CGSizeMake(CGImageGetWidth(imageRef), CGImageGetHeight(imageRef));
    CGRect imageRect = (CGRect){.origin = CGPointZero, .size = imageSize};

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);

    int infoMask = (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);

    UIImage *decompressedImage = [UIImage imageWithCGImage:decompressedImageRef scale:image.scale orientation:image.imageOrientation];
    CGImageRelease(decompressedImageRef);
    return decompressedImage;
}

2.4 关于第3,4点,内存级别拷贝

FastImageCache对这一点做了很大的优化,其他的2个开源库则未关注这一点。这一块木有深入研究,就引用一下FastImageCache团队对该点的一些说明。

内存映射 平常我们读取磁盘上的一个文件,上层API调用到最后会使用系统方法read()读取数据,内核把磁盘数据读入内核缓冲区,用户再从内核缓冲区读取数据复制到用户内存空间,这里有一次内存拷贝的时间消耗,并且读取后整个文件数据就已经存在于用户内存中,占用了进程的内存空间。
FastImageCache采用了另一种读写文件的方法,就是用mmap把文件映射到用户空间里的虚拟内存,文件中的位置在虚拟内存中有了对应的地址,可以像操作内存一样操作这个文件,相当于已经把整个文件放入内存,但在真正使用到这些数据前却不会消耗物理内存,也不会有读写磁盘的操作,只有真正使用这些数据时,也就是图像准备渲染在屏幕上时,虚拟内存管理系统VMS才根据缺页加载的机制从磁盘加载对应的数据块到物理内存,再进行渲染。这样的文件读写文件方式少了数据从内核缓存到用户空间的拷贝,效率很高。

你可能感兴趣的:(关于SDWebImage的一些小技巧)