SDWebImage源码剖析(-)

在开发项目的过程中会用到很多第三方库,比如AFNetWorking,SDWebImage,FMDB等,但一直都没去好好的研究一下,最近刚好项目不是太紧,闲下来可以给自己充充电,先研究一下SDWebImage的底层实现,源码地址:SDWebImage
  先介绍一下SDWebImage,我们使用较多的是它提供的UIImageView分类,支持从远程服务器下载并缓存图片。自从iOS5.0开始,NSURLCache也可以处理磁盘缓存,那么SDWebImage的优势在哪?首先NSURLCache是缓存原始数据(raw data)到磁盘或内存,因此每次使用的时候需要将原始数据转换成具体的对象,如UIImage等,这会导致额外的数据解析以及内存占用等,而SDWebImage则是缓存UIImage对象在内存,缓存在NSCache中,同时直接保存压缩过的图片到磁盘中;还有一个问题是当你第一次在UIImageView中使用image对象的时候,图片的解码是在主线程中运行的!而SDWebImage会强制将解码操作放到子线程中。下图是SDWebImage简单的类图关系:

SDWebImage.png

下面从UIImageView的图片加载开始看起,Let's go!

首先我们在给UIImageView设置图片的时候会调用方法:

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;

其中url为远程图片的地址,而placeholder为预显示的图片。
其实还可以添加一些额外的参数,比如图片选项SDWebImageOptions


typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    SDWebImageRetryFailed = 1 << 0,//下载失败了会再次尝试下载
    WebImageLowPriority = 1 << 1,//当UIScrollView等正在滚动时,延迟下载图片(放置scrollView滚动卡)
    SDWebImageCacheMemoryOnly = 1 << 2,//只缓存到内存中
    SDWebImageProgressiveDownload = 1 << 3,// 图片会边下边显示
    SDWebImageRefreshCached = 1 << 4,//将硬盘缓存交给系统自带的NSURLCache去处理
    SDWebImageContinueInBackground = 1 << 5,//后台下载
    SDWebImageHandleCookies = 1 << 6,// 通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES来处理存储在NSHTTPCookieStore中的cookie
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,// 允许不受信任的SSL证书。主要用于测试目的。
    SDWebImageHighPriority = 1 << 8,
    SDWebImageDelayPlaceholder = 1 << 9,
    SDWebImageTransformAnimatedImage = 1 << 10,
};

一般使用的是SDWebImageRetryFailed | SDWebImageLowPriority,下面看看具体的函数调用:

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock 
{
    [self sd_cancelCurrentImageLoad];//取消正在下载的操作
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);//关联该view对应的图片URL  
   /*...*/ 
    if (url) {
        __weak UIImageView *wself = self;//防止retain cricle
        //由SDWebImageManager负责图片的获取
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
              /*获取图片到主线层显示*/ 
        }];
        [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
    } 
}

可以看出图片是从服务端、内存或者硬盘获取是由SDWebImageManager管理的,这个类有几个重要的属性:
<pre><code>
@property (strong, nonatomic, readwrite) SDImageCache *imageCache;//负责管理cache,涉及内存缓存和硬盘保存
@property (strong, nonatomic, readwrite) SDWebImageDownloader *imageDownloader;//负责从网络下载图片
@property (strong, nonatomic) NSMutableArray *runningOperations;//包含所有当前正在下载的操作对象
</code></pre>

manager会根据URL先去imageCache中查找对应的图片,如果没有在使用downloader去下载,并在下载完成缓存图片到imageCache,接着看实现:

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock
 {
     /*...*/
    //根据URL生成对应的key,没有特殊处理为[url absoluteString];
    NSString *key = [self cacheKeyForURL:url];
    //去imageCache中寻找图片
    operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) 
    {
       /*...*/
       //如果图片没有找到,或者采用的SDWebImageRefreshCached选项,则从网络下载
        if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
                dispatch_main_sync_safe(^{
                  //如果图片找到了,但是采用的SDWebImageRefreshCached选项,通知获取到了图片,并再次从网络下载,使NSURLCache重新刷新
                     completedBlock(image, nil, cacheType, YES, url);
                });
            }
            /*下载选项设置*/ 
            //使用imageDownloader开启网络下载
            id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
                /*...*/
               if (downloadedImage && finished) {
                     //下载完成后,先将图片保存到imageCache中,然后主线程返回
                     [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
                        }
                     dispatch_main_sync_safe(^{
                            if (!weakOperation.isCancelled) {
                                completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
                            }
                        });
                    }
                }
          /*...*/
       }
        else if (image) {
          //在cache中找到图片了,直接返回
            dispatch_main_sync_safe(^{
                if (!weakOperation.isCancelled) {
                    completedBlock(image, nil, cacheType, YES, url);
                }
            });
        }
    }];
    return operation;
}

下面先看downloader从网络下载的过程,下载是放在NSOperationQueue中进行的,默认maxConcurrentOperationCount为6,timeout时间为15s:

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
    __block SDWebImageDownloaderOperation *operation;
    __weak SDWebImageDownloader *wself = self;
    /*...*/
    //防止NSURLCache和SDImageCache重复缓存
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy :NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
    request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
    request.HTTPShouldUsePipelining = YES;
    request.allHTTPHeaderFields = wself.HTTPHeaders;//设置http头部
    //SDWebImageDownloaderOperation派生自NSOperation,负责图片下载工作
    operation = [[SDWebImageDownloaderOperation alloc] initWithRequest:request
                                                          options:options
                                                         progress:^(NSInteger receivedSize, NSInteger expectedSize) {}
                                                        completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {}
                                                        cancelled:^{}];
    operation.shouldDecompressImages = wself.shouldDecompressImages;//是否需要解码
    if (wself.username && wself.password) {
            operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
    }
    if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
    }
        [wself.downloadQueue addOperation:operation];
        if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // 如果下载顺序是后面添加的先运行
            [wself.lastAddedOperation addDependency:operation];
            wself.lastAddedOperation = operation;
        }
    }];
    return operation;
}

SDWebImageDownloaderOperation派生自NSOperation,通过NSURLConnection进行图片的下载,为了确保能够处理下载的数据,需要在后台运行runloop:

- (void)start {
  /*...*/
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
        //开启后台下载
        if ([self shouldContinueWhenAppEntersBackground]) {
            __weak __typeof__ (self) wself = self;
            self.backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
                __strong __typeof (wself) sself = wself;
                if (sself) {
                    [sself cancel];
                    [[UIApplication sharedApplication] endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif
        self.executing = YES;
        self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
    }
    [self.connection start];

    if (self.connection) {
        if (self.progressBlock) {
            self.progressBlock(0, NSURLResponseUnknownLength);
        }
       //在主线程发通知,这样也保证在主线程收到通知
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });
       CFRunLoopRun();//在默认模式下运行当前runlooprun,直到调用CFRunLoopStop停止运行
        if (!self.isFinished) {
            [self.connection cancel];
            [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
        }
    }
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
    if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
        [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId];
        self.backgroundTaskId = UIBackgroundTaskInvalid;
    }
#endif
}

下载过程中,在代理 - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data中将接收到的数据保存到NSMutableData中,[self.imageData appendData:data],下载完成后在该线程完成图片的解码,并在完成的completionBlock中进行imageCache的缓存:

- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
    SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
    @synchronized(self) {
        CFRunLoopStop(CFRunLoopGetCurrent());//停止当前对runloop
        /*...*/
        if (completionBlock) {
            /*...*/
            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) {
                 if (self.shouldDecompressImages) {
                    image = [UIImage decodedImageWithImage:image];//图片解码
                }
            }
            if (CGSizeEqualToSize(image.size, CGSizeZero)) {
                completionBlock(nil, nil, [NSError errorWithDomain:@"SDWebImageErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES);
            }
            else {
                completionBlock(image, self.imageData, nil, YES);
            }
        }
    }
    self.completionBlock = nil;
    [self done];
}

后续的图片缓存可以参考:SDWebImage源码剖析(二)

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

推荐阅读更多精彩内容