SDWebImage 源碼學習筆記 ? SDWebImageDownloader

SDWebImage-源碼學習筆記.png

前言

這是本系列的第 4 篇,本篇將主要介紹 SDWebImageDownloader 這個負責下載的類,當然還有一些相關類及協議,如: SDWebImageDownloadTokenSDWebImageDownloaderOperationSDWebImageDownloaderOperationInterface 等。

正文

開啟正文描述之前,依舊先看 2 個重要的枚舉:SDWebImageDownloaderOptionsSDWebImageDownloaderExecutionOrder,具體含義見下方代碼注釋。

// 控制下載過程的選項
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    // 降低下載任務在隊列中的優先級
    SDWebImageDownloaderLowPriority = 1 << 0,
    // 圖片將在下載過程中逐步展示,而不是等下載完成后才一次性展示
    SDWebImageDownloaderProgressiveDownload = 1 << 1,
    // 使用 NSURLCache,默認是不使用的
    SDWebImageDownloaderUseNSURLCache = 1 << 2,
    // 如果圖片是從 NSURLCache 讀取的,那么執行 completionHandler 的時候,回傳 nil
    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
    // 后臺繼續執行任務
    SDWebImageDownloaderContinueInBackground = 1 << 4,
    // 允許處理存儲在 NSHTTPCookieStore 中的 Cookie
    SDWebImageDownloaderHandleCookies = 1 << 5,
    // 允許不受信任的 SSL 證書,生產環境慎用
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
    // 提高下載任務在隊列中的優先級
    SDWebImageDownloaderHighPriority = 1 << 7,
    // 縮放大圖
    SDWebImageDownloaderScaleDownLargeImages = 1 << 8,
};

// 任務的執行順序
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
    // 先進先出,也是隊列的默認執行順序
    SDWebImageDownloaderFIFOExecutionOrder,
    // 后進先出,棧的執行順序
    SDWebImageDownloaderLIFOExecutionOrder
};

接下來就到了 SDWebImageDownloader 這個類,我不準備一個屬性一個方法地按順序討論,而是先說創建方法,然后通過一個主要方法將主體串起來。先看創建方法吧!

對外其實只公開了一個創建單例的方法 sharedDownloader,仔細查看代碼會發現,最終調用的是 - (nonnull instancetype)initWithSessionConfiguration: 這個初始化方法,主要做一些初始化工作,并創建一個新 session。如果使用單例方法創建 downloader,則只會有一個 session,而如果通過其他方法創建,則可能創建 session 之前已經有一個了,這時候就需要先將之前的 cancel 之后再創建新的。

+ (nonnull instancetype)sharedDownloader {
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        instance = [self new];
    });
    return instance;
}

- (nonnull instancetype)init {
    return [self initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
}

- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
    if ((self = [super init])) {
        // 執行下載任務的 operation
        _operationClass = [SDWebImageDownloaderOperation class];
        // 要求解壓圖片
        _shouldDecompressImages = YES;
        // 執行順序,先進先出
        _executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
        
        // 設置下載操作的隊列,由于最大并發數是 6,所以此 queue 是 并發隊列,如果是 1,則為串行隊列。
        _downloadQueue = [NSOperationQueue new];
        _downloadQueue.maxConcurrentOperationCount = 6;
        _downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
        
        _URLOperations = [NSMutableDictionary new];
        
        // 請求頭的字段,可接受的文件類型
#ifdef SD_WEBP
        _HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
#else
        _HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
        
        // 鎖,這里使用了信號量
        _operationsLock = dispatch_semaphore_create(1);
        _headersLock = dispatch_semaphore_create(1);
        // 超時時間
        _downloadTimeout = 15.0;

        [self createNewSessionWithConfiguration:sessionConfiguration];
    }
    return self;
}

// 創建新的 session
- (void)createNewSessionWithConfiguration:(NSURLSessionConfiguration *)sessionConfiguration {
    // 為避免影響,先取消可能存在的下載任務
    [self cancelAllDownloads];

    // cancel 之前的 session,然后創建一個新的
    if (self.session) {
        [self.session invalidateAndCancel];
    }

    sessionConfiguration.timeoutIntervalForRequest = self.downloadTimeout;

    self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
                                                 delegate:self
                                            delegateQueue:nil];
}

然后,我們看一下主要方法 - (nullable SDWebImageDownloadToken *)downloadImageWithURL:url options:options progress:progressBlock completed:completedBlock。直接調用了添加進度與完成回調的方法,并將返回值作為結果返回。

添加進度與完成回調的方法我們稍后再議,先看一下調用時傳入的 createCallback。就做了兩件事:先創建一個 request,用于準備一些基礎參數,然后,依據 request 創建 operation,詳見代碼注釋 ?。

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    __weak SDWebImageDownloader *wself = self;

    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
        
        __strong __typeof (wself) sself = wself;
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

// *** 1.創建 request
        
        // 為避免重復緩存 (NSURLCache + SDImageCache) ,如果沒有明確要求使用 NSURLCache,我們默認忽略本地緩存
        NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
                                                                    cachePolicy:cachePolicy
                                                                timeoutInterval:timeoutInterval];
        // The default is YES - in other words, cookies are sent from and stored to the cookie manager by default.
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        
        // 設置 header,headersFilter 是過濾頭部參數的block
        if (sself.headersFilter) {
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself allHTTPHeaderFields]);
        } else {
            request.allHTTPHeaderFields = [sself allHTTPHeaderFields];
        }
        
// *** 2.創建下載的 operation (這個 operationClass ,給他賦什么值,他就是什么,如果不設置,就是默認值:[SDWebImageDownloaderOperation class])
        
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request
                                                                                       inSession:sself.session
                                                                                         options:options];
        
        operation.shouldDecompressImages = sself.shouldDecompressImages;
        
        // NSURLCredential 身份認證
        if (sself.urlCredential) {
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            // NSURLCredentialPersistenceForSession: Credential should be stored only for this session.
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        // 設置優先級
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }
        
        // 更改執行順序:先進后出(可在此設置) or 先進先出(默認)
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // 通過反向設置依賴,指定了隊列中任務的執行順序先加進去的依賴于后加進去的,那就成了后進先出了??
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }

        return operation;
    }];
}

現在,我們來看看添加進度與完成回調的方法:

- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {
    // 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);
    
    SDWebImageDownloaderOperation *operation = [self.URLOperations objectForKey:url];
    
    // 如果是第 1 次進來,通過 url 是取不出 URLOperation 的,但是第 2 次就有可能找到,也就是想要重復發第 2 次請求的話,就可以取到。
    // 第 2 次可以取到(并且已經完成的情況下),則不會走括號里邊,也就不會執行關鍵步驟:[self.downloadQueue addOperation:operation]; ,所以就不會發起請求了,因為將 operation 添加到隊列的時候,系統會自動觸發請求。
    if (!operation || operation.isFinished) {
        // 創建 operation
        operation = createCallback();
        __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);

    // 存放進度和完成回調的 數組 array
    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    
    // 與下載任務關聯的一個對象,用于取消操作的時候
    SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
    token.downloadOperation = operation;
    token.url = url;
    token.downloadOperationCancelToken = downloadOperationCancelToken;

    return token;
}

主要做了這么幾件事:

  • 開始依然是參數校驗

  • 然后從 self.URLOperation 里邊取 operation

  • 第一次進來當然取不到 operation,于是就會進入 if (!operation || operation.isFinished) { ... } 的代碼塊。先執行我們傳入的 createCallback() 創建 operation,然后將 operation 加入到 self.URLOperations 里邊,同時設置好 operation 的 completionBlock,到時將 operation 移除,最后將 operation 加入到操作隊列里,就會自動開始執行了。

  • 創建一個 token,他是 SDWebImageDownloadToken 的實例,將它與 operation、url、progressBlock 及 completedBlock 關聯起來,用于后邊之后的取消操作。其中 progressBlock 及 completedBlock 是通過 downloadOperationCancelToken 與 token 關聯起來的,這里用到了 operation 中的一個方法:

// SDWebImageDownloaderOperation

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    
    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
    
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    
    LOCK(self.callbacksLock);
    
    [self.callbackBlocks addObject:callbacks];
    
    UNLOCK(self.callbacksLock);
    
    return callbacks;
}

由此可知,這個 id downloadOperationCancelToken 是一個存放 progressBlock 和 completedBlock 的 dictionary。

這里 if 語句起到了一個非常重要的作用,即 避免重復下載相同數據,具體原因就不解釋了,上邊的代碼注釋里已經寫了。

到這里是不是覺得少了點什么,是的,SDWebImageDownloaderOperationSDWebImageDownloadToken 的具體實現還不知道呢,接下來我們就分別查看這 2 個類,先從簡單的開始吧!

SDWebImageDownloadToken

這個類只有 3 個屬性,前邊都用到了,屬性聲明如下:

// 下載任務對應的 url
@property (nonatomic, strong, nullable) NSURL *url;
// 實際是包含 progressBlock 和 completionBlock 的字典,是通過 `addHandlersForProgress:completed:` 返回的,用于取消操作
@property (nonatomic, strong, nullable) id downloadOperationCancelToken;
// 操作的 operation,繼承自 NSOperation,不過又遵守了 `SDWebImageDownloaderOperationInterface` 這個協議,擴展了一些方法。
@property (nonatomic, weak, nullable) NSOperation<SDWebImageDownloaderOperationInterface> *downloadOperation;

它只實現了一個協議方法 cancel(Protocol: SDWebImageOperation),其中 self.downloadOperationCancelToken就是存放 progressBlock 和 completionBlock 的字典,然后將這個 token 創遞給了 operation 的 cancel: 方法(也是一個協議方法),這個方法的具體實現下邊就會說到。

- (void)cancel {
    if (self.downloadOperation) {
        SDWebImageDownloadToken *cancelToken = self.downloadOperationCancelToken;
        if (cancelToken) {
            [self.downloadOperation cancel:cancelToken];
        }
    }
}
SDWebImageDownloaderOperationInterface

在開始介紹 operation 之前,先看看他遵守的協議 SDWebImageDownloaderOperationInterface,聲明了以下協議方法。如果想要使用自定義的 operation,則它必須繼承自 NSOperation 并且遵守這個協議。這些方法的實現可以參考 SDWebImageDownloaderOperation

// SDWebImageDownloaderOperationInterface
// 初始化方法
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options;
// 保存進度和完成的回調
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
// 是否需要解壓圖片
- (BOOL)shouldDecompressImages;
- (void)setShouldDecompressImages:(BOOL)value;
// 憑證或稱證書信息
- (nullable NSURLCredential *)credential;
- (void)setCredential:(nullable NSURLCredential *)value;
// 取消
- (BOOL)cancel:(nullable id)token;
SDWebImageDownloaderOperation

現在開始討論 SDWebImageDownloaderOperation 這個類,下邊是初始化方法:

- (nonnull instancetype)init {
    return [self initWithRequest:nil inSession:nil options:0];
}
// 也是協議方法
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options {
    if ((self = [super init])) {
        _request = [request copy];
        _shouldDecompressImages = YES;
        _options = options;
        _callbackBlocks = [NSMutableArray new];
        _executing = NO;
        _finished = NO;
        _expectedSize = 0;
        _unownedSession = session;
        _callbacksLock = dispatch_semaphore_create(1);
        _coderQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

下面介紹 2 個比較重要的方法:

  • 核心方法 start

這是重寫父類 NSOperation 的 start 方法,添加了自定義的操作。這個方法不需要手動調用,在將 operation 添加到 operationQueue 中的時候,系統會自動調用其 start 方法。重寫后的操作包括以下幾點:

①檢測操作是否已取消,如果取消了,重置數據后直接返回。

        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

②如果需要 App 進入后臺時,繼續執行下載操作,需要開啟后臺任務。并設置 ExpirationHandler,取消下載任務,并結束后臺操作。

        // 如果需要App進入后臺時,繼續執行此操作,需要開啟后臺任務。
        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;
                }
            }];
        }

③獲取或創建 session,然后創建 dataTask,

④若 dataTask 創建成功,啟動下載任務 [self.dataTask resume];,然后執行一次 progressBlock,并發送開始下載的通知。

④若 dataTask 創建失敗,直接調用完成回調,構建 error 信息并返回,然后重置數據。

⑤關閉可能存在的后臺下載任務。

具體下載過程中的操作,都在 session 相關的那些協議方法里邊,詳見代碼注釋,這里就不啰嗦了。

  • 取消操作

最后看一下取消操作,即協議方法 - (BOOL)cancel:(nullable id)token; 的實現,將取消過程中用到的所有方法都展開就是下邊這樣:

- (BOOL)cancel:(nullable id)token {
    BOOL shouldCancel = NO;
    
    LOCK(self.callbacksLock);
    
    // 移除 token,即移除一個存儲著 completionBlock 和 progressBlock 的字典
    [self.callbackBlocks removeObjectIdenticalTo:token];
    
    // 如果已經沒有回調,就去執行整體的 cancel 操作
    if (self.callbackBlocks.count == 0) {
        shouldCancel = YES;
    }
    
    UNLOCK(self.callbacksLock);
    
    if (shouldCancel) {
        // *** 點開 ?
        [self cancel];
    }
    
    return shouldCancel;
}

- (void)cancel {
    @synchronized (self) {
        // *** 點開 ?
        [self cancelInternal];
    }
}

- (void)cancelInternal {
    if (self.isFinished) return;
    [super cancel];

    if (self.dataTask) {
        // 取消下載任務,并發出停止的通知
        [self.dataTask cancel];
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
        });

        // As we cancelled the task, its callback won't be called and thus won't
        // maintain the isFinished and isExecuting flags.
        if (self.isExecuting) self.executing = NO;
        if (!self.isFinished) self.finished = YES;
    }
    // *** 點開 ?
    [self reset];
}

// 重置變量
- (void)reset {
    LOCK(self.callbacksLock);
    [self.callbackBlocks removeAllObjects];
    UNLOCK(self.callbacksLock);
    self.dataTask = nil;
    
    if (self.ownedSession) {
        [self.ownedSession invalidateAndCancel];
        self.ownedSession = nil;
    }
}

小結

SDWebImageDownloader 的內容就先介紹到這類,其他細節見 HHSDWebImageStudy 中的源碼注釋。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373