SDWebImage源碼解析(一)

1 概述

SDWebImage基本是iOS項目的標配。他以靈活簡單的api,提供了圖片從加載、解析、處理、緩存、清理等一些列功能。讓咱們專心於業務的處理。可是並不意味着會用就能夠了,經過源碼分析和學習,讓咱們知道如何用好它。學習分析優秀源碼也能夠從潛移默化中給咱們提供不少解決平常需求的思路。下面就是一張圖來概述SDWebImage的全部類:html

img

經過對這個圖片的分析,咱們能夠把SDWebImage的源碼分爲三種:git

  • 各類分類:github

    • UIButton(WebCache)UIButton類添加載圖片的方法。好比正常狀況下、點擊狀況下、的image屬性和背景圖片等。api

    • MKAnnotationView(WebCache)MKAnnotationView類添加各類加載圖片的方法。緩存

    • UIImageView(WebCache)UIImageView類添加加載圖片的方法。cookie

    • UIImageView(HighlightedWebCache)UIImageView類添加高亮狀態下加載圖片的方法。網絡

    • FLAnimatedImageView(WebCache)FLAnimatedImageView類添加加載動態的方法,這個分類須要引入FLAnimatedImage框架。SDWebImage推薦使用這個框架來處理動態圖片(GIF)的加載。框架

    • UIImageView、UIButton、FLAnimatedImageView經過sd_setImageWithURL等api來作圖片加載請求。這也是咱們惟一須要作的。async

    • 上面的幾個UIView子類都會調用UIView(WebCache)分類的sd_internalSetImageWithURL方法來作圖片加載請求。具體是經過SDWebImageManager調用來實現的。同時實現了Operation取消、ActivityIndicator的添加與取消。工具

  • 各類工具類:

    • NSData+ImageContentType: 根據圖片數據獲取圖片的類型,好比GIF、PNG等。

    • SDWebImageCompat: 根據屏幕的分辨倍數成倍放大或者縮小圖片大小。

    • SDImageCacheConfig: 圖片緩存策略記錄。好比是否解壓縮、是否容許iCloud、是否容許內存緩存、緩存時間等。默認的緩存時間是一週。

    • UIImage+MultiFormat: 獲取UIImage對象對應的data、或者根據data生成指定格式的UIImage,其實就是UIImage和NSData之間的轉換處理。

    • UIImage+GIF: 對於一張圖片是否GIF作判斷。能夠根據NSData返回一張GIF的UIImage對象,而且只返回GIF的第一張圖片生成的GIF。若是要顯示多張GIF,使用FLAnimatedImageView

    • SDWebImageDecoder: 根據圖片的狀況,作圖片的解壓縮處理。而且根據圖片的狀況決定如何處理解壓縮。

  • 核心類:

    • SDImageCache: 負責SDWebImage的整個緩存工做,是一個單列對象。緩存路徑處理、緩存名字處理、管理內存緩存和磁盤緩存的建立和刪除、根據指定key獲取圖片、存入圖片的類型處理、根據緩存的建立和修改日期刪除緩存。

    • SDWebImageManager: 擁有一個SDWebImageCacheSDWebImageDownloader屬性分別用於圖片的緩存和加載處理。爲UIView及其子類提供了加載圖片的統一接口。管理正在加載操做的集合。這個類是一個單列。還有就是各類加載選項的處理。

    • SDWebImageDownloader: 實現了圖片加載的具體處理,若是圖片在緩存存在則從緩存區。若是緩存不存在,則直接建立一個。SDWebImageDownloaderOperation對象來下載圖片。管理NSURLRequest對象請求頭的封裝、緩存、cookie的設置。加載選項的處理等功能。管理Operation之間的依賴關係。這個類是一個單列.

    • SDWebImageDownloaderOperation: 一個自定義的並行Operation子類。這個類主要實現了圖片下載的具體操做、以及圖片下載完成之後的圖片解壓縮、Operation生命週期管理等。

    • UIView+WebCache: 全部的UIButton、UIImageView都回調用這個分類的方法來完成圖片加載的處理。同時經過UIView+WebCacheOperation分類來管理請求的取消和記錄工做。全部UIView及其子類的分類都是用這個類的sd_intemalSetImageWithURL:來實現圖片的加載。

    • FLAnimatedImageView: 動態圖片的數據經過ALAnimatedImage對象來封裝。FLAnimatedImageViewUIImageView的子類。經過他徹底能夠實現動態圖片的加載顯示和管理。而且比UIImageView作了流程優化。

2 實現流程

SDWebImage爲咱們實現了圖片加載、數據處理、圖片緩存等一些列工做。經過下圖咱們能夠分析一下他的流程:

img

經過這個圖,咱們發現SDWebImage加載的過程是首先從緩存中加載數據。並且緩存加載又是優先從內存緩存中加載,而後纔是磁盤加載。最後若是緩存沒有,才從網絡上加載。同時網絡成功加載圖片之後,存入本地緩存。

3 UIView+WebCache分析

UIImageView、UIButton、FLAnimatedImageView都會調用UIView(WebCache)分類的sd_internalSetImageWithURL方法來作圖片加載請求。具體是經過SDWebImageManager調用來實現的。同時實現了Operation取消、ActivityIndicator的添加與取消。咱們首先來看sd_internalSetImageWithURL方法的實現:

/**
 全部UIView及其子類都是經過這個方法來加載圖片

 @param url 加載的url
 @param placeholder 佔位圖
 @param options 加載選項
 @param operationKey key
 @param setImageBlock Block
 @param progressBlock 進度Block
 @param completedBlock 回調Block
 */
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock {
    
    //取消當前類所對應的全部下載Operation對象
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    /*
     把UIImageView的加載圖片操做和他自身用關聯對象關聯起來,方便後面取消等操做。關聯的key就是UIImageView對應的類名
     */
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //若是有設置站位圖,則先顯示站位圖
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }
    
    if (url) {
        // check if activityView is enabled or not
        //若是UIImageView對象有設置添加轉動菊花數據,加載的時候添加轉動的菊花
        if ([self sd_showActivityIndicatorView]) {
            [self sd_addActivityIndicator];
        }
        
        __weak __typeof(self)wself = self;
        /*
         *operation是一個`SDWebImageCombinedOperation`對象。經過這個對象來獲取圖片
         */
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            __strong __typeof (wself) sself = wself;
            //中止菊花
            [sself sd_removeActivityIndicator];
            if (!sself) {
                return;
            }
            dispatch_main_async_safe(^{
                if (!sself) {
                    return;
                }
                //若是設置了不自動顯示圖片,則直接調用completedBlock,讓調用者處理圖片的顯示
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
                    completedBlock(image, error, cacheType, url);
                    return;
                } else if (image) {
                    //自動顯示圖片
                    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    [sself sd_setNeedsLayout];
                } else {
                    //若是設置了延遲顯示佔位圖,則圖片加載失敗的狀況下顯示佔位圖
                    if ((options & SDWebImageDelayPlaceholder)) {
                        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                        [sself sd_setNeedsLayout];
                    }
                }
                //完成回調
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        //關聯Operationkey與Operation對象。方便後面根據key取消operation操做等。
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
        //加載失敗的狀況
        dispatch_main_async_safe(^{
            //移除菊花
            [self sd_removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

給UIView及其子類添加旋轉菊花是經過關聯對象來實現的。經過以下幾個方法來實現:

#pragma mark 經過關聯對象來實現菊花的添加
- (UIActivityIndicatorView *)activityIndicator {
    return (UIActivityIndicatorView *)objc_getAssociatedObject(self, &TAG_ACTIVITY_INDICATOR);
}

- (void)setActivityIndicator:(UIActivityIndicatorView *)activityIndicator {
    objc_setAssociatedObject(self, &TAG_ACTIVITY_INDICATOR, activityIndicator, OBJC_ASSOCIATION_RETAIN);
}
#pragma mark 是否顯示旋轉菊花
- (void)sd_setShowActivityIndicatorView:(BOOL)show {
    objc_setAssociatedObject(self, &TAG_ACTIVITY_SHOW, @(show), OBJC_ASSOCIATION_RETAIN);
}

- (BOOL)sd_showActivityIndicatorView {
    return [objc_getAssociatedObject(self, &TAG_ACTIVITY_SHOW) boolValue];
}
#pragma mark 旋轉菊花的樣式
- (void)sd_setIndicatorStyle:(UIActivityIndicatorViewStyle)style{
    objc_setAssociatedObject(self, &TAG_ACTIVITY_STYLE, [NSNumber numberWithInt:style], OBJC_ASSOCIATION_RETAIN);
}

- (int)sd_getIndicatorStyle{
    return [objc_getAssociatedObject(self, &TAG_ACTIVITY_STYLE) intValue];
}

還有就是經過UIView+WebCacheOperation類來實現UIView的圖片下載Operation的關聯和取消。具體key的值能夠從sd_internalSetImageWithURL中找到具體獲取方式,經過在這個方法中實現Operation的關聯與取消。

/**
 關聯Operation對象與key對象

 @param operation Operation對象
 @param key key
 */
- (void)sd_setImageLoadOperation:(nullable id)operation forKey:(nullable NSString *)key {
    if (key) {
        [self sd_cancelImageLoadOperationWithKey:key];
        if (operation) {
            SDOperationsDictionary *operationDictionary = [self operationDictionary];
            operationDictionary[key] = operation;
        }
    }
}
/**
 取消當前key對應的全部實現了SDWebImageOperation協議的Operation對象

 @param key Operation對應的key
 */
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    // Cancel in progress downloader from queue
    //獲取當前View對應的全部key
    SDOperationsDictionary *operationDictionary = [self operationDictionary];
    //獲取對應的圖片加載Operation
    id operations = operationDictionary[key];
    //取消全部當前View對應的全部Operation
    if (operations) {
        if ([operations isKindOfClass:[NSArray class]]) {
            for (id <SDWebImageOperation> operation in operations) {
                if (operation) {
                    [operation cancel];
                }
            }
        } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
            [(id<SDWebImageOperation>) operations cancel];
        }
        [operationDictionary removeObjectForKey:key];
    }
}

4 FLAnimatedImageView分析

SDWebImage使用FLAnimatedImage框架來處理動態圖片,它包含FLAnimatedImageFLAnimatedImageView兩個雷。動態圖片的數據經過ALAnimatedImage對象來封裝。FLAnimatedImageViewUIImageView的子類。經過他徹底能夠實現動態圖片的加載顯示和管理。而且比UIImageView作了流程優化。咱們來看一下FLAnimatedImageView.h裏面定義的接口:

/**
 `FLAnimatedImageView`是一個`UIImageView`的子類。實現了`UIImageView`的`start/stop/isAnimating`方法。因此咱們能夠直接使用`FLAnimatedImageView`替代`UIImageView`。
 經過`CADisplayLink`對象來處理當前圖片幀和下一幀圖片的顯示。
 */
@interface FLAnimatedImageView : UIImageView
/**
 動態圖片的封裝對象。首先經過設置`[UIImageView.image]`爲nil來清除已經存在的動態圖片。設置`animatedImage`屬性會自動設置新的動態圖片而且開始顯示。並且會把當前顯示的UIImage存入`currentFrame`中。
 */
@property (nonatomic, strong) FLAnimatedImage *animatedImage;

@property (nonatomic, copy) void(^loopCompletionBlock)(NSUInteger loopCountRemaining);
/**
 當前動畫幀對應的UIImage對象
 */
@property (nonatomic, strong, readonly) UIImage *currentFrame;
/**
 當前圖片鎮對應的索引
 */
@property (nonatomic, assign, readonly) NSUInteger currentFrameIndex;
/**
 指定動態圖片執行所在的runloop的mode。NSRunLoopCommonMode
 */
@property (nonatomic, copy) NSString *runLoopMode;
@end

咱們經過FLAnimatedImageView+WebCache這個分類的sd_setImageWithURL來加載動態圖片:

/**
 FLAnimatedImage+WebCache分類經過這個方法來加載動態圖片

 @param url 圖片的url
 @param placeholder 佔位圖
 @param options 加載選項
 @param progressBlock 進度Block
 @param completedBlock 完成Block
 */
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock {
    __weak typeof(self)weakSelf = self;
    [self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                        operationKey:nil
                       setImageBlock:^(UIImage *image, NSData *imageData) {
                           //根據NSData的類型獲取圖片的類型
                           SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData];
                           //若是是GIF,則處理
                           if (imageFormat == SDImageFormatGIF) {
                               //給FLAnimatedImageView的animatedImage屬性設置動態圖片。這個setter方法被重寫了
                               weakSelf.animatedImage = [FLAnimatedImage animatedImageWithGIFData:imageData];
                               weakSelf.image = nil;
                           } else {
                               //不是動態圖片,則正常顯示
                               weakSelf.image = image;
                               weakSelf.animatedImage = nil;
                           }
                       }
                            progress:progressBlock
                           completed:completedBlock];
}

從上面能夠看出,獲取圖片數據之後。首先經過SDImageFormat獲得圖片的類型。若是是GIF類型,則先把圖片數據封裝成一個FLAnimatedImage對象。而後設置給animatedImage屬性。這個屬性的setter方法以下:

/**
 animatedImage的setter方法。經過這個屬性setter方法來設置FLAnimatedImageView的數據。而且開始動態顯示

 @param animatedImage animatedImage屬性
 */
- (void)setAnimatedImage:(FLAnimatedImage *)animatedImage
{
    if (![_animatedImage isEqual:animatedImage]) {
        if (animatedImage) {
            //清除UIImageView之前的圖片數據
            super.image = nil;
            super.highlighted = NO;
            //先說intrinsicContentSize,也就是控件的內置大小。好比UILabel,UIButton等控件,他們都有本身的內置大小。控件的內置大小每每是由控件自己的內容所決定的,好比一個UILabel的文字很長,那麼該UILabel的內置大小天然會很長。控件的內置大小能夠經過UIView的intrinsicContentSize屬性來獲取內置大小,也能夠經過invalidateIntrinsicContentSize方法來在下次UI規劃事件中從新計算intrinsicContentSize。若是直接建立一個原始的UIView對象,顯然它的內置大小爲0。
            [self invalidateIntrinsicContentSize];
        } else {
            //中止動態圖片的動態顯示
            [self stopAnimating];
        }
        //賦值
        _animatedImage = animatedImage;
        //當前動態圖片數據幀
        self.currentFrame = animatedImage.posterImage;
        //當前數據幀索引
        self.currentFrameIndex = 0;
        if (animatedImage.loopCount > 0) {
            self.loopCountdown = animatedImage.loopCount;
        } else {
            self.loopCountdown = NSUIntegerMax;
        }
        self.accumulator = 0.0;
        
        //更新對象的狀態。從而更新shouldAnimated這個屬性的值。
        [self updateShouldAnimate];
        if (self.shouldAnimate) {
            //開始動態顯示
            [self startAnimating];
        }
        
        [self.layer setNeedsDisplay];
    }
}
/**
 判斷當前FLAnimatedImageView是否須要顯示動畫
 */
- (void)updateShouldAnimate
{
    BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0;
    self.shouldAnimate = self.animatedImage && isVisible;
}

5 CADisplayLink

有趣的地方是FLAnimatedImageView經過過CADisplayLink來刷新動態圖片幀的顯示。CADisplayLink是一個能讓咱們以和屏幕刷新率相同的頻率將內容畫到屏幕上的定時器。咱們在應用中建立一個新的CADisplayLink對象,把它添加到一個runloop中,並給它提供一個target和selector在屏幕刷新的時候調用。

一但CADisplayLink以特定的模式註冊到runloop以後,每當屏幕須要刷新的時候runloop就會調用CADisplayLink綁定的target上的selector,這時target能夠讀到CADisplayLink的每次調用的時間戳,用來準備下一幀顯示須要的數據。例如一個視頻應用使用時間戳來計算下一幀要顯示的視頻數據。在UI作動畫的過程當中,須要經過時間戳來計算UI對象在動畫的下一幀要更新的大小等等。在添加進runloop的時候咱們應該選用高一些的優先級,來保證動畫的平滑。能夠設想一下,咱們在動畫的過程當中,runloop被添加進來了一個高優先級的任務,那麼,下一次的調用就會被暫停轉而先去執行高優先級的任務,而後在接着執行CADisplayLink的調用,從而形成動畫過程的卡頓,使動畫不流暢。duration屬性提供了每幀之間的時間,也就是屏幕每次刷新之間的的時間。咱們可使用這個時間來計算出下一幀要顯示的UI的數值。可是duration只是個大概的時間,若是CPU忙於其它計算,就無法保證以相同的頻率執行屏幕的繪製操做,這樣會跳過幾回調用回調方法的機會。frameInterval屬性是可讀可寫的NSInteger型值,標識間隔多少幀調用一次selector方法,默認值是1,即每幀都調用一次。若是每幀都調用一次的話,對於iOS設備來講那刷新頻率就是60HZ也就是每秒60次,若是將 frameInterval 設爲2 那麼就會兩幀調用一次,也就是變成了每秒刷新30次。咱們經過pause屬性開控制CADisplayLink的運行。當咱們想結束一個CADisplayLink的時候,應該調用-(void)invalidate從runloop中刪除並刪除以前綁定的 target跟selector。另外CADisplayLink 不能被繼承。

//每1/60秒都回調用一次displayDidRefresh方法來作UI處理
self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
//把displayLink加入主線程的commomMode裏面        
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];

最後原文地址.html),demo地址

相關文章
相關標籤/搜索