SDWebImage源码解析

来源:互联网 发布:nba2k18球星捏脸数据 编辑:程序博客网 时间:2024/04/29 01:01

http://www.jianshu.com/p/c07df06c60be


在开发项目的过程中会用到很多第三方库,比如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管理的,这个类有几个重要的属性:

@property (strong, nonatomic, readwrite) SDImageCache imageCache;//负责管理cache,涉及内存缓存和硬盘保存@property (strong, nonatomic, readwrite) SDWebImageDownloader imageDownloader;//负责从网络下载图片@property (strong, nonatomic) NSMutableArray *runningOperations;//包含所有当前正在下载的操作对象

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源码剖析(二)



文/树下的老男孩(简书作者)
原文链接:http://www.jianshu.com/p/c07df06c60be
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

0 0
原创粉丝点击