SDWebImage源码剖析(-)

作者: 树下老男孩 | 来源:发表于2015-05-10 12:19 被阅读17147次

    在开发项目的过程中会用到很多第三方库,比如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源码剖析(二)

    相关文章

      网友评论

      • a72cb3f812b5:我要给sdwebimage添加cookie应该怎么添加,改options?
      • 夏趣意转秋来:一直不明白为什么SDW只需要一句CFRunLoopRun() 就可以保持线程生命 在CFRunLoopRun()之前不是应该添加个source/Timer/Observer才能有效么 没看见SDW哪里添加了啊
        夏趣意转秋来:@树下老男孩 不会自动创建吧
        那AFN为啥run之前要addPort
        树下老男孩:run的时候会自动创建
      • 74563645bb46: 为什么没有把connection添加到对应的runloop中,就直接CFRunLoopRun()
        夏趣意转秋来:AFN一样的调用了 [self.connection start]; 为毛还是要加上 [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        74563645bb46:@树下的老男孩 谢了,能加好友么?想多交流交流。
        树下老男孩:@74563645bb46 通过start来开始发送网络请求,该方法会将当前的connection作为一个Source添加到当前线程所在的Runloop中,如果当前线程没有runloop会自己创建一个
      • 74563645bb46:__strong __typeof (wself) sself = wself; 为什么要这么转
        树下老男孩:@74563645bb46 由于你外面只是weak引用,如果内部不做一次强引用的话可能在执行这些代码的期间self被释放
      • eae8518bc399:上午刚把源码看了下,下午就看到你的这篇文章了。还是你分析的深刻 :+1:
      • 2068e5e51f60:你前面的几段代码都折叠到一行了,请用三个```包裹多行代码,要不然没法看。
        树下老男孩:@黄立波 嗯嗯,估计之前转换的时候出错了 thanks~~~
      • __微凉:我查了一下资料http://stackoverflow.com/questions/18355795/what-does-isconcurrent-mean-for-nsoperation-running-from-nsoperationqueue,"In OS X v10.6 and later, operation queues ignore the value returned by this method and always start operations on a separate thread.",iOS4以后就没有并不并发的概念了.所以这里重写start不是为了能够并发.我认为这里重写start是因为main方法跑完之后这个operation就完成了,队列会把这个operation对象销毁,如果在main中设置了一个网络请求,那么回调应该会因为找不到代理方法而崩溃.而重写start方法,就可以自己掌控operation的生命周期了,能让这个operation在完成网络请求之前常驻线程.不知这样理解对不对
        树下老男孩:@__微凉 应该是,我了解到的也是这关乎NSOperation的生命周期,我后面在整理下,good question,😄
      • __微凉:我有个疑问困扰了好久,为什么它要重写start方法,而不重写main方法,重写start是为了支持并发.可是这个operation最终是要加入到OperationQueue里面,queue是异步执行的,那么这个支持并发的意义在哪里?

      本文标题:SDWebImage源码剖析(-)

      本文链接:https://www.haomeiwen.com/subject/vwvwfttx.html