以不一樣的方式理解SDWebImage

本文由 iMetalk 團(tuán)隊(duì)的成員 Lefe 完成匾寝,主要幫助讀者深入理解一個(gè)第三方庫(kù)搬葬。

本文不會(huì)教你咋么使用SD,而是要告訴你如何讀懂SD艳悔,掌握SD的原理及架構(gòu)急凰。可能猜年,你也看過(guò)別人的對(duì)SD的源碼解析抡锈,不過(guò) Lefe 上網(wǎng)看了一下,大部分都是以一種簡(jiǎn)單的方式介紹SD乔外。本文主要通過(guò)不同的角度來(lái)學(xué)習(xí)SD床三,主要從以下方面著手:

  • 各個(gè)文件的作用是什么
  • SD 使用的知識(shí)點(diǎn)總結(jié)
  • SD 中的思想
  • 時(shí)序圖
  • SD類(lèi)圖
  • 使用實(shí)例
  • 總結(jié)

各個(gè)文件的作用是什么

擴(kuò)展文件( UIView + ... ):

這些文件讓使用者更簡(jiǎn)單的使用,基本是傻瓜式的杨幼,你可以在不懂 SD 的情況下寫(xiě)出高性能的圖片加載勿璃。這就是 SD 的優(yōu)點(diǎn)所在。

  • UIView+WebCache.h

這個(gè)文件可以說(shuō)是其它視圖加載圖片的關(guān)鍵推汽,其它擴(kuò)展是基于 UIView 擴(kuò)展的基礎(chǔ)上,實(shí)現(xiàn)了視圖本身加載圖片的方式歧沪。它和 UIView+WebCacheOperation.h 配合使用歹撒。這個(gè)類(lèi)主要提供了加載圖片的方法和加載圖片時(shí)顯示的 Loading。
加載圖片的方法主要是:

- (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;
                         

這個(gè)方法主要用來(lái)加載圖片诊胞,其實(shí) UIImageViewUIButton 加載圖片時(shí)最終會(huì)調(diào)用這個(gè)方法暖夭。這個(gè)方法會(huì)異步下載圖片并且添加緩存,這樣保證下次直接可以從緩存中讀取圖片撵孤。

參數(shù)說(shuō)明:

url:圖片在服務(wù)器上的路徑迈着;
placeholder:圖片加載時(shí)顯示的默認(rèn)圖;
options:控制圖片的加載方式邪码,關(guān)于更多的 SDWebImageOptions 將在下文講解
operationKey:操作(operation)的 key裕菠,如果為空時(shí),將使用類(lèi)名闭专。這個(gè)主要使用來(lái)取消一個(gè) opetion奴潘,結(jié)合 UIView+WebCacheOperation.h 使用;
setImageBlock:如果不想使用 SD 加載完圖片后顯示到視圖上影钉,可以使用這個(gè) Block 自定義加載圖片画髓,這樣就可以在調(diào)用加載圖片的方法中加載圖片。它的完整定義是:

typedef void(^SDSetImageBlock)(UIImage * _Nullable image, NSData * _Nullable imageData);

progress:進(jìn)度回調(diào)平委,它的完整定義是奈虾,注意這里有一個(gè) targetURL:

typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL);

completed:圖片加載完成后的回調(diào),

typedef void(^SDExternalCompletionBlock)(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL);

這里摘錄一段代碼,簡(jiǎn)單講解一些肉微,以下代碼主要用到的知識(shí)點(diǎn)有:

  • 位運(yùn)算 &
  • 使用 NSOperation 下載圖片
  • 使用 runtime 給擴(kuò)展添加屬性
  • 顯示加載 Loading
// 設(shè)置圖片時(shí)先取消以前的下載任務(wù)匾鸥,這樣避免了復(fù)用圖片錯(cuò)誤問(wèn)題
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            // 設(shè)置默認(rèn)圖
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }
    
    if (url) {
        // check if activityView is enabled or not
        if ([self sd_showActivityIndicatorView]) {
            [self sd_addActivityIndicator];
        }
        
        __weak __typeof(self)wself = self;
        // 加載圖片
        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;
                }
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
                    // 如果是自動(dòng)設(shè)置圖,直接回調(diào)出去
                    completedBlock(image, error, cacheType, url);
                    return;
                } else if (image) {
                    // 設(shè)置圖片
                    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    [sself sd_setNeedsLayout];
                } else {
                    if ((options & SDWebImageDelayPlaceholder)) {
                        // 如果圖片加載失敗浪册,加載默認(rèn)圖
                        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                        [sself sd_setNeedsLayout];
                    }
                }
                // 回調(diào)出去
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        // 保存當(dāng)前運(yùn)行的 operation
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    }

例子主要展示直接使用 UIView 的擴(kuò)展加載圖片扫腺,且使用 setImageBlock 加載圖片。只要理解了這個(gè)方法村象,那么關(guān)于 UIView 加載圖片基本上已經(jīng)掌握了:

[cell.sdimageView sd_internalSetImageWithURL:[NSURL URLWithString:urlStr] placeholderImage:nil options:SDWebImageLowPriority operationKey:nil setImageBlock:^(UIImage * _Nullable image, NSData * _Nullable imageData) {
        cell.sdimageView.image = image;
  } progress:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
            
 }];
        
  • UIView+WebCacheOperation.h

這個(gè)類(lèi)主要用來(lái)記錄 UIView 加載 Operation 操作笆环,大多數(shù)情況下一個(gè) View 僅擁有
一個(gè) Operation ,默認(rèn)的 key 是當(dāng)前類(lèi)的類(lèi)名厚者,如果設(shè)置了不同的 key躁劣,將
保存不同個(gè) Operation 。比如一個(gè) UIButton库菲,可以設(shè)置不同狀態(tài)下的圖片账忘,那么我需要記錄多個(gè) Operation 。它主要采用一個(gè)字典來(lái)保存所有的 Operation 熙宇。

operations = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

取消一個(gè) Operation鳖擒,這里需要注意 SDWebImageOperation。取消當(dāng)前正在進(jìn)行的 Operation烫止。

- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    // Cancel in progress downloader from queue
    SDOperationsDictionary *operationDictionary = [self operationDictionary];
    id operations = operationDictionary[key];
    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];
    }
}

  • UIImageView+WebCache.h
  • UIImageView+HighlightedWebCache.h
  • UIButton+WebCache.h

這幾個(gè)類(lèi)主要是基于以下方法的進(jìn)一步封裝蒋荚,方便實(shí)用,這里就不做介紹了馆蠕。

- (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;
  • UIImage+GIF.h

主要用來(lái)根據(jù) NSData 生成一個(gè) GIF 圖片和一個(gè)判斷是否為 GIF 圖片期升。

  • UIImage+MultiFormat.h

主要用來(lái)根據(jù) NSData 生成不同格式的圖片,這里可能我們需要用到的是互躬,根據(jù) Data 判斷圖片的格式播赁。

下載操作

  • SDWebImageDownloaderOperation:NSOperation
@interface SDWebImageDownloaderOperation : NSOperation <SDWebImageDownloaderOperationInterface, SDWebImageOperation, NSURLSessionTaskDelegate, NSURLSessionDataDelegate>

這個(gè)文件可以說(shuō)是整個(gè) SD 的靈魂,它控制著圖片的下載過(guò)程吼渡,它與 NSOperationQueue 配合使用容为。關(guān)于更多 NSOperation 的介紹,近期會(huì)翻譯一篇文章來(lái)聊一聊 NSOperation寺酪。SDWebImageDownloaderOperationInterface:這是一個(gè)協(xié)議舟奠,可以自定義自己的 NSOperation,只要實(shí)現(xiàn)該協(xié)議中的方法房维,并且繼承自 NSOperation沼瘫。

主要用到的知識(shí)點(diǎn):

  • 使用 NSURLSession 下載
  • dispatch_barrier_async,dispatch_barrier_sync咙俩,dispatch_sync
  • 自定義 NSOperation
  • 網(wǎng)絡(luò)請(qǐng)求認(rèn)證
  • 通知中心
  • 后臺(tái)任務(wù)

初始化:

- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options NS_DESIGNATED_INITIALIZER;

使用這個(gè)方法來(lái)創(chuàng)建一個(gè) SDWebImageDownloaderOperation耿戚,NS_DESIGNATED_INITIALIZER 這個(gè)宏說(shuō)明所有的初始化方法最終都要調(diào)用這個(gè)方法湿故,request 就是網(wǎng)絡(luò)請(qǐng)求的 request,session 當(dāng)前 Operation 所在的 Session膜蛔,options:SDWebImageDownloaderOptions坛猪,如何來(lái)下載任務(wù),有一些枚舉值皂股。

SDWebImageDownloader

這個(gè)類(lèi)主要負(fù)責(zé)下載圖片墅茉,它是一個(gè)單例。它內(nèi)部有 SDWebImageDownloadToken呜呐,用來(lái)標(biāo)示一個(gè)下載任務(wù)就斤,這樣根據(jù) token 來(lái)取消對(duì)應(yīng)的任務(wù)∧⒓可以使用以下方法對(duì) SDWebImageDownloader 進(jìn)行初始化洋机。當(dāng)然如果想使用一個(gè)自定義的 NSURLSessionConfiguration,可以使用下面這個(gè)初始化方法:

- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration NS_DESIGNATED_INITIALIZER;

來(lái)初始化洋魂,下面是它的具體實(shí)現(xiàn):

- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
    if ((self = [super init])) {
        // 下載的 Operation
        _operationClass = [SDWebImageDownloaderOperation class];
        _shouldDecompressImages = YES;
        _executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
        
        // 下載對(duì)列绷旗,最大的并發(fā)數(shù)是6
        _downloadQueue = [NSOperationQueue new];
        _downloadQueue.maxConcurrentOperationCount = 6;
        _downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
        _URLOperations = [NSMutableDictionary new];
        
        // HTTP header
#ifdef SD_WEBP
        _HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
#else
        _HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
        _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
        _downloadTimeout = 15.0;

        // NSURLSession
        sessionConfiguration.timeoutIntervalForRequest = _downloadTimeout;
        self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
                                                     delegate:self
                                                delegateQueue:nil];
    }
    return self;
}

這是 SDWebImageDownloader 最終調(diào)用的初始化方法,主要配置了一些下載必備的數(shù)據(jù)副砍。

下載方法:這個(gè)方法主要用來(lái)下載一個(gè)任務(wù)衔肢,下載任務(wù)使用的是 NSOperation + NSOperationQueue,來(lái)控制下載豁翎。也就是說(shuō)這個(gè)方法主要生產(chǎn)一個(gè) NSOperation 角骤,并添加到 NSOperationQueue 中,這樣 NSOperationQueue 將自動(dòng)管理下載任務(wù)谨垃。使用 NSOperation 的好處就是可以控制下載的整個(gè)過(guò)程,并且不需要管理線程的創(chuàng)建硼控。當(dāng)然它的優(yōu)點(diǎn)也就是它的缺點(diǎn)刘陶,只是使用場(chǎng)景的不同。

url:圖片下載的路徑
options:圖片下載的選項(xiàng)牢撼,它主要有下面這幾種選項(xiàng):

  • SDWebImageDownloaderLowPriority = 1 << 0, 低優(yōu)先級(jí)
  • SDWebImageDownloaderProgressiveDownload = 1 << 1, 漸進(jìn)式的下載匙隔,也就是一塊一塊的下載
  • SDWebImageDownloaderUseNSURLCache = 1 << 2, 默認(rèn)情況不使用 URLCache,它與 NSURLRequestUseProtocolCachePolicy 對(duì)應(yīng)熏版,設(shè)置后使用 URLCache
  • SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
  • SDWebImageDownloaderContinueInBackground = 1 << 4, 后臺(tái)下載任務(wù)
  • SDWebImageDownloaderHandleCookies = 1 << 5, 它與 HTTPShouldHandleCookies 對(duì)應(yīng)
  • SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6, 允許不信任的 SSL 證書(shū)
  • SDWebImageDownloaderHighPriority = 1 << 7, 高優(yōu)先級(jí)下載
  • SDWebImageDownloaderScaleDownLargeImages = 1 << 8, 對(duì)下載后的圖片做處理

progress:進(jìn)度回調(diào)纷责,注意這個(gè)進(jìn)度是在后臺(tái)線程執(zhí)行,刷新 UI 需要回到主線程
completed:下載完成后的回調(diào)
SDWebImageDownloadToken:返回值用這個(gè)來(lái)標(biāo)示一個(gè)下載任務(wù)撼短,取消的時(shí)候使用

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

// block 返回值是 SDWebImageDownloaderOperation再膳,在 block 中創(chuàng)建一個(gè) SDWebImageDownloaderOperation
 
    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;
        }

        // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        if (sself.headersFilter) {
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = sself.HTTPHeaders;
        }
        
        // 創(chuàng)建 SDWebImageDownloaderOperation,創(chuàng)建完成后添加到downloadQueue 中
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        operation.shouldDecompressImages = sself.shouldDecompressImages;
        
        // 處理 HTTP 認(rèn)證的曲横,大多情況不用處理
        if (sself.urlCredential) {
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        // 設(shè)置 Operation 的優(yōu)先級(jí)
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }

        [sself.downloadQueue addOperation:operation];
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }

        return operation;
    }];
}

使用上面這個(gè)方法下載時(shí)喂柒,前提需要了解下面這個(gè)方法的實(shí)現(xiàn)不瓶。它使用一個(gè)字典緩存了所有的下載。使用 SDWebImageDownloadToken 來(lái)標(biāo)記一個(gè)下載任務(wù)灾杰。

@property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations;
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
    // 如果 URL 為空直接回調(diào)蚊丐,并返回
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }

    __block SDWebImageDownloadToken *token = nil;

    dispatch_barrier_sync(self.barrierQueue, ^{
    // 從緩存中取出 Operation
        SDWebImageDownloaderOperation *operation = self.URLOperations[url];
        if (!operation) {
            // 緩存不存在,調(diào)用 Block 創(chuàng)建一個(gè)新的 Operation
            operation = createCallback();
            self.URLOperations[url] = operation;

            __weak SDWebImageDownloaderOperation *woperation = operation;
            operation.completionBlock = ^{
              SDWebImageDownloaderOperation *soperation = woperation;
              if (!soperation) return;
              if (self.URLOperations[url] == soperation) {
                  [self.URLOperations removeObjectForKey:url];
              };
            };
        }
        
        // 創(chuàng)建一個(gè)標(biāo)記艳吠,并添加回調(diào)到緩存
        id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];

        token = [SDWebImageDownloadToken new];
        token.url = url;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
    });

    return token;
}

以上就是下載的主要方法麦备。還有一些設(shè)置屬性,很簡(jiǎn)單昭娩,這里不作介紹凛篙。

緩存 SDImageCache

SD中的緩存主要采用了內(nèi)存緩存(NSCache)加磁盤(pán)緩存(保存到沙河目錄中的 Cache 目錄下),SDImageCacheConfig 主要負(fù)責(zé)配置緩存题禀。

初始化

directory:文件所要保存到沙河目錄鞋诗,默認(rèn)的是 Cache 目錄
ns:文件的域名,最終的路徑為:.../cache/om.hackemist.SDWebImageCache.ns
迈嘹。需要注意的是所有的I/O操作都在一個(gè)串行對(duì)列中執(zhí)行削彬。這里主要用到了文件的一些操作,比如文件大小秀仲,保存文件融痛,文件路徑等。文件保存到沙盒時(shí)主要以文件的下載路徑神僵,MD5后雁刷,加上文件后綴作為文件名,保存到本地和 NSCache 中保礼。

_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory NS_DESIGNATED_INITIALIZER;

它監(jiān)聽(tīng)了3個(gè)通知在初始化的時(shí)候:

  • UIApplicationDidReceiveMemoryWarningNotification:有內(nèi)存警告時(shí)清除所有的緩存
  • UIApplicationWillTerminateNotification:刪除已過(guò)期的文件
  • UIApplicationDidEnterBackgroundNotification:在后臺(tái)刪除已過(guò)期的文件

當(dāng)然可以使用單例初始化沛励,使用默認(rèn)的配置。
+ (nonnull instancetype)sharedImageCache;

SDWebImageManager

主要用來(lái)管理 SDImageCache 和 SDWebImageDownloader炮障。也就是它把緩存和下載結(jié)合起來(lái)目派。

初始化:

這個(gè)方法是 SDWebImageManager 最終的初始化方法,也就是說(shuō)所有的初始化方法最終都會(huì)調(diào)用這個(gè)方法胁赢,方便使用者自定義 SDWebImageManager企蹭,當(dāng)然通常情況下使用單例方法初始化 + (nonnull instancetype)sharedManager;

- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader NS_DESIGNATED_INITIALIZER;
- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
    if ((self = [super init])) {
        _imageCache = cache;
        _imageDownloader = downloader;
        // 下載失敗的 URL 緩存智末,注意它使用的是集合谅摄,這樣保證緩存中沒(méi)有重復(fù)的 URL
        _failedURLs = [NSMutableSet new];
        // 正在運(yùn)行的操作
        _runningOperations = [NSMutableArray new];
    }
    return self;
}

下載一個(gè)圖片的主要方法:

- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock

這里會(huì)將方法分成很多部分來(lái)講:

  • 1.參數(shù)異常判斷,保證程序的健壯性系馆,一個(gè)好的程序送漠,要處理好各種異常情況
// 使用斷言來(lái)保證完成的 Block 不能為空,也就是說(shuō)如果你不需要完成回調(diào)由蘑,直接使用 SDWebImagePrefetcher 就行
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

// 保證 URL 是 NSString 類(lèi)型螺男,轉(zhuǎn)換成 NSURL 類(lèi)型
if ([url isKindOfClass:NSString.class]) {
   url = [NSURL URLWithString:(NSString *)url];
}

// 保證 url 為 NSURL 類(lèi)型
if (![url isKindOfClass:NSURL.class]) {
   url = nil;
}
  • 2.對(duì) url 做異常處理棒厘,是否為不可使用的下載鏈接。SDWebImageCombinedOperation 是一個(gè) NSObeject 對(duì)象下隧。
 __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
 __weak SDWebImageCombinedOperation *weakOperation = operation;

// 判斷是否為下載失敗的 url
BOOL isFailedUrl = NO;
if (url) {
   // 保證線程安全
   @synchronized (self.failedURLs) {
       isFailedUrl = [self.failedURLs containsObject:url];
    }
}

// 如果是失敗的 url 且 operations 不為 SDWebImageRetryFailed奢人,或者 url 為空直接返回錯(cuò)誤
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
        return operation;
}
  • 3.保存當(dāng)前的 Operation 到緩存
@synchronized (self.runningOperations) {
    [self.runningOperations addObject:operation];
}
// 獲取 url 對(duì)應(yīng)的 Key
NSString *key = [self cacheKeyForURL:url];

- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
    if (!url) {
        return @"";
    }
    // typedef NSString * _Nullable (^SDWebImageCacheKeyFilterBlock)(NSURL * _Nullable url);,cacheKeyFilter 是一個(gè) Block淆院,你可以自己設(shè)置 Cache 對(duì)應(yīng)的 key
    if (self.cacheKeyFilter) {
        return self.cacheKeyFilter(url);
    } else {
        return url.absoluteString;
    }
}
    1. 從 Cache 中獲取圖片何乎,它結(jié)合 option,進(jìn)行不同的操作
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock
  • 4-1.如果 Operation 已經(jīng)取消土辩,則移除支救,并結(jié)束程序的執(zhí)行
if (operation.isCancelled) {
    [self safelyRemoveOperationFromRunning:operation];
    return;
}
  • 4-2. 如果未能在緩存中找到圖片,或者強(qiáng)制刷新緩存拷淘,或者代理中未實(shí)現(xiàn)要強(qiáng)制下載圖片各墨,那么它就需要下載圖片。
if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {}

SDWebImageDownloaderOptions 根據(jù)不同的選項(xiàng)做不同的操作启涯,根據(jù) SDWebImageOptions 轉(zhuǎn)換成對(duì)應(yīng)的 SDWebImageDownloaderOptions贬堵。這里需要注意位運(yùn)算,根據(jù)位運(yùn)算可以計(jì)算出不同的選項(xiàng)结洼。那么使用位定義的枚舉和用普通定義的枚舉值有什么優(yōu)缺點(diǎn)黎做?需要讀者考慮。比如下面這兩種定義方法個(gè)的優(yōu)缺點(diǎn)松忍。

SDWebImageDownloaderLowPriority = 1 << 0,

SDWebImageDownloaderLowPriority = 1,
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority)
downloaderOptions |= SDWebImageDownloaderLowPriority;

if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
            
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
            
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
            
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
            
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
            
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
            
if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
            
if (cachedImage && options & SDWebImageRefreshCached) {
  downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
  downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}

使用 imageDownloader 下載圖片蒸殿,下載完成后保存到緩存,并移除 Operation鸣峭。如果發(fā)生錯(cuò)誤宏所,,需要將失敗的 Url 保存到 failedURLs摊溶,避免實(shí)效的 Url 多次下載爬骤。這里需要注意一個(gè) delegate ([self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url]),它需要調(diào)用者自己實(shí)現(xiàn)更扁,這樣緩存中將保存轉(zhuǎn)換后的圖片盖腕。

SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished){

}
  • 4-3. 在緩存中找到圖片赫冬,直接返回

  • 4-4. 圖片不在緩存或者代理中不需要下載的浓镜,直接返回

SDWebImagePrefetcher

它是一個(gè)圖片預(yù)加載的類(lèi),你可以設(shè)置多個(gè) URL劲厌。這種更適合哪些膛薛,在 wifi 情況下提前加載一些圖片,緩存起來(lái)补鼻,用戶使用的時(shí)候哄啄,直接從本地緩存中讀取雅任。它實(shí)現(xiàn)起來(lái)也很簡(jiǎn)單,使用一個(gè)遞歸來(lái)執(zhí)行每一個(gè)下載咨跌。它的本質(zhì)使用的是 SDWebImageManager 處理下載沪么,沒(méi)有使用單例,而新創(chuàng)建一個(gè) manager锌半。

初始化:

 (nonnull instancetype)initWithImageManager:(SDWebImageManager *)manager {
    if ((self = [super init])) {
        _manager = manager;
        _options = SDWebImageLowPriority;
        _prefetcherQueue = dispatch_get_main_queue();
        self.maxConcurrentDownloads = 3;
    }
    return self;
}

SDWebImagePrefetcherDelegate:

每下載完一個(gè)后禽车,走一次回調(diào)

- (void)imagePrefetcher:(nonnull SDWebImagePrefetcher *)imagePrefetcher didPrefetchURL:(nullable NSURL *)imageURL finishedCount:(NSUInteger)finishedCount totalCount:(NSUInteger)totalCount;

所有任務(wù)下載完后,執(zhí)行回調(diào)

- (void)imagePrefetcher:(nonnull SDWebImagePrefetcher *)imagePrefetcher didFinishWithTotalCount:(NSUInteger)totalCount skippedCount:(NSUInteger)skippedCount;

SDWebImageCompat

由于 SD 會(huì)用到不同的平臺(tái)刊殉,需要做一些兼容性的處理殉摔。

NSData+ImageContentType

根據(jù) Data 來(lái)解析圖片的格式

SD 使用的知識(shí)點(diǎn)總結(jié)

  • GCD:

關(guān)于引用一段話:

Dispatch barriers 是一組函數(shù),在并發(fā)隊(duì)列上工作時(shí)扮演一個(gè)串行式的瓶頸记焊。使用 GCD 的障礙(barrier)API 確保提交的 Block 在那個(gè)特定時(shí)間上是指定隊(duì)列上唯一被執(zhí)行的條目逸月。這就意味著所有的先于調(diào)度障礙提交到隊(duì)列的條目必能在這個(gè) Block 執(zhí)行前完成。

// 創(chuàng)建一個(gè)并行隊(duì)列
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationBarrierQueue", DISPATCH_QUEUE_CONCURRENT);

// 添加一個(gè)任務(wù)到對(duì)列中遍膜,使用 dispatch_barrier_async 添加的任務(wù)可以保存后添加
的任務(wù)依賴與前面添加過(guò)的任務(wù)碗硬,也就是說(shuō)如果先前添加的任務(wù)還沒(méi)有執(zhí)行完成,那么后添加
的任務(wù)不會(huì)執(zhí)行捌归,從而保證了線程安全肛响。 
dispatch_barrier_async(self.barrierQueue, ^{
    [self.callbackBlocks addObject:callbacks];
});

// dispatch_sync 保證同步執(zhí)行方法,保證了線程安全
- (nullable NSArray<id> *)callbacksForKey:(NSString *)key {
    __block NSMutableArray<id> *callbacks = nil;
    dispatch_sync(self.barrierQueue, ^{
        callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
        [callbacks removeObjectIdenticalTo:[NSNull null]];
    });
    return [callbacks copy]; 
}

// dispatch_barrier_sync 保證同步執(zhí)行方法惜索,保證了線程安全
- (BOOL)cancel:(nullable id)token {
    __block BOOL shouldCancel = NO;
    dispatch_barrier_sync(self.barrierQueue, ^{
        [self.callbackBlocks removeObjectIdenticalTo:token];
        if (self.callbackBlocks.count == 0) {
            shouldCancel = YES;
        }
    });
    if (shouldCancel) {
        [self cancel];
    }
    return shouldCancel;
}

// 回到主線程
dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});

// SD 的 cache 使用一個(gè)串行對(duì)列特笋,控制線程的訪問(wèn)
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);

  • NSOperation:
    使用 NSOperation 更好的控制一個(gè)邏輯復(fù)雜的操作,可以控制它的整個(gè)操作過(guò)程巾兆,同時(shí)也不需要自己管理和創(chuàng)建線程猎物。關(guān)于自定義 NSOperation,這里不做過(guò)多的解釋角塑。不過(guò)使用 NSOperation 可以做到 Operation 之間的依賴蔫磨,控制隊(duì)列中操作的最大并發(fā)數(shù),取消某個(gè)操作圃伶,而使用 GCD 的話做不到這一點(diǎn)堤如。

  • NSURLSession:
    這是 iOS7 以后網(wǎng)絡(luò)請(qǐng)求類(lèi),它可以支持文件上傳窒朋,文件下載搀罢。

  • 使用 runtime 給某個(gè)已有的類(lèi)添加屬性

static char TAG_ACTIVITY_STYLE;

- (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];
}
  • NSCache:
    內(nèi)存緩存,如同字典一樣很好用侥猩。

SD 中的思想

  • 耦合度低榔至,每個(gè)類(lèi)負(fù)責(zé)不同的操作,相互之間可以獨(dú)立使用
  • 使用擴(kuò)展欺劳,方便使用者
  • 異步下載圖片唧取,并保存到內(nèi)存與磁盤(pán)铅鲤,提高系統(tǒng)性能
  • 保證主線程不被卡頓,提高性能
  • 通過(guò)一個(gè) Manager 來(lái)控制不同的操作

時(shí)序圖

這張流程圖涵蓋了 SD 加載一張圖片時(shí)需要經(jīng)歷的過(guò)程:

流程圖

SD類(lèi)圖

通過(guò)以上的學(xué)習(xí)枫弟,我們可以掌握各個(gè)類(lèi)的作用邢享,那么可以總結(jié)一下這張圖。

  • 所有的操作都圍繞在 SDWebImageManager;
  • SDWebImageManager 中包含了 SDImageCache 和 SDWebImageDownloader,來(lái)處理圖片的下載和緩存艘儒;
  • SDWebImageDownloader 使用 SDWebImageDownloaderOperation 執(zhí)行下載操作;
  • SDImageCache 使用 SDImageConfig 來(lái)配置緩存
  • 從 SDWebImageManager 衍生出一個(gè)預(yù)加載圖片的類(lèi) SDWebImagePrefetcher绪爸,負(fù)責(zé)多個(gè)圖片的預(yù)先加載
  • 底層封裝好通過(guò)擴(kuò)展 UIView 讓視圖可以加載圖片

看懂這張圖需要明白 UML(Unified Modeling Language) 類(lèi)圖:

  • 依賴關(guān)系(dependency):
    依賴關(guān)系是用一套帶箭頭的虛線表示的,UIButton(WebCache) 依賴于 UIView(WebCache)宙攻;

它是一種臨時(shí)性的關(guān)系奠货,通常在運(yùn)行期間產(chǎn)生,并且隨著運(yùn)行時(shí)的變化座掘; 依賴關(guān)系也可能發(fā)生變化.顯然递惋,依賴也有方向,雙向依賴是一種非常糟糕的結(jié)構(gòu)溢陪,我們總是應(yīng)該保持單向依賴萍虽,杜絕雙向依賴的產(chǎn)生;

  • 聚合關(guān)系(aggregation):聚合關(guān)系用一條帶空心菱形箭頭的直線表示形真,聚合關(guān)系用于表示實(shí)體對(duì)象之間的關(guān)系杉编,表示整體由部分構(gòu)成的語(yǔ)義;例如一個(gè)部門(mén)由多個(gè)員工組成咆霜;SDWebImagePrefetcher 由 SDWebImageManager 組成邓馒;

  • 實(shí)現(xiàn)關(guān)系(realize):實(shí)現(xiàn)關(guān)系用一條帶空心箭頭的虛線表示;比如 SDWebImageDownloaderOperation 實(shí)現(xiàn)了協(xié)議 SDWebImageOperation

  • 泛化關(guān)系(generalization):泛化關(guān)系用一條帶空心箭頭的實(shí)線表示蛾坯,它是一種繼承關(guān)系光酣。

整體架構(gòu)

使用實(shí)例

  • 實(shí)例一:使用 UIView 的擴(kuò)展加載圖片,并外部自動(dòng)設(shè)置圖片
[cell.sdimageView sd_internalSetImageWithURL:[NSURL URLWithString:urlStr] placeholderImage:nil options:SDWebImageLowPriority operationKey:nil setImageBlock:^(UIImage * _Nullable image, NSData * _Nullable imageData) {
     cell.sdimageView.image = image;
} progress:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
            
}];
  • 實(shí)例二:預(yù)加載圖片
[SDWebImagePrefetcher sharedImagePrefetcher].delegate = self;
    [[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:resultUrl progress:^(NSUInteger noOfFinishedUrls, NSUInteger noOfTotalUrls) {
        
} completed:^(NSUInteger noOfFinishedUrls, NSUInteger noOfSkippedUrls) {
        
}];

總結(jié)

通過(guò) SD 的深入學(xué)習(xí)脉课,讓我了解到一個(gè)好的開(kāi)源庫(kù)中使用的思想救军,深有體會(huì),建議讀者也可以嘗試詳細(xì)讀一個(gè)開(kāi)源庫(kù)倘零。在讀 SD 的時(shí)候唱遭,需要把自己不懂的知識(shí)點(diǎn),通過(guò)其它資料來(lái)掌握视事,這個(gè)過(guò)程收獲很大胆萧。前后大約花費(fèi)了一周的時(shí)間(每天 1小時(shí) 30 分庆揩,大約)俐东,完成了這篇博客跌穗,如果有什么不合理的地方,讀者可以指出虏辫。深知寫(xiě)博客需要一個(gè)長(zhǎng)期堅(jiān)持的過(guò)程蚌吸,而付出很多自由的時(shí)間。所以我在看別人的博客時(shí)會(huì)特別認(rèn)真的融入作者當(dāng)時(shí)的思想中砌庄。那么 SD 中的思想究竟如何運(yùn)用到我們的項(xiàng)目中呢羹唠?lefe 建議讀者可以從以下方面入手:

  • 解耦:模塊之間一定不要有太多的關(guān)聯(lián),我們往往對(duì)項(xiàng)目中的某個(gè)類(lèi)做增量操作娄昆,不斷的給某個(gè)類(lèi)添加新的代碼佩微,導(dǎo)致這個(gè)類(lèi)越來(lái)越重,我們?cè)囍岩粋€(gè)類(lèi)拆分為不同的功能模塊萌焰;
  • 思路明確:從圖片的下載到圖片顯示到視圖上哺眯,要有明確的思路,先有一個(gè)大致的流程扒俯,然后逐步細(xì)化奶卓,逐步實(shí)現(xiàn);
  • 層次明確:應(yīng)用層的使用不會(huì)印象到底層的設(shè)計(jì)撼玄;
  • GCD 和 NSOperation: 各有利弊夺姑,要合理的使用;
  • 注意性能:一定要注意性能掌猛,結(jié)合多線程盏浙,提升性能,比如 SD 讀取文件時(shí)會(huì)在一條線程中讀壤蟛纭只盹;
  • 方便使用者:寫(xiě)三方庫(kù)時(shí),要讓用戶使用起來(lái)超級(jí)方便兔院,比如在自己項(xiàng)目中寫(xiě)項(xiàng)目組中公用的模塊時(shí)殖卑,要有明確的注釋?zhuān)屖褂眠@更方便的使用;

參考

GCD

時(shí)序圖

類(lèi)圖

如果您想第一時(shí)間看到我們的文章坊萝,歡迎關(guān)注公眾號(hào)孵稽。


微信公眾號(hào)

===== 我是有底線的 ======
喜歡我的文章,歡迎關(guān)注我的新浪微博 Lefe_x十偶,我會(huì)不定期的分享一些開(kāi)發(fā)技巧

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末菩鲜,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子惦积,更是在濱河造成了極大的恐慌接校,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蛛勉,居然都是意外死亡鹿寻,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)诽凌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)毡熏,“玉大人,你說(shuō)我怎么就攤上這事侣诵×》ǎ” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵杜顺,是天一觀的道長(zhǎng)财搁。 經(jīng)常有香客問(wèn)我,道長(zhǎng)躬络,這世上最難降的妖魔是什么妇拯? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮洗鸵,結(jié)果婚禮上越锈,老公的妹妹穿的比我還像新娘。我一直安慰自己膘滨,他們只是感情好甘凭,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著火邓,像睡著了一般丹弱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上铲咨,一...
    開(kāi)封第一講書(shū)人閱讀 49,079評(píng)論 1 285
  • 那天躲胳,我揣著相機(jī)與錄音,去河邊找鬼纤勒。 笑死坯苹,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的摇天。 我是一名探鬼主播粹湃,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼泉坐!你這毒婦竟也來(lái)了为鳄?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤腕让,失蹤者是張志新(化名)和其女友劉穎孤钦,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡偏形,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年静袖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片壳猜。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖滑凉,靈堂內(nèi)的尸體忽然破棺而出统扳,到底是詐尸還是另有隱情,我是刑警寧澤畅姊,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布咒钟,位于F島的核電站,受9級(jí)特大地震影響若未,放射性物質(zhì)發(fā)生泄漏朱嘴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一粗合、第九天 我趴在偏房一處隱蔽的房頂上張望萍嬉。 院中可真熱鬧,春花似錦隙疚、人聲如沸壤追。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)行冰。三九已至,卻和暖如春伶丐,著一層夾襖步出監(jiān)牢的瞬間悼做,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工哗魂, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留肛走,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓录别,卻偏偏與公主長(zhǎng)得像羹与,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子庶灿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容