通过UML类图迅速学习SDWebImage源码设计

趁着国庆,赶紧给自己充充电,最近两天看了一本大话设计模式,里面有一节讲的是UML类图,并且重温了Objective-C编程之道,iOS设计模式解析(提取码: sj2u),以前看各个设计模式的UML类图一直不理解,也记不住那些符号什么意思,以至于对设计模式的理解不够深入,仅仅停留在一知半解的层面上,于是结合以前看的SDWebImage的架构图和源码,对UML类图有了更近一步的了解,同时将SDWebImage的源码又学习了一遍,本文的SDWebImage版本是4.4.2

一、SDWebImage的UML类图

类的表示

UML类的表示

注意:第一层表示类名,如果是抽象类就用斜体表示。第二层表示类的字段和属性。第三层表示类的方法操作。前面的符号“+”表示公有的,public;“-”表示私有的,private。
如果是接口顶端会有《interface》,在iOS中即是协议,如下所示:

UML中各类关系表示



由上两张图可知:

  1. 依赖关系表示:使用虚线箭头,如上图以UIButton+WebCacheUIView+WebCache为例: sd_setImageWithURL依赖于sd_internalSetImageWithURL方法;
  2. 聚合关系表示:空心菱形+实线,聚合表示一种拥有关系,体现的是A对象包含B对象,但B对象不是A对象的一部分(这里后半部分定义和实际好像不一样)。在上图的SDWebImageManagerSDImageCacheSDWebImageDownloader就是一种聚合关系,表示SDWebImageManager聚合SDImageCacheSDWebImageDownloader,并且拥有这两个对象的实例,分别是imageCacheimageDownloader
  3. 实现协议三角形+虚线,如上图,以SDWebImageOperation协议和SDWebImageCombinedOperation类为例,SDWebImageCombinedOperation遵循SDWebImageOperation协议,并且实现了cancel方法;
  4. 组合关系,表示整体和部分的关系,
  5. 继承关系,包括子类继承父类,协议继承,上图中显示的协议继承关系。

总的UML架构图


整个设计完全满足面向对象的六大原则

单一职责原则

定义:一个类应该是一组相关性很高的函数、数据的封装。
SDImageCache专门负责图片的缓存逻辑,SDWebImageDownloader专门负责图片下载逻辑;

开闭原则

定义:对象、模块、函数对于扩展是开放,对于修改是封闭的。
UML类图中的编解码模块体现了开闭原则的思想,SDWebImageImageIOCoderSDWebImageGIFCoderSDWebImageWebPCoder都实现了SDWebImageCoder协议所定义的图片编码解码策略。他们编解码图片的具体实现完全不一样,而且如果用户需要自定义实现编解码策略时,只需新建一个实现SDWebImageCoder协议的类。

里氏替换原则

定义:所有引用基类的地方必须能透明地使用其子类对象。里氏替换核心原则是抽象,抽象又依赖于继承。
同样是编解码模块,SDWebImageProgressiveCoder继承SDWebImageCoder

依赖倒置原则

核心就是:面向接口编程
解释同开闭原则里说的的SDWebImageCoder协议。

接口隔离原则

定义:客户端不应该依赖他不需要的接口。
SDWebImageDownloader就是接口隔离运用,SDWebImageManager只需要知道该下载对象有downloadImageWithURL下载图片的接口即可。下载图片的具体实现对SDWebImageManager隐藏,他用最小化接口隔离了实现细节。

迪米特原则

定义:一个对象应该对其他对象有最少的了解。通俗点讲就是:一个类应该对自己需要耦合或者调用的类知道最少,类的内部如何实现与调用者或者依赖者没有关系,调用者或者依赖者只需要知道她需要的方法即可。
UML类图中的组合,聚合,依赖关系都体现了这个原则,UIkit扩展模块就很明显的体现了这种思想,只需要知道有一个调用方法sd_setImageWithURL:能够设置图片,外部调用者(也就是程序员)不需要具体的图片下载缓存逻辑和缓存策略。

二、SDWebImage加载图片逻辑

时序图.png

图片加载逻辑如下:

首先会在SDImageCache中以图片url作为key在内存缓存中查找,是否有对应的缓存图片UIImage;

- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
    return [self.memCache objectForKey:key];
}

如果内存缓存中没有找到,则会以图片url进行MD5加密后作为key在磁盘中查找。如果找到了,则会把磁盘中的数据加载到内存中,并且将图片显示出来;


// url md5加密
- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
    const char *str = key.UTF8String;
    if (str == NULL) {
        str = "";
    }
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSURL *keyURL = [NSURL URLWithString:key];
    NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
            r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
            r[11], r[12], r[13], r[14], r[15], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]];
    return filename;
}

// 磁盘中查找图片是否存在
- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
    NSString *defaultPath = [self defaultCachePathForKey:key];
    NSData *data = [NSData dataWithContentsOfFile:defaultPath options:self.config.diskCacheReadingOptions error:nil];
    if (data) {
        return data;
    }

    // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
    // checking the key with and without the extension
    data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
    if (data) {
        return data;
    }

    NSArray<NSString *> *customPaths = [self.customPaths copy];
    for (NSString *path in customPaths) {
        NSString *filePath = [self cachePathForKey:key inPath:path];
        NSData *imageData = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil];
        if (imageData) {
            return imageData;
        }

        // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
        // checking the key with and without the extension
        imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
        if (imageData) {
            return imageData;
        }
    }

    return nil;
}

NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeDisk;
if (image) {
    // the image is from in-memory cache
    diskImage = image;
    cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
    // decode image data only if in-memory cache missed
    diskImage = [self diskImageForKey:key data:diskData options:options];
    if (diskImage && self.config.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(diskImage);
        [self.memCache setObject:diskImage forKey:key cost:cost];
    }
}

如果内存和磁盘缓存中都没有找到,就会向后台发送请求,下载图片;

下载后的图片会缓存到内存中,并且写入磁盘中(写入磁盘得判断SDWebImageCacheMemoryOnly,这个过程在异步线程中完成);

// SDWebImageManager
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    @autoreleasepool {
        UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

        if (transformedImage && finished) {
            BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
            NSData *cacheData;
            // pass nil if the image was transformed, so we can recalculate the data from the image
            if (self.cacheSerializer) {
            cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
        } else {
            cacheData = (imageWasTransformed ? nil : downloadedData);
        }
        [self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
        }

        [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
    }
});

// SDImageCache
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock

三、SDWebImage架构组成


由上图可以看出,SDWebImage的整体架构分为如下5个功能模块

SDWebImageManager承上启下部分

SDWebImageManager组合了图片下载和图片缓存,还可以查询图片url对应的缓存是否在内存还是在磁盘,以及手动调用缓存图片到内存和磁盘
这里主要介绍SDWebImageOptions的含义

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    /**
     * By default, when a URL fail to be downloaded, the URL is blacklisted so the library won't keep trying.
     * This flag disable this blacklisting.
     */
// 默认情况下,当一个URL下载失败,该URL被列入黑名单,将不会继续尝试下载,此标志取消黑名单
    SDWebImageRetryFailed = 1 << 0,

    /**
     * By default, image downloads are started during UI interactions, this flags disable this feature,
     * leading to delayed download on UIScrollView deceleration for instance.
     */
// 默认情况下,在UI交互式会下载图片,此标志取消这一功能,会延迟下载,当scrollview减速的时候继续下载
// 下载监听的运行runloop时defaultmode
    SDWebImageLowPriority = 1 << 1, // 低优先级

    /**
     * This flag disables on-disk caching after the download finished, only cache in memory
     */
// 禁止磁盘缓存,只用内存缓存图片
    SDWebImageCacheMemoryOnly = 1 << 2,

    /**
     * This flag enables progressive download, the image is displayed progressively during download as a browser would do.
     * By default, the image is only displayed once completely downloaded.
     */
// 此标志允许渐进氏下载,就像浏览器中那样,下载过程中,图片逐步显示出来
// 默认情况下,图像只会在下载完成显示
    SDWebImageProgressiveDownload = 1 << 3,

    /**
     * Even if the image is cached, respect the HTTP response cache control, and refresh the image from remote location if needed.
     * The disk caching will be handled by NSURLCache instead of SDWebImage leading to slight performance degradation.
     * This option helps deal with images changing behind the same request URL, e.g. Facebook graph api profile pics.
     * If a cached image is refreshed, the completion block is called once with the cached image and again with the final image.
     *
     * Use this flag only if you can't make your URLs static with embedded cache busting parameter.
     */
// 即使图片被缓存,遵守http响应的缓存机制,如果需要,从远程刷新图片;磁盘缓存将由NSURLCache处理而不是
// SDWebImage,这会对性能有轻微影响;此标志有助于处理同一个请求URL的图像发生变化;如果缓存的图片被刷新,会调用依稀competion,并传递最终的图片;仅在无法使用嵌入式参数确定图片URL时使用此标志
    SDWebImageRefreshCached = 1 << 4, // 刷新缓存

    /**
     * In iOS 4+, continue the download of the image if the app goes to background. This is achieved by asking the system for
     * extra time in background to let the request finish. If the background task expires the operation will be cancelled.
     */
    SDWebImageContinueInBackground = 1 << 5, // 后台下载

    /**
     * Handles cookies stored in NSHTTPCookieStore by setting
     * NSMutableURLRequest.HTTPShouldHandleCookies = YES;
     */
    SDWebImageHandleCookies = 1 << 6,

    /**
     * Enable to allow untrusted SSL certificates.
     * Useful for testing purposes. Use with caution in production.
     */
// 可以出于测试目的使用,在正式产品中慎用
    SDWebImageAllowInvalidSSLCertificates = 1 << 7, // 允许不信任的SSL证书

    /**
     * By default, images are loaded in the order in which they were queued. This flag moves them to
     * the front of the queue.
     */
    SDWebImageHighPriority = 1 << 8, // 高优先级下载
    
    /**
     * By default, placeholder images are loaded while the image is loading. This flag will delay the loading
     * of the placeholder image until after the image has finished loading.
     */
    SDWebImageDelayPlaceholder = 1 << 9,

    /**
     * We usually don't call transformDownloadedImage delegate method on animated images,
     * as most transformation code would mangle it.
     * Use this flag to transform them anyway.
     */
    SDWebImageTransformAnimatedImage = 1 << 10,
    
    /**
     * By default, image is added to the imageView after download. But in some cases, we want to
     * have the hand before setting the image (apply a filter or add it with cross-fade animation for instance)
     * Use this flag if you want to manually set the image in the completion when success
     */
    SDWebImageAvoidAutoSetImage = 1 << 11,
    
    /**
     * By default, images are decoded respecting their original size. On iOS, this flag will scale down the
     * images to a size compatible with the constrained memory of devices.
     * If `SDWebImageProgressiveDownload` flag is set the scale down is deactivated.
     */
    SDWebImageScaleDownLargeImages = 1 << 12,
    
    /**
     * By default, we do not query disk data when the image is cached in memory. This mask can force to query disk data at the same time.
     * This flag is recommend to be used with `SDWebImageQueryDiskSync` to ensure the image is loaded in the same runloop.
     */
    SDWebImageQueryDataWhenInMemory = 1 << 13,
    
    /**
     * By default, we query the memory cache synchronously, disk cache asynchronously. This mask can force to query disk cache synchronously to ensure that image is loaded in the same runloop.
     * This flag can avoid flashing during cell reuse if you disable memory cache or in some other cases.
     */
    SDWebImageQueryDiskSync = 1 << 14,
    
    /**
     * By default, when the cache missed, the image is download from the network. This flag can prevent network to load from cache only.
     */
    SDWebImageFromCacheOnly = 1 << 15,
    /**
     * By default, when you use `SDWebImageTransition` to do some view transition after the image load finished, this transition is only applied for image download from the network. This mask can force to apply view transition for memory and disk cache as well.
     */
    SDWebImageForceTransition = 1 << 16
};

图片缓存模块

图片缓存包括内存缓存磁盘缓存,总共就两个类:SDImageCacheConfig缓存配置类和SDImageCache缓存核心类。

  1. 先看SDImageCacheConfig类的初始化方法,里面主要是一些配置信息
- (instancetype)init {
    if (self = [super init]) {
        _shouldDecompressImages = YES; // 解压缩下载和缓存的图像可以提高性能,但会占用大量内存。默认为YES。 如果由于过多的内存消耗而遇到崩溃,请将此项设置为NO。
        _shouldDisableiCloud = YES; // 禁止iCloud备份,默认为YES
        _shouldCacheImagesInMemory = YES; // 是否使用内存缓存,默认为YES,禁用内存缓存时,也会禁用弱内存缓存。
        _shouldUseWeakMemoryCache = YES; // 弱内存缓存,默认为YES
        _diskCacheReadingOptions = 0; // 从磁盘读取缓存时的读取选项。 默认为0,可以将其设置为`NSDataReadingMappedIfSafe`以提高性能。
        _diskCacheWritingOptions = NSDataWritingAtomic; // 将缓存写入磁盘时的写入选项。默认为`NSDataWritingAtomic`。 可以将其设置为“NSDataWritingWithoutOverwriting”以防止覆盖现有文件。
        _maxCacheAge = kDefaultCacheMaxCacheAge; // 图片保留在缓存中的最长时间(以秒为单位),默认为7天。
        _maxCacheSize = 0; // 缓存的最大大小,以字节为单位,默认为0。
        _diskCacheExpireType = SDImageCacheConfigExpireTypeModificationDate; // 清除磁盘缓存时将检查清除缓存的属性,默认为修改日期
    }
    return self;
}

  1. SDImageCache
    2.1 SDMemoryCache继承NSCache,专门处理内存缓存。,NSCache是苹果官方提供的缓存类,和NSMutableDictionary类似,但是又有区别,区别如下:NSCache是线程安全的;NSCache的Key只是对对象进行强引用,不是拷贝(NSDictionary对key会进行拷贝);
    最主要是SDMemoryCache里面有一个NSMapTable类型的的weakCache属性进行二级缓存,他的主要作用是当内存警告,缓存被清除时, 但是,图像实例可以由其他实例保留例如imageViews而存活,在这种情况下,我们可以同步弱缓存,而不需要从磁盘缓存加载。NSMapTable: more than an NSDictionary for weak pointers
    weakCache的设值和取值,使用了GCD中的信号量进行加锁保证线程安全,self.weakCacheLock = dispatch_semaphore_create(1);
    监听系统内存警告通知UIApplicationDidReceiveMemoryWarningNotification,并且通过调用[super removeAllObjects];移除缓存,但是保留了弱缓存weakCache
    设置内存缓存的方法是:[self.memCache setObject:image forKey:key cost:cost];,其中key是图片的url,object是UIImage类型对象,cost等于image.size.height * image.size.width * image.scale * image.scale
    2.2 磁盘缓存,缓存路径是:~/Library/Caches/default/com.hackemist.SDWebImageCache.default。图片缓存名:url(md5) + .扩展。图片存储到磁盘和查询都是在串行队列中进行,保证线程安全//_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
    监听了UIApplicationWillTerminateNotificationUIApplicationDidEnterBackgroundNotification事件进行磁盘清理,清理操作也是在ioQueue中进行异步清理,保证线程安全。
    清理磁盘缓存逻辑如下:
    第一步:清除已经超多最大缓存时间(默认一周)的缓存文件;
    第二步:保存缓存文件大小;
    第三步:判断设置的缓存大小,进行第二轮清除。
    具体代码如下:
// 清除磁盘缓存的核心方法
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        // 磁盘缓存路径
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];

        // Compute content date key to be used for tests
        NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
        switch (self.config.diskCacheExpireType) {
            case SDImageCacheConfigExpireTypeAccessDate:
                cacheContentDateKey = NSURLContentAccessDateKey;
                break;

            case SDImageCacheConfigExpireTypeModificationDate:
                cacheContentDateKey = NSURLContentModificationDateKey;
                break;

            default:
                break;
        }
        // NSURLIsDirectoryKey:拿到目录的key;vcacheContentDateKey:最后修改时间的key;vNSURLTotalFileAllocatedSizeKey:文件总大小key
        NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];

        // This enumerator prefetches useful properties for our cache files.
        // 枚举器遍历磁盘缓存路径,NSDirectoryEnumerationSkipsHiddenFiles:不遍历隐藏路径
        // 迭代器设计模式
        NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
        includingPropertiesForKeys:resourceKeys
        options:NSDirectoryEnumerationSkipsHiddenFiles
        errorHandler:NULL];
        // 截止日期(返回最大时间之前的日期),7天之前的时间
        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
        NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
        NSUInteger currentCacheSize = 0;

        // Enumerate all of the files in the cache directory.  This loop has two purposes:
        //
        //  1. Removing files that are older than the expiration date.
        //  2. Storing file attributes for the size-based cleanup pass.
        NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSError *error;
            NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];

            // Skip directories and errors.
            if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }

            // Remove files that are older than the expiration date;
            // 拿到最后的修改日期
            NSDate *modifiedDate = resourceValues[cacheContentDateKey];
            // 过期的存到数组里面
            if ([[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }

            // 未过期的添加到cacheFiles里面
            // Store a reference to this file and account for its total size.
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
            cacheFiles[fileURL] = resourceValues;
        }

        for (NSURL *fileURL in urlsToDelete) {
            [self.fileManager removeItemAtURL:fileURL error:nil];
        }

        // If our remaining disk cache exceeds a configured maximum size, perform a second
        // size-based cleanup pass.  We delete the oldest files first.
        // 判断是否超出磁盘缓存上限,默认是没有指定的
        if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
            // Target half of our maximum cache size for this cleanup pass.
            const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;

            // Sort the remaining cache files by their last modification time or last access time (oldest first).
            NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
            usingComparator:^NSComparisonResult(id obj1, id obj2) {
            return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
            }];

            // Delete files until we fall below our desired cache size.
            for (NSURL *fileURL in sortedFiles) {
                if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
                    NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;

                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

app进入后台进行清理有向系统申请后台存活时间,具体代码如下:

- (void)backgroundDeleteOldFiles {
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
    __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
        // Clean up any unfinished task business by marking where you
        // stopped or ending the task outright.
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];

    // Start the long-running task and return immediately.
    [self deleteOldFilesWithCompletionBlock:^{
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
}

图片下载模块

总共就两个类:SDWebImageDownloaderSDWebImageDownloaderOperation
每个下载任务的超时时长15s,_downloadTimeout = 15.0;
下载的最大并发数是6个,_downloadQueue.maxConcurrentOperationCount = 6;;这里说下串行队列和并发队列的区别:串行队列(Serial Queue)指队列中同一时间只能执行一个任务,当前任务执行完后才能执行下一个任务,在串行队列中只有一个线程。并发队列(Concurrent Queue)允许多个任务在同一个时间同时进行,在并发队列中有多个线程。串行队列的任务一定是按开始的顺序结束,而并发队列的任务并不一定会按照开始的顺序而结束。这里最大并发数设置为6表示队列中最多只能有6个图片下载任务同时进行。
SDWebImageDownloader类的核心思想逻辑是以需要下载图片url作为key来创建一个NSOperation,用URLOperations保存,这么做的目的是避免创建重复的下载;创建完成后的operation就会被添加到downloadQueue中开始下载任务:

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }
    
    LOCK(self.operationsLock);
    NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
    // There is a case that the operation may be marked as finished, but not been removed from `self.URLOperations`.
    if (!operation || operation.isFinished) {
        operation = [self createDownloaderOperationWithUrl:url options:options];
        __weak typeof(self) wself = self;
        operation.completionBlock = ^{
            __strong typeof(wself) sself = wself;
            if (!sself) {
                return;
            }
            LOCK(sself.operationsLock);
            [sself.URLOperations removeObjectForKey:url];
            UNLOCK(sself.operationsLock);
        };
        [self.URLOperations setObject:operation forKey:url];
        // Add operation to operation queue only after all configuration done according to Apple's doc.
        // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
        [self.downloadQueue addOperation:operation];
    }
    UNLOCK(self.operationsLock);

    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    
    SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
    token.downloadOperation = operation;
    token.url = url;
    token.downloadOperationCancelToken = downloadOperationCancelToken;

    return token;
}

SDWebImageDownloaderOperation就是具体进行下载图片操作的类,核心方法就是start方法里面使用创建一个NSURLSession进行下载操作

- (void)start {
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

#if SD_UIKIT
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
        if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
            __weak __typeof__ (self) wself = self;
            UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                __strong __typeof (wself) sself = wself;

                if (sself) {
                    [sself cancel];

                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif
        NSURLSession *session = self.unownedSession;
        if (!session) {
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            
            /**
             *  Create the session for this task
             *  We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
             *  method calls and completion handler calls.
             */
            session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                    delegate:self
                                               delegateQueue:nil];
            self.ownedSession = session;
        }
        
        if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
            // Grab the cached data for later check
            NSURLCache *URLCache = session.configuration.URLCache;
            if (!URLCache) {
                URLCache = [NSURLCache sharedURLCache];
            }
            NSCachedURLResponse *cachedResponse;
            // NSURLCache's `cachedResponseForRequest:` is not thread-safe, see https://developer.apple.com/documentation/foundation/nsurlcache#2317483
            @synchronized (URLCache) {
                cachedResponse = [URLCache cachedResponseForRequest:self.request];
            }
            if (cachedResponse) {
                self.cachedData = cachedResponse.data;
            }
        }
        
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }

    if (self.dataTask) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
        if ([self.dataTask respondsToSelector:@selector(setPriority:)]) {
            if (self.options & SDWebImageDownloaderHighPriority) {
                self.dataTask.priority = NSURLSessionTaskPriorityHigh;
            } else if (self.options & SDWebImageDownloaderLowPriority) {
                self.dataTask.priority = NSURLSessionTaskPriorityLow;
            }
        }
#pragma clang diagnostic pop
        [self.dataTask resume];
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:weakSelf];
        });
    } else {
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
        [self done];
        return;
    }

#if SD_UIKIT
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
        UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
        [app endBackgroundTask:self.backgroundTaskId];
        self.backgroundTaskId = UIBackgroundTaskInvalid;
    }
#endif
}

图片编解码模块

这一块内容自己也还是没有完全弄懂,包括decode,decompress,encode,目前还在学习中。
核心方法:强制解压图片(此过程在子线程中进行),谈谈 iOS 中图片的解压缩

- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image {
    if (![[self class] shouldDecodeImage:image]) {
        return image;
    }
    
    // autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
    // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
    @autoreleasepool{
        
        CGImageRef imageRef = image.CGImage;
        // device color space
        CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();
        BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);
        // iOS display alpha info (BRGA8888/BGRX8888)
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
        
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        
        // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
        // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
        // to create bitmap graphics contexts without alpha info.
        CGContextRef context = CGBitmapContextCreate(NULL,
                                                     width,
                                                     height,
                                                     kBitsPerComponent,
                                                     0,
                                                     colorspaceRef,
                                                     bitmapInfo);
        if (context == NULL) {
            return image;
        }
        
        // Draw the image into the context and retrieve the new bitmap image without alpha
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
        CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
        UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];
        CGContextRelease(context);
        CGImageRelease(imageRefWithoutAlpha);
        
        return imageWithoutAlpha;
    }
}

UIkit扩展模块

这里主要是各个UI控件设置网络图片的便捷方法,使用category刚好满足需求,也是用户接触最多的。比如下面的设置方法:

[cell.customImageView sd_setImageWithURL:[NSURL URLWithString:self.objects[indexPath.row]]
placeholderImage:placeholderImage
options:indexPath.row == 0 ? SDWebImageRefreshCached : 0];

总之,学完之后,很多编程思想可以借鉴这些优秀的开源库,并且在自己的项目中实践。

原文链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,423评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,339评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,241评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,503评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,824评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,262评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,615评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,337评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,989评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,300评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,829评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,193评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,753评论 3 230
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,970评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,708评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,295评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,207评论 2 258

推荐阅读更多精彩内容