SDWebImage源碼學習筆記

1. 前言

大名鼎鼎SDWebImage不用多說,相信每一個iOS程序員或多或少都有了解。比如我,之前就大概只知道是個什么東西,基本屬于沒用過的狀態。最近抽空學習了一下源碼,在此記錄下。

GitHub上,SDWebImage描述為Asynchronous image downloader with cache support as a UIImageView category,翻譯成中文是“UIImageView的一個category,支持緩存的異步圖片下載器”。

可以在該鏈接中查看到最新的文檔https://sdwebimage.github.io

本文使用的源碼為SDWebImage 5.0+版本:

2. 架構

在GitHub上,SDWebImage提供了非常詳細的架構圖、類圖和順序圖,其中下圖是整體的架構圖

SDWebImageHighLevelDiagram.jpeg

這個圖中可以看到總體包括以下幾個部分

  • 基礎組件:包括工具類、分類方法、Image Coder(圖片編碼/解碼)、Image Transformer(圖片轉換)
  • 頂層組件
    • Image Manager:負責處理Image Cache(處理圖片緩存和落地)和Image Loader(處理圖片的網絡加載)
    • View Category:提供對外的API接口,圖片加載動畫和轉場動畫等。
    • Image Prefetcher:圖片的預加載器,是相對比較獨立的部分。

可以看到,SDWebImage提供了圖片緩存的能力、網絡加載的能力,還包括一些圖片的處理。

順序圖

SDWebImageSequenceDiagram.jpg

通過順序圖,可以清楚的看到整個接口的調用流程。

  1. 需要加載圖片時,Other Object只需要調用``UIImageView+WebCahce中的sd_setImageWithURL()`方法即可
  2. sd_setImageWithURL()會調用UIVIew+WebCache中的內部加載方法sd_internalSetImageWithURL()
  3. 接下來會調用SDWebImageManagerloadImage()方法,可以看到,主要的邏輯都在這個SDWebImageManager
  4. SDWebImageManager會分別調用SDImageCache加載緩存數據,然后調用SDWebImageDownloader從網絡中加載圖片
  5. 加載完成后,會回調回UIImageView中,設置圖片

對于使用者來說,復雜的邏輯都隱藏在SDWebImageManager之后,還有一些更詳細的類圖,有興趣的可以直接到GitHub的ReadMe去查看。

3. View Category

3.1 WebCache

SDWebImage提供了以下幾個Category可以方便的完成圖片加載

  • UIImageView+HighlightedWebCache
  • UIImageView+WebCache
  • UIButton+WebCache
  • NSButton+WebCache
  • UIView+WebCache

主要的處理邏輯,最終都會調用UIView+WebCache的下述接口:

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                           context:(nullable SDWebImageContext *)context
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDImageLoaderProgressBlock)progressBlock
                         completed:(nullable SDInternalCompletionBlock)completedBlock;

該方法非常長,主要的流程如下:

  1. 取消在進行的operation。
    • operation存儲在由UIView+WebCacheOperation中維護的字典SDOperationsDictionary中,默認使用當前類名作為operation的key,其中value是weak指針,因為該operationSDWebImageManager維護
  2. 若外部沒有設置SDWebImageDelayPlaceholder,則異步在主線程將placeholder設置到UIImageView
  3. 重置記錄進度的NSProgress對象,該對象由當前分類實例維護
  4. 啟動ImageIndicator,默認是一個旋轉菊花,其中iWatch是不支持的
  5. 接下來就是獲取SDWebImageManager了,可以支持外部配置,否則會使用全局唯一的單例
  6. 設置進度的回調SDImageLoaderProgressBlock,該block中,會更新內部的進度條、菊花,然后再回調給外層調用者
  7. 調用SDWebImageManager的加載方法loadImageWithURL:options:context:progress:completed:,啟動圖片的加載流程
  8. 在7中方法的completed回調中,完成進度更新、關閉菊花、回調completedBlock以及設置圖片等操作

4. SDWebImageManager

SDWebImageManager是一個單例類,維護了兩個主要的對象imageCacheimageLoader:

@property (strong, nonatomic, readonly, nonnull) id<SDImageCache> imageCache;
@property (strong, nonatomic, readonly, nonnull) id<SDImageLoader> imageLoader;

4.1 加載圖片前的準備工作

主要接口loadImageWithURL:options:context:progress:completed:的實現邏輯如下:

  1. 兼容邏輯,若傳進來的url是NSString而不是NSURL,則轉換為NSURL
  2. 創建一個新的SDWebImageCombinedOperation
  3. 判斷是否是已經失敗且不需要重試的url或者url無效,直接回調completedBlock返回
  4. operation加入到Set runningOperations
  5. 在執行加載操作前,調用processedResultForURL方法,對url、 optionscontext做一次加工操作
    • 在該方法中,SDWebImageManager設置會判斷是否外部有設置transformer、cacheKeyFiltercacheSerializer
    • 最后,會調用外部配置的optionsProcessor對象的processedResultForURL方法,讓使用者有機會修改上述參數
  6. 調用callCacheProcessForOperation方法,開始從緩存中加載圖片

關鍵代碼,代碼中只保留關鍵邏輯:

- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                          options:(SDWebImageOptions)options
                                          context:(nullable SDWebImageContext *)context
                                         progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                        completed:(nonnull SDInternalCompletionBlock)completedBlock {
    // 1
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }
    // 2
    SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    operation.manager = self;
    // 3
    BOOL isFailedUrl = NO;
    if (url) {
        SD_LOCK(self.failedURLsLock);
        isFailedUrl = [self.failedURLs containsObject:url];
        SD_UNLOCK(self.failedURLsLock);
    }

    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}] url:url];
        return operation;
    }
    // 4
    SD_LOCK(self.runningOperationsLock);
    [self.runningOperations addObject:operation];
    SD_UNLOCK(self.runningOperationsLock);
    // 5
    SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
    // 6
    [self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];

    return operation;
}

4.2 從緩存中加載圖片

接口名稱為callCacheProcessForOperation,該方法中

  1. 判斷context中是否傳入了自定義的SDImageCache,否則使用默認的imageCache
  2. 判斷options是否配置了SDWebImageFromLoaderOnly,該參數表明,是否僅從網絡加載
  3. 若僅從網絡中加載,直接調用callDownloadProcessForOperation接口,開始下載的步驟
  4. 否則,獲取url對應的key,并調用imageCache的接口queryImageForKey,從緩存中加載圖片,在該接口回調中,調用3中的下載接口。

關鍵代碼如下:

- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                 url:(nonnull NSURL *)url
                             options:(SDWebImageOptions)options
                             context:(nullable SDWebImageContext *)context
                            progress:(nullable SDImageLoaderProgressBlock)progressBlock
                           completed:(nullable SDInternalCompletionBlock)completedBlock {
    // 1
    id<SDImageCache> imageCache;
    if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
        imageCache = context[SDWebImageContextImageCache];
    } else {
        imageCache = self.imageCache;
    }
    // 2
    BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
    if (shouldQueryCache) {
        // 4
        NSString *key = [self cacheKeyForURL:url context:context];
        @weakify(operation);
        operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
            @strongify(operation);
            if (!operation || operation.isCancelled) {
                // Image combined operation cancelled by user
                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
                [self safelyRemoveOperationFromRunning:operation];
                return;
            }
            // Continue download process
            [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
        }];
    } else {
        // 3
        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
    }
}

從網絡中加載圖片

接口名為callDownloadProcessForOperation,實現邏輯如下:

  1. 與SDImageCache類似,SDImageLoader也支持外部配置,否則使用默認的imageLoader
  2. 一系列參數判斷,主要為了判斷是否可以下載
  3. 當有圖片時,在該方法中可能會先通過callCompletionBlockForOperation接口,異步回調completedBlock設置已經加載好的圖片
  4. 當判斷可以下載后,會調用imageLoaderrequestImageWithURL接口,啟動下載
  5. requestImageWithURL的回調中,處理一些失敗等異常邏輯。
  6. 若加載成功,則通過callStoreCacheProcessForOperation接口,將下載的圖片緩存到本地
  7. 當不需要下載時,會直接返回,若有緩存則會帶上緩存的圖片。

關鍵代碼:

- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                    url:(nonnull NSURL *)url
                                options:(SDWebImageOptions)options
                                context:(SDWebImageContext *)context
                            cachedImage:(nullable UIImage *)cachedImage
                             cachedData:(nullable NSData *)cachedData
                              cacheType:(SDImageCacheType)cacheType
                               progress:(nullable SDImageLoaderProgressBlock)progressBlock
                              completed:(nullable SDInternalCompletionBlock)completedBlock {
    // 1
    id<SDImageLoader> imageLoader;
    if ([context[SDWebImageContextImageLoader] conformsToProtocol:@protocol(SDImageLoader)]) {
        imageLoader = context[SDWebImageContextImageLoader];
    } else {
        imageLoader = self.imageLoader;
    }
    // 2
    BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly);
    shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);
    shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
    shouldDownload &= [imageLoader canRequestImageForURL:url];
    if (shouldDownload) {
        if (cachedImage && options & SDWebImageRefreshCached) {
            // 3
            [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            // 將cachedImage傳到image loader中用于檢查是否是相同的圖片
            mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
            context = [mutableContext copy];
        }
        // 4
        @weakify(operation);
        operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
            @strongify(operation);
            // 5
            if {
              // 一系列失敗邏輯
            } else {
                // 6
                [self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
            }
        }];
    } else if (cachedImage) { // 7
        [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
        [self safelyRemoveOperationFromRunning:operation];
    } else {
        [self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
        [self safelyRemoveOperationFromRunning:operation];
    }
}

該方法中第6步從網絡拉取成功后,會調用callStoreCacheProcessForOperation方法將圖片緩存到本地,以及通過調用者提供的SDImageTransformer轉換圖片。

緩存圖片

調用者提供兩種自定義操作:

  • 自定義的SDImageTransformer將圖片轉換成另一個圖片
  • 自定義的SDWebImageCacheSerializer將圖片序列化為NSData

具體邏輯如下:

  1. 如果有提供SDWebImageCacheSerializer,則會先調用接口將圖片序列化之后,再調用存儲接口緩存圖片。注意這一步是放在global_queue中執行的,不會阻塞主線程,同時使用autoreleasepool保證NSData能第一時間釋放。
  2. 第1步結束后,調用storeImage接口,通過imageCache對象將圖片緩存到本地。默認該操作是放在imageCache維護的io隊列中執行的。
  3. 最后一步操作,則是調用callTransformProcessForOperation接口,轉換圖片。

關鍵代碼:

- (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                      url:(nonnull NSURL *)url
                                  options:(SDWebImageOptions)options
                                  context:(SDWebImageContext *)context
                          downloadedImage:(nullable UIImage *)downloadedImage
                           downloadedData:(nullable NSData *)downloadedData
                                 finished:(BOOL)finished
                                 progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                completed:(nullable SDInternalCompletionBlock)completedBlock {
    // 默認拉回來的圖片就是originImage,當提供了transformer轉化圖片時,可以選擇將原圖片和轉換后的圖片都緩存起來
    NSString *key = [self cacheKeyForURL:url context:context];
    id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
    id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];

    // 這里會緩存原圖,如果轉換只要完成下載,始終緩存原圖
    if (shouldCacheOriginal) {
        SDImageCacheType targetStoreCacheType = shouldTransformImage ? originalStoreCacheType : storeCacheType;
        if (cacheSerializer && (targetStoreCacheType == SDImageCacheTypeDisk || targetStoreCacheType == SDImageCacheTypeAll)) {
            // 1 放到全局隊列中異步序列化
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                @autoreleasepool {
                    NSData *cacheData = [cacheSerializer cacheDataWithImage:downloadedImage originalData:downloadedData imageURL:url];
                    [self storeImage:downloadedImage imageData:cacheData forKey:key cacheType:targetStoreCacheType options:options context:context completion:^{
                        [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
                    }];
                }
            });
        } else {
            // 2
            [self storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:targetStoreCacheType options:options context:context completion:^{
                [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
            }];
        }
    } else {
        [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
    }
}

轉換圖片

如果外部有設置SDImageTransformer,則會判斷是否需要將轉換后的圖片也緩存起來,關鍵代碼:

- (void)callTransformProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                     url:(nonnull NSURL *)url
                                 options:(SDWebImageOptions)options
                                 context:(SDWebImageContext *)context
                           originalImage:(nullable UIImage *)originalImage
                            originalData:(nullable NSData *)originalData
                                finished:(BOOL)finished
                                progress:(nullable SDImageLoaderProgressBlock)progressBlock
                               completed:(nullable SDInternalCompletionBlock)completedBlock {
    // the target image store cache type

    NSString *key = [self cacheKeyForURL:url context:context];
    id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
    id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
    
    if (shouldTransformImage) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            @autoreleasepool {
                UIImage *transformedImage = [transformer transformedImageWithImage:originalImage forKey:key];
                if (transformedImage && finished) {
                    if (cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) {
                        cacheData = [cacheSerializer cacheDataWithImage:transformedImage originalData:(imageWasTransformed ? nil : originalData) imageURL:url];
                    } else {
                        cacheData = (imageWasTransformed ? nil : originalData);
                    }
                    // keep the original image format and extended data
                    SDImageCopyAssociatedObject(originalImage, transformedImage);
                    [self storeImage:transformedImage imageData:cacheData forKey:cacheKey cacheType:storeCacheType options:options context:context completion:^{
                        [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }];
                } else {
                    [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                }
            }
        });
    } else {
        [self callCompletionBlockForOperation:operation completion:completedBlock image:originalImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
    }
}

加載完成

一切完成后,會通過callCompletionBlockForOperation回調到最外層的調用者。

5. SDImageCache

關于SDIMageCache,可以直接看以下類圖,用協議定義了所有關鍵類,包括SDImageCacheSDMemoryCache、SDDiskCache。

SDWebImageCacheClassDiagram.png

5.1 SDImageCache

  • 持有SDMemoryCacheSDDiskCache,用于從內存和硬盤中加載圖片。可以通過SDImageCacheConfig配置我們自定義實現的Cache
  • 維護了一個io隊列,所有從硬盤中異步讀取內容的操作均通過該io隊列執行
  • 監聽了App進程被系統殺掉和App切換到后臺的通知,清除過期的數據

獲取圖片接口queryCacheOperationForKey

  1. 首先判斷外部是否有傳入transformer對象,若有,則會將key通過SDTransformedKeyForKey接口將keytranformerKey拼接在一起得到新的key

  2. 通過memoryCache從內存中獲取圖片,默認情況下,如果獲取到圖片,則直接返回

  3. 若設置了SDImageCacheQueryMemoryData參數,則仍然從硬盤中加載圖片的data數據。默認異步從硬盤加載,可通過設置參數同步加載

  4. 加載完成后,通過block同步或異步返回

有兩處細節需要注意:

  • 使用@autoreleasepool保證大的內存占用可以快速釋放
  • 異步加載時,使用io隊列。異步回調block時,使用主線程回調

關鍵代碼:

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
    // 1
    id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
    if (transformer) 
        NSString *transformerKey = [transformer transformerKey];
        key = SDTransformedKeyForKey(key, transformerKey);
    }
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    
    if (image) {
          // 處理SDImageCacheDecodeFirstFrameOnly或SDImageCacheMatchAnimatedImageClass的邏輯
    }

    // 2
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryMemoryData));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }
    
    // 3
    NSOperation *operation = [NSOperation new];
    // 檢查是否需要同步查詢disk
    // 1. 內存緩存命中且設置了同步
    // 2. 內存緩存沒有命中但設置了同步讀取硬盤數據
    BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
                                (!image && options & SDImageCacheQueryDiskDataSync));
    void(^queryDiskBlock)(void) =  ^{
        if (operation.isCancelled) {
            if (doneBlock) {
                doneBlock(nil, nil, SDImageCacheTypeNone);
            }
            return;
        }
        
        @autoreleasepool {
            // 從硬盤中加載圖片的data
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            SDImageCacheType cacheType = SDImageCacheTypeNone;
            if (image) { // 內存中已經有圖片,但是需要圖片data
                diskImage = image;
                cacheType = SDImageCacheTypeMemory;
            } else if (diskData) {
                cacheType = SDImageCacheTypeDisk;
                // 將imageData轉換成image
                diskImage = [self diskImageForKey:key data:diskData options:options context:context];
                // 將圖片緩存到內存中
                if (diskImage && self.config.shouldCacheImagesInMemory) {
                    NSUInteger cost = diskImage.sd_memoryCost;
                    [self.memoryCache setObject:diskImage forKey:key cost:cost];
                }
            }
            
            if (doneBlock) {
                if (shouldQueryDiskSync) {
                    doneBlock(diskImage, diskData, cacheType);
                } else {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        doneBlock(diskImage, diskData, cacheType);
                    });
                }
            }
        }
    };
    
    // 4
    if (shouldQueryDiskSync) {
        dispatch_sync(self.ioQueue, queryDiskBlock);
    } else {
        dispatch_async(self.ioQueue, queryDiskBlock);
    }
    
    return operation;
}

存儲圖片接口storeImage

外部可設置不同的SDImageCacheType,決定是否需要緩存到內存以及硬盤中

  1. 內存緩存:根據shouldCacheImagesInMemory接口判斷是否要緩存到內存中

  2. 硬盤緩存:

    1. 首次將圖片轉換為NSData,使用SDAnimatedImage接口或者SDImageCodersManager將圖片轉化為NSData
    2. 通過diskCache存儲NSData到硬盤中
    3. 檢查圖片是否有sd_extendedObject,如果有則也會存儲到硬盤中,使用了NSKeyedArchiversd_extendedObject轉換為NSData
      1. NSKeyedArchiver的在iOS 11上提供了新的接口archivedDataWithRootObject:requiringSecureCoding:error
      2. 這里為了兼容iOS 11以下的系統,使用了舊的接口archivedDataWithRootObject:,通過clang diagnostic ignored "-Wincompatible-pointer-types"屏蔽了方法過期警告;使用try catch捕獲異常
      3. 通過diskCachesetExtendedData將擴展數據存儲到硬盤中

關鍵代碼:

- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
          toMemory:(BOOL)toMemory
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {

    // 1 
    if (toMemory && self.config.shouldCacheImagesInMemory) {
        NSUInteger cost = image.sd_memoryCost;
        [self.memoryCache setObject:image forKey:key cost:cost];
    }
    // 2
    if (toDisk) {
        // 使用iO隊列異步存儲
        dispatch_async(self.ioQueue, ^{
            @autoreleasepool {
                // 2.1
                NSData *data = imageData;
                if (!data && image) {
                    data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
                }
                // 2.2
                [self _storeImageDataToDisk:data forKey:key];
                if (image) {
                    // 2.3
                    id extendedObject = image.sd_extendedObject;
                    if ([extendedObject conformsToProtocol:@protocol(NSCoding)]) {
                        NSData *extendedData;
                        // 2.3.1
                        if (@available(iOS 11, tvOS 11, macOS 10.13, watchOS 4, *)) {
                            NSError *error;
                            extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject requiringSecureCoding:NO error:&error];
                            if (error) {
                                NSLog(@"NSKeyedArchiver archive failed with error: %@", error);
                            }
                        } else {
                            // 2.3.2
                            @try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
                                extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject];
#pragma clang diagnostic pop
                            } @catch (NSException *exception) {
                                NSLog(@"NSKeyedArchiver archive failed with exception: %@", exception);
                            }
                        }
                        if (extendedData) { // 2.3.4
                            [self.diskCache setExtendedData:extendedData forKey:key];
                        }
                    }
                }
            }
            
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    } else {
        if (completionBlock) {
            completionBlock();
        }
    }
}

5.2 SDMemoryCache

SDMemoryCache繼承自NSCache,其內部做了如下一些事情:

持有一個NSMapTableweakCache

weakCachekeystrong類型,valueweak類型,在緩存圖片時,weakCache也會緩存一份圖片的keyvalue

這么做的目的是,當NSCache因內存警告清除了緩存內容后,如果有圖片在App某些地方仍然被引用,那么就可以通過weakCache來快速加入到NSCache中,從而阻止了重復從硬盤中讀取。

weakCacheLock

使用了GCD的dispatch_semaphore_t信號量方式,保證多線程操作weakCache時的安全性。

5.3 SDDiskCache

SDDiskCache內部通過NSFileManager實現了文件的讀寫。值得注意的是

  • 存儲文件到硬盤時,SDDiskCache會將存儲的key轉換成md5值后存入本地。
  • 清理過期數據邏輯,總共分兩個步驟
    • 第一個步驟:根據SDImageCacheConfigExpireType設定的排序依據,刪除超過設定的過期時間的文件。
    • 在遍歷所有文件時,計算當前存儲文件的總大小。
    • 第二個步驟:當存儲的總大小超過設定的總大小時,按照SDImageCacheConfigExpireType設定的時間排序,刪除文件,直到設定大小的1/2為止。
    • 清除文件的時機是在App退出或退到后臺時,由SDImageCache調用。
  • 存儲extendData:使用了系統的庫<sys/xattr.h>,通過setxattr,getxattrremovexattr實現了extendData的設置、讀取、移除操作。

清理過期數據的關鍵代碼:

- (void)removeExpiredData {

    NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
                                               includingPropertiesForKeys:resourceKeys
                                                                  options:NSDirectoryEnumerationSkipsHiddenFiles
                                                             errorHandler:NULL];
    
    NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
    for (NSURL *fileURL in fileEnumerator) {
        
        NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
        // 刪除過期的文件
        NSDate *modifiedDate = resourceValues[cacheContentDateKey];
        if (expirationDate && [[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
            [urlsToDelete addObject:fileURL];
            continue;
        }
        
        // 存儲文件屬性為后邊的文件大小檢查做準備
        NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
        currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
        cacheFiles[fileURL] = resourceValues;
    }
    
    for (NSURL *fileURL in urlsToDelete) {
        [self.fileManager removeItemAtURL:fileURL error:nil];
    }
    
    // 若剩余的文件大小仍然超過了設定的最大值,那么執行第二步步驟。優先刪除更早的文件
    NSUInteger maxDiskSize = self.config.maxDiskSize;
    if (maxDiskSize > 0 && currentCacheSize > maxDiskSize) {
        // 目標是刪除到最大值的一半
        const NSUInteger desiredCacheSize = maxDiskSize / 2;
        // 按時間排序
        NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent                                                 usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                     return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
                                                                 }];
        
        // 刪除文件直到剩余大小是最大值的一半
        for (NSURL *fileURL in sortedFiles) {
            if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
                NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
                NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
                
                if (currentCacheSize < desiredCacheSize) {
                    break;
                }
            }
        }
    }
}

6. SDImageLoader

SDImageLoader的類圖如下,該模塊主要處理網絡請求邏輯。

SDWebImageLoaderClassDiagram.png

6.1 SDWebImageDownloader

SDWebImageDownloaderSDWebImage提供的圖片下載器類,實現了SDImageLoader協議。提供了一些配置參數以及多個下載圖片接口。

@interface SDWebImageDownloader : NSObject

@property (nonatomic, copy, readonly) SDWebImageDownloaderConfig *config;
@property (nonatomic, strong) id<SDWebImageDownloaderRequestModifier> requestModifier;
@property (nonatomic, strong) id<SDWebImageDownloaderResponseModifier> responseModifier;
@property (nonatomic, strong) id<SDWebImageDownloaderDecryptor> decryptor;
@property (nonatomic, readonly) NSURLSessionConfiguration *sessionConfiguration;

- (SDWebImageDownloadToken *)downloadImageWithURL:(NSURL *)url completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

- (SDWebImageDownloadToken *)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

- (SDWebImageDownloadToken *)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options context:(SDWebImageContext *)context progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

#pragma mark - Protocol<SDImageLoader>

- (id<SDWebImageOperation>)requestImageWithURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDImageLoaderCompletedBlock)completedBlock;

一些關鍵的參數如下:

  • downloadQueueNSOperationQueue類型,用于執行每一個下載任務創建的NSOperation;

  • URLOperations:字典類型,key為URL,valueNSOperation<SDWebImageDownloaderOperation>,使用該對象來維護SDWebImageDownloader生命周期內所有網絡請求的Operation對象。

  • session:使用外部或者默認的sessionConfiguration創建的NSURLSession對象。

圖片下載核心流程

核心圖片下載方法為downloadImageWithURL,主要流程如下:

  1. 判斷URLOperations是否已經緩存該url對應的NSOperation<SDWebImageDownloaderOperation>對象

  2. 若已經存在operation,將該方法傳入的progressBlockcompletedBlock加入到operation中,同時若該operation還未被執行時,會根據傳入的options調整當前queue的優先級。

  3. operation不存在、已經完成或者被取消,通過createDownloaderOperationWithUrl方法創建一個新的operation。

  4. operation創建成功,設置completionBlock,添加operationURLOperations中,調用addHandlersForProgress添加progressBlockcompletedBlock,最后,將operation添加到downloadQueue(根據蘋果文檔,在添加operationqueue之前,需要執行完所有配置)。

  5. 最后,創建并返回SDWebImageDownloadToken對象,該對象包含了url、request、以及downloadOperationCancelToken。`

關鍵代碼:

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {

    id downloadOperationCancelToken;
    // 1
    NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
    // 3
    if (!operation || operation.isFinished || operation.isCancelled) {
        operation = [self createDownloaderOperationWithUrl:url options:options context:context];
        @weakify(self);
        operation.completionBlock = ^{
            @strongify(self);
            if (!self) {
                return;
            }
            [self.URLOperations removeObjectForKey:url];
        };
        self.URLOperations[url] = operation;
        // 4
        downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        [self.downloadQueue addOperation:operation];
    } else { // 2
        @synchronized (operation) {
            downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        }
        if (!operation.isExecuting) {
            if (options & SDWebImageDownloaderHighPriority) {
                operation.queuePriority = NSOperationQueuePriorityHigh;
            } else if (options & SDWebImageDownloaderLowPriority) {
                operation.queuePriority = NSOperationQueuePriorityLow;
            } else {
                operation.queuePriority = NSOperationQueuePriorityNormal;
            }
        }
    }
    // 5
    SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
    token.url = url;
    token.request = operation.request;
    token.downloadOperationCancelToken = downloadOperationCancelToken;
    
    return token;
}

SDWebImageDownloadToken實現了SDWebImageOperation協議,對于外部調用者來說,可以通過id<SDWebImageOperation>取消當前操作,定義如下:

@interface SDWebImageDownloadToken : NSObject <SDWebImageOperation>

- (void)cancel;
@property (nonatomic, strong, nullable, readonly) NSURL *url;
@property (nonatomic, strong, nullable, readonly) NSURLRequest *request;
@property (nonatomic, strong, nullable, readonly) NSURLResponse *response;
@property (nonatomic, strong, nullable, readonly) NSURLSessionTaskMetrics *metrics;

@end

創建NSOperation<SDWebImageDownloaderOperation>對象

NSOperation通過createDownloaderOperationWithUrl方法創建,主要流程如下:

  1. 創建NSMutableURLRequest對象,設置緩存策略、是否使用默認Cookies 、Http頭信息等。
  2. 獲取外部配置的SDWebImageDownloaderRequestModifier對象,若沒有則使用self的,通過modifiedRequestWithRequest接口在請求之前有機會檢查并修改一次Request,若返回了nil,本次請求會終止。
  3. 外部同樣可以配置SDWebImageDownloaderResponseModifier對象,用來修改Response,這個會先存儲在context中,等待請求回來后再去調用。
  4. 獲取SDWebImageDownloaderDecryptor對象,同樣是請求回來后,用于解密相關操作。
  5. context參數檢查完畢后,需要創建NSOperation<SDWebImageDownloaderOperation>對象,此處可以通過設置configoperationClass來傳入自定義的類名,若外部沒有傳入,則會使用SDWebImage提供改的SDWebImageDownloaderOperation類。
  6. 設置http請求的證書,首先獲取config中的urlCredential,其次通過config中的usrnamepassword創建NSURLCredential對象。
  7. 設置其他參數,如http請求的證書、最小進度間隔、當前請求的優先級等。
  8. 如果設置了SDWebImageDownloaderLIFOExecutionOrder,表明所有的請求都是LIFO(后進先出)的執行方式,此處的處理方式是遍歷當前downloadQueueoperations,將新的operation設置為所有operations的依賴,代碼如下:

關鍵代碼:

- (nullable NSOperation<SDWebImageDownloaderOperation> *)createDownloaderOperationWithUrl:(nonnull NSURL *)url options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context {
    NSTimeInterval timeoutInterval = self.config.downloadTimeout;
    
    // 1
    NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData; // 默認情況下不使用NSURLCache
    NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
    mutableRequest.HTTPShouldHandleCookies = SD_OPTIONS_CONTAINS(options, SDWebImageDownloaderHandleCookies);
    mutableRequest.HTTPShouldUsePipelining = YES;
    mutableRequest.allHTTPHeaderFields = self.HTTPHeaders;
   
    // 2
    id<SDWebImageDownloaderRequestModifier> requestModifier;
    if ([context valueForKey:SDWebImageContextDownloadRequestModifier]) {
        requestModifier = [context valueForKey:SDWebImageContextDownloadRequestModifier];
    } else {
        requestModifier = self.requestModifier;
    }
    NSURLRequest *request;
    if (requestModifier) {
        NSURLRequest *modifiedRequest = [requestModifier modifiedRequestWithRequest:[mutableRequest copy]];
    } else {
        request = [mutableRequest copy];
    }
    // 3
    id<SDWebImageDownloaderResponseModifier> responseModifier;
    if ([context valueForKey:SDWebImageContextDownloadResponseModifier]) {
        responseModifier = [context valueForKey:SDWebImageContextDownloadResponseModifier];
    } else {
        responseModifier = self.responseModifier;
    }
    if (responseModifier) {
        mutableContext[SDWebImageContextDownloadResponseModifier] = responseModifier;
    }
    // 4
    id<SDWebImageDownloaderDecryptor> decryptor;
    if ([context valueForKey:SDWebImageContextDownloadDecryptor]) {
        decryptor = [context valueForKey:SDWebImageContextDownloadDecryptor];
    } else {
        decryptor = self.decryptor;
    }
    if (decryptor) {
        mutableContext[SDWebImageContextDownloadDecryptor] = decryptor;
    }
    
    context = [mutableContext copy];
    
    // 5
    Class operationClass = self.config.operationClass;
    if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)]) {
    } else {
        operationClass = [SDWebImageDownloaderOperation class];
    }
    NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
    // 6
    if ([operation respondsToSelector:@selector(setCredential:)]) {
        if (self.config.urlCredential) {
            operation.credential = self.config.urlCredential;
        } else if (self.config.username && self.config.password) {
            operation.credential = [NSURLCredential credentialWithUser:self.config.username password:self.config.password persistence:NSURLCredentialPersistenceForSession];
        }
    }
    // 7
    if ([operation respondsToSelector:@selector(setMinimumProgressInterval:)]) {
        operation.minimumProgressInterval = MIN(MAX(self.config.minimumProgressInterval, 0), 1);
    }
    
    if (options & SDWebImageDownloaderHighPriority) {
        operation.queuePriority = NSOperationQueuePriorityHigh;
    } else if (options & SDWebImageDownloaderLowPriority) {
        operation.queuePriority = NSOperationQueuePriorityLow;
    }
    // 8
    if (self.config.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
        for (NSOperation *pendingOperation in self.downloadQueue.operations) {
            [pendingOperation addDependency:operation];
        }
    }
    
    return operation;
}

6.2 SDWebImageDownloaderOperation

在前邊的SDWebImageDownloader初始化時,可以看到創建了NSURLSession對象,且delegate設置的為self,但實際上,當SDWebImageDownloader接收到NSURLSessionTaskDelegate或者NSURLSessionDataDelegate回調時,都會轉發到對應的NSOperation<SDWebImageDownloaderOperation>對象去處理,默認情況,就是SDWebImageDownloaderOperation。來看看這里的主要流程吧。

啟動方法start

  1. 通過beginBackgroundTaskWithExpirationHandler方法申請在進入后臺后,更多的時間執行下載任務。

  2. 判斷session,該類中有兩個session,unownedSession(外部傳入),ownedSession(內部創建),當外部沒有傳入session時,內部則會再創建一個,保證任務可以繼續執行。

  3. 保存緩存數據,如果設置了SDWebImageDownloaderIgnoreCachedResponse時,當拉取回來的數據和已緩存的數據一致,就回調上層nil,這里保存的緩存數據用于拉取結束后的判斷。

  4. 通過dataTaskWithRequest創建NSURLSessionTask對象dataTask。

  5. 設置dataTaskcoderQueue的優先級。

  6. 啟動本次任務,通過progressBlock回調當前進度,這里block可以存儲多個,外部通過addHandlersForProgress方法添加。

  7. 這里還會再在主線程拋一個啟動的通知SDWebImageDownloadStartNotification

關鍵代碼

- (void)start {
        // 1
#if SD_UIKIT
        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:^{
                [wself cancel];
            }];
        }
#endif
        // 2
        NSURLSession *session = self.unownedSession;
        if (!session) {
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                    delegate:self
                                               delegateQueue:nil];
            self.ownedSession = session;
        }
        // 3
        if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
            NSURLCache *URLCache = session.configuration.URLCache;
            if (!URLCache) {
                URLCache = [NSURLCache sharedURLCache];
            }
            NSCachedURLResponse *cachedResponse;
            @synchronized (URLCache) {
                cachedResponse = [URLCache cachedResponseForRequest:self.request];
            }
            if (cachedResponse) {
                self.cachedData = cachedResponse.data;
            }
        }
        // 4
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }
    // 5
    if (self.dataTask) {
        if (self.options & SDWebImageDownloaderHighPriority) {
            self.dataTask.priority = NSURLSessionTaskPriorityHigh;
            self.coderQueue.qualityOfService = NSQualityOfServiceUserInteractive;
        } else if (self.options & SDWebImageDownloaderLowPriority) {
            self.dataTask.priority = NSURLSessionTaskPriorityLow;
            self.coderQueue.qualityOfService = NSQualityOfServiceBackground;
        } else {
            self.dataTask.priority = NSURLSessionTaskPriorityDefault;
            self.coderQueue.qualityOfService = NSQualityOfServiceDefault;
        }
        // 6
        [self.dataTask resume];
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        __block typeof(self) strongSelf = self;
        // 7
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:strongSelf];
        });
    } else {
        [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
        [self done];
    }
}

NSURLSessionTaskDelegateNSURLSessionDataDelegate

SDWebImageDownloaderOperation中,實現了NSURLSessiondelegate回調處理,具體邏輯比較多且不復雜,就不在這里贅述,可自行查閱代碼。

7. 一些細節

7.1 宏定義

多平臺適配

SDWebImage中多處地方使用了平臺宏去區分不同平臺的特性,對于想要了解跨平臺的一些特性,非常有借鑒意義。

// iOS and tvOS are very similar, UIKit exists on both platforms
// Note: watchOS also has UIKit, but it's very limited
#if TARGET_OS_IOS || TARGET_OS_TV
    #define SD_UIKIT 1
#else
    #define SD_UIKIT 0
#endif

#if TARGET_OS_IOS
    #define SD_IOS 1
#else
    #define SD_IOS 0
#endif

#if TARGET_OS_TV
    #define SD_TV 1
#else
    #define SD_TV 0
#endif

#if TARGET_OS_WATCH
    #define SD_WATCH 1
#else
    #define SD_WATCH 0
#endif

以及通過宏將Mac平臺的NSImage聲明為UIImageNSImageView聲明為UIImageView等,讓一套代碼得以方便你的適配多個平臺不同的控件名稱。

#if SD_MAC
    #import <AppKit/AppKit.h>
    #ifndef UIImage
        #define UIImage NSImage
    #endif
    #ifndef UIImageView
        #define UIImageView NSImageView
    #endif
    #ifndef UIView
        #define UIView NSView
    #endif
    #ifndef UIColor
        #define UIColor NSColor
    #endif
#else
    #if SD_UIKIT
        #import <UIKit/UIKit.h>
    #endif
    #if SD_WATCH
        #import <WatchKit/WatchKit.h>
        #ifndef UIView
            #define UIView WKInterfaceObject
        #endif
        #ifndef UIImageView
            #define UIImageView WKInterfaceImage
        #endif
    #endif
#endif

判斷是否主線程dispatch_main_async_safe

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
    if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }
#endif

一般情況下,需要判斷是否在主線程,可能會使用NSThread.isMainThread來判斷,這個就可以滿足大部分的場景了。而SDWebImage的實現有些不一樣,判斷的方式是當前的queue是否是主隊列,并沒有判斷當前的線程。

實際上,主線程和主隊列不完全是一個東西,有微小的區別。主線程上也可以運行其他隊列。

在這篇OpenRadar中有提到,在主線程但非主隊列中調用MKMapViewaddOverlay方法是不安全的。具體可參考下列文章:

7.2 多線程安全

在代碼中有大量的地方使用了鎖去保證多線程安全,包括常見的@synchonzied以及GCD的信號量

#ifndef SD_LOCK
#define SD_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#endif

#ifndef SD_UNLOCK
#define SD_UNLOCK(lock) dispatch_semaphore_signal(lock);
#endif

8. 結語

SDWebImage代碼暫時就講解這么多,不過該庫的功能遠不止于此,非常強大,對于有需要使用的,可以再詳細的去了解具體使用的地方。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。