SDWebImage(v4.4.2)源碼學(xué)習(xí)及知識點分析

SDWebImage這個第三方庫有多厲害账千,從它的GitHub上過萬的Star就可以看出來贝次。一直以來都想好好拜讀它的源碼成洗,但之前每次都看得頭昏腦脹的五督,最后都是不了了之。方知武俠小說中修為沒到瓶殃,強練絕世秘籍會導(dǎo)致走火入魔的說法并不是無稽之談充包。

害怕.jpg

最近項目沒有這么緊張,又靜下心來遥椿,好好研讀了幾遍基矮。終于看出了一點點門道,所以寫篇筆記記錄一下冠场。話不多說愈捅,進入正題。先來一張流程圖壓壓驚:

流程圖.png

本文采用講解主體邏輯慈鸠,貼出源碼蓝谨,并在源碼中添加注釋的方法;同時會把比較有特色的點青团,結(jié)合自己的理解譬巫,稍作分析。作為iOS碼農(nóng)界的小學(xué)生督笆,能力有限芦昔,水平一般(腦補郭德綱的聲音。娃肿。咕缎。)。如有不對之處料扰,還望指正凭豪。


為已有的類添加方法,毫無疑問應(yīng)該首先想到類別(Category)這種方法晒杈。那直接進入到UIImageView+WebCache.m文件中嫂伞,看到一系列的方法,其實最終都是走到了UIView+WebCache.m的的這個方法中:

- (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
                           context:(nullable NSDictionary<NSString *, id> *)context;

為什么要這么設(shè)計呢拯钻?因為前面流程圖上說了帖努,不光UIImageView有擴展,UIButton也有擴展方法粪般,那么把最終的實現(xiàn)放到他們的共同父類UIView的類別中拼余,也就順理成章了。方法實現(xiàn)中亩歹,一進來是這么兩行代碼:

NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];

這個validOperationKey其實就是為了緩存或者從緩存中查找operation當(dāng)作key用的匙监;如果外部沒有傳入的話寡润,就默認(rèn)的取類名。先用這個key取消可能正在操作的operation舅柜,避免后續(xù)的回調(diào)混亂,保證這個Imageview或者Button只存在一個請求圖片的操作躲惰。進入sd_cancelImageLoadOperationWithKey:方法的內(nèi)部致份,可以看到是在UIView+WebCacheOperation.m中實現(xiàn)的。這里將key和operation映射保存在動態(tài)綁定的SDOperationsDictionary中础拨,名字是dictionary氮块,實際上用到的是NSMapTable。類似于字典诡宗,但比字典更靈活的一個類滔蝉。這篇文章說的比較詳細。
接下來是動態(tài)綁定url到當(dāng)前對象上塔沃;如果設(shè)置的options不是延遲設(shè)置占位圖的話蝠引,就在主線程回調(diào)設(shè)置占位圖:

//動態(tài)綁定url到當(dāng)前對象上
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//如果設(shè)置的options不是延遲設(shè)置占位圖的話,就在主線程回調(diào)設(shè)置占位圖
if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }

接下來的這個最外層的if else的大邏輯就是蛀柴,如果傳入的url不為nil的話螃概,就走下面一大段邏輯;否則發(fā)起錯誤回調(diào)鸽疾〉跬荩看看url不為空后續(xù)的邏輯,對照著注釋應(yīng)該比較清楚:

#if SD_UIKIT
        // 檢查activityView是否可用
        if ([self sd_showActivityIndicatorView]) {
            //添加菊花控件
            [self sd_addActivityIndicator];
        }
#endif
        
        // 初始化sd_imageProgress的總?cè)蝿?wù)數(shù)和任務(wù)完成數(shù)
        self.sd_imageProgress.totalUnitCount = 0;
        self.sd_imageProgress.completedUnitCount = 0;
        
        //取到manager制肮,如果外部傳入了就用傳入的值
        SDWebImageManager *manager;
        if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
            manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
        } else {
            manager = [SDWebImageManager sharedManager];
        }
        
        //弱引用self冒窍,防止引用循環(huán)
        __weak __typeof(self)wself = self;
        //把傳進來的progressBlock封裝一下,后續(xù)生成operation時使用
        SDWebImageDownloaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            wself.sd_imageProgress.totalUnitCount = expectedSize;
            wself.sd_imageProgress.completedUnitCount = receivedSize;
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };

其中的SD_UIKIT宏定義后面會頻繁出現(xiàn)豺鼻,其實就類似一個bool值综液,在iOS和tvOS中為真,其他系統(tǒng)下為假:

#if TARGET_OS_IOS || TARGET_OS_TV
    #define SD_UIKIT 1
#else
    #define SD_UIKIT 0
#endif

然后是生成operation的實現(xiàn):

id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            __strong __typeof (wself) sself = wself;
            if (!sself) { return; }
#if SD_UIKIT
            [sself sd_removeActivityIndicator];
#endif
...

代碼太長了沒有截完儒飒,大體邏輯就是manager使用傳進來的url意乓,options,completedBlock约素,和上面生成的combinedProgressBlock參數(shù)届良,生成一個operation,并把它和key映射保存在動態(tài)綁定的SDOperationsDictionary中圣猎。我們不管生成operation的細節(jié)士葫,先來看看它的回調(diào)block實現(xiàn)。

block一進來是對weakSelf的強引用送悔,因為前面對self進行了弱引用慢显。這里說個比較有意思的點爪模。以前最開始看到block實現(xiàn)中這種先weak再strong的做法我其實非常不理解,疑惑的是這樣做到底會不會增加該對象的引用計數(shù)荚藻?如果會的話這跟不做轉(zhuǎn)換有什么區(qū)別屋灌?當(dāng)時百度google了一大堆,硬是沒有看懂应狱,所以這個問題拖了很久共郭,面試還被問到過。后來有一次突然看到一篇文章里面說疾呻,block里面的strong引用weakSelf除嘹,是為了防止多線程切換的時候,weakSelf被提前釋放了岸蜗,后續(xù)再訪問該對象的時候尉咕,引起野指針崩潰才這么做的。我突然豁然開朗璃岳,原來先weak后strong的目的其實有兩個:

  • 先weak引用當(dāng)前對象年缎,是為了讓block捕獲的對象是一個弱引用的對象。這樣就打破了引用循環(huán)铃慷,防止雙方都強引用對方晦款,形成引用循環(huán),導(dǎo)致內(nèi)存泄漏枚冗。
  • block內(nèi)部的strong是為了增加對象的引用計數(shù)缓溅,保證該對象在block內(nèi)部是一直存在的,防止在多線程切換的時候?qū)ο蟊惶崆搬尫帕尬拢罄m(xù)訪問導(dǎo)致野指針崩潰坛怪。

有時候不得不感嘆,能夠在特定的時間節(jié)點碰到對的人或物股囊,是多么幸運的一件事情袜匿。

好了,我們繼續(xù)來看回調(diào)block的實現(xiàn)細節(jié),主要是一些條件判斷和回調(diào)處理稚疹,沒有什么值得特別說明的居灯,對照注釋應(yīng)該是挺好理解的:

#if SD_UIKIT
            //如果前面添加了菊花控件,這里先移除
            [sself sd_removeActivityIndicator];
#endif
            // 如果操作完成内狗,沒有錯誤怪嫌,而且progress沒有更新的話,手動將其置為Unknown狀態(tài)柳沙。
            if (finished && !error && sself.sd_imageProgress.totalUnitCount == 0 && sself.sd_imageProgress.completedUnitCount == 0) {
                sself.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
                sself.sd_imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
            }
            //如果已經(jīng)完成岩灭,或者options設(shè)置了不自動賦值圖片選項的話,就需要執(zhí)行完成回調(diào)
            BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
            //如果(圖片存在 且 options設(shè)置了不自動賦值圖片這個選項的話) 或者 (圖片不存在 且 options沒有設(shè)置延遲顯示placeholder圖片選項)的話赂鲤,就不要將回調(diào)中的image設(shè)置給當(dāng)前控件噪径。
            BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                      (!image && !(options & SDWebImageDelayPlaceholder)));
            //為callCompletedBlockClojure賦值
            SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
                if (!sself) { return; }
                if (!shouldNotSetImage) {
                    [sself sd_setNeedsLayout];
                }
                if (completedBlock && shouldCallCompletedBlock) {
                    completedBlock(image, error, cacheType, url);
                }
            };
            
            if (shouldNotSetImage) {
                dispatch_main_async_safe(callCompletedBlockClojure);
                return;
            }
            
            UIImage *targetImage = nil;
            NSData *targetData = nil;
            if (image) {
                targetImage = image;
                targetData = data;
            } else if (options & SDWebImageDelayPlaceholder) {
                targetImage = placeholder;
                targetData = nil;
            }
            
#if SD_UIKIT || SD_MAC
            // 檢查是否需要執(zhí)行圖片轉(zhuǎn)換
            SDWebImageTransition *transition = nil;
            //如果已經(jīng)完成 且 (options設(shè)置了強制轉(zhuǎn)換選項 或者 緩存類型為SDImageCacheTypeNone柱恤,即沒有命中緩存,是從網(wǎng)絡(luò)獲取的圖片)的話找爱,就取UIView+WebCache.h頭文件中定義的sd_imageTransition轉(zhuǎn)換策略梗顺。其實是提供了自定義圖片轉(zhuǎn)換的功能,SD默認(rèn)這個屬性是nil车摄。
            if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
                transition = sself.sd_imageTransition;
            }
#endif
            //不同宏定義下執(zhí)行響應(yīng)的設(shè)置方法
            dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
                [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
                [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
#endif
                callCompletedBlockClojure();
            });

說完了block回調(diào)寺谤,我們進入manager生成operation的方法中一探究竟,即這個方法:

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

這個方法的實現(xiàn)代碼比較多,我們先把它拆分成以下幾個點來逐一研究:

  • url參數(shù)判斷轉(zhuǎn)換
  • 判斷緩存策略练般,生成operation的緩存查詢操作
  • 在operation的緩存查詢回調(diào)中看是否需要下載

看一下第一部分,我在代碼中加了注釋锈候,也是比較好理解的:

    //先是一個斷言薄料,指明完成回調(diào)是必要的參數(shù),否則調(diào)用這個方法是沒有意義的
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

    // 防止常見的把NSString當(dāng)作NSURL傳入的錯誤
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }
    
    SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    //operation引用了manager泵琳,方便它內(nèi)部使用摄职。注意operation的manager屬性是弱引用,防止引用循環(huán)获列。
    operation.manager = self;

    //查詢是否是之前請求過的但是失敗了的url谷市,這里用了信號量做鎖,下面會詳細說一下
    BOOL isFailedUrl = NO;
    if (url) {
        LOCK(self.failedURLsLock);
        isFailedUrl = [self.failedURLs containsObject:url];
        UNLOCK(self.failedURLsLock);
    }

    //如果url長度為空 或者 (options沒有設(shè)置失敗后重試選項 且 是之前請求失敗的url) 的話击孩,調(diào)用完成回調(diào)迫悠,回傳error。callCompletionBlockForOperation:方法其實沒做什么事巩梢,最終使用的是一個安全調(diào)用block的宏创泄。
    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;
    }

這里比較有意思的是橙喘,查詢是否是之前請求過的失敗url時抑淫,使用了信號量加鎖∠臃停縱觀整個SD忌警,很多地方都使用了這種方法替代互斥鎖來保證線程安全搁拙。把信號量當(dāng)做鎖其實用法也比較簡單,看manager 的 init 方法里法绵,初始化了兩個信號量箕速,都是當(dāng)做鎖來用的。

- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
    if ((self = [super init])) {
        _imageCache = cache;
        _imageDownloader = downloader;
        _failedURLs = [NSMutableSet new];
        _failedURLsLock = dispatch_semaphore_create(1);
        _runningOperations = [NSMutableSet new];
        _runningOperationsLock = dispatch_semaphore_create(1);
    }
    return self;
}
//再看LOCK 和 UNLOCK是什么
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);

就是初始化一個為1的dispatch_semaphore_t變量朋譬,當(dāng)前operation搶占到資源時弧满,先調(diào)用dispatch_semaphore_wait方法將信號量減1,此時dispatch_semaphore_t變量為零此熬,如果再有其他operation想要獲取該變量庭呜,就只能排隊等著滑进,啥時候前一個operation跑完了dispatch_semaphore_signal方法,將信號量加了1募谎。后面的operation才能獲取到該信號量進行下一步扶关。當(dāng)然信號量是一種比較底層的同步機制,不光是當(dāng)鎖用這么簡單数冬。這篇文章有各種鎖的說明和比較节槐。

接下來是判斷緩存策略,生成operation的緩存查詢操作:

    //先將當(dāng)前operation添加到正在進行的所有operation的無序集合中拐纱,也用到了前面說的信號量加鎖
    LOCK(self.runningOperationsLock);
    [self.runningOperations addObject:operation];
    UNLOCK(self.runningOperationsLock);
    //生成后續(xù)查詢和存儲的key铜异,如果用戶自定義了生成key的方法,SD就使用用戶自定義的秸架,否則默認(rèn)為url的absoluteString
    NSString *key = [self cacheKeyForURL:url];
    //獲取緩存options
    SDImageCacheOptions cacheOptions = 0;
    if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
    if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
    if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
    //弱引用當(dāng)前operation
    __weak SDWebImageCombinedOperation *weakOperation = operation;
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        __strong __typeof(weakOperation) strongOperation = weakOperation;
        //如果當(dāng)前operation不存在或者已經(jīng)被取消揍庄,則將其從正在進行的operations無序集合中安全的移除,也用到信號量加鎖
        if (!strongOperation || strongOperation.isCancelled) {
            [self safelyRemoveOperationFromRunning:strongOperation];
            return;
        }
        
        // 如果 (options沒有設(shè)置只從緩存中獲取圖片選項) 且 (沒有命中緩存 或者 options設(shè)置了刷新緩存選項) 且 (manager的delegate沒有實現(xiàn)imageManager:shouldDownloadImageForURL代理方法 或者 實現(xiàn)了該方法东抹,返回YES) 的話蚂子,就需要下載圖片
        BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
            && (!cachedImage || options & SDWebImageRefreshCached)
            && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
        if (shouldDownload) {
            if (cachedImage && options & SDWebImageRefreshCached) {
                // 如果命中了緩存,且options設(shè)置了刷新緩存選項缭黔,那么先執(zhí)行完成回調(diào)食茎,再去請求圖片,以刷新NSURLCache的緩存
                [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }

            // 設(shè)置后續(xù)downloadToken的options
            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;
            }
            
            //設(shè)置operation的downloadToken
            __weak typeof(strongOperation) weakSubOperation = strongOperation;
            strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {   
            xxx(代碼沒截完)

這段實現(xiàn)的大體邏輯就是馏谨,生成operation以后别渔,設(shè)置它的緩存查詢,在緩存查詢的回調(diào)中檢查是否需要下載圖片惧互,如果需要的話钠糊,再設(shè)置它的downloadToken。也就是先查詢緩存壹哺,如果沒有命中或者設(shè)置了刷新緩存選項的話抄伍,就去下載圖片。那么我們SDImageCache這個工具類中管宵,緩存查詢方法是怎么實現(xiàn)的截珍,對照著注釋看應(yīng)該是比較清楚了:

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }
    
    // 先從內(nèi)存緩存中查詢圖片,即從SDImageCache的SDMemoryCache類型的memCache屬性中查詢箩朴,SDMemoryCache繼承自NSCache岗喉,我記得比較早的SD版本,memCache使用的是NSDictionary炸庞。NSCache對比字典有什么優(yōu)勢呢钱床?主要有兩點,一是NSCache是線程安全的埠居,另一個是NSCache在內(nèi)存緊張時查牌,會自動清理部分無用數(shù)據(jù)事期。
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    //如果內(nèi)存中命中了圖片,且options沒有設(shè)置內(nèi)存中有數(shù)據(jù)仍舊查詢磁盤緩存的選項的話纸颜,就直接執(zhí)行完成回調(diào)兽泣,并且返回nil。
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }
    //如果需要進入磁盤查詢胁孙,先設(shè)置好它的回調(diào)block
    NSOperation *operation = [NSOperation new];
    void(^queryDiskBlock)(void) =  ^{
        if (operation.isCancelled) {
            // do not call the completion if cancelled
            return;
        }
        
        @autoreleasepool {
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            SDImageCacheType cacheType = SDImageCacheTypeDisk;
            if (image) {
                // 圖片是從內(nèi)存緩存中命中的
                diskImage = image;
                cacheType = SDImageCacheTypeMemory;
            } else if (diskData) {
                // 圖片是從磁盤緩存中命中的
                diskImage = [self diskImageForKey:key data:diskData options:options];
                if (diskImage && self.config.shouldCacheImagesInMemory) {
                    //如果SDImageCache的config設(shè)置了shouldCacheImagesInMemory屬性唠倦,那么將從磁盤命中的圖片保存到內(nèi)存中,方便下次使用涮较。SD默認(rèn)將該屬性置為YES
                    NSUInteger cost = SDCacheCostForImage(diskImage);
                    [self.memCache setObject:diskImage forKey:key cost:cost];
                }
            }
            //如果options設(shè)置的是同步查詢稠鼻,就直接執(zhí)行完成回調(diào);否則狂票,將回調(diào)異步提交到主隊列候齿。
            if (doneBlock) {
                if (options & SDImageCacheQueryDiskSync) {
                    doneBlock(diskImage, diskData, cacheType);
                } else {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        doneBlock(diskImage, diskData, cacheType);
                    });
                }
            }
        }
    };
    
    if (options & SDImageCacheQueryDiskSync) {
        //如果options設(shè)置的是同步查詢,就直接執(zhí)行queryDiskBlock
        queryDiskBlock();
    } else {
        //否則苫亦,將block提交到自己的IO隊列毛肋,SDImageCache初始化時將該隊列指定為了串行怨咪,只能一個接一個的執(zhí)行回調(diào)
        dispatch_async(self.ioQueue, queryDiskBlock);
    }
    
    return operation;
}

可以看到SD使用的是內(nèi)存和磁盤的二級緩存屋剑,先查詢內(nèi)存,如果命中就直接返回诗眨,沒有命中的話再查詢磁盤緩存唉匾;如果磁盤緩存命中,默認(rèn)會將圖片設(shè)置到內(nèi)存緩存中匠楚,方便下次使用巍膘。同時回調(diào)有同步和異步兩種選擇。緩存查詢這塊的大體邏輯已經(jīng)講完了芋簿,我們順便來看看SDImageCache類中峡懈,關(guān)于緩存清理這部分的實現(xiàn)邏輯。它在初始化的時候就注冊了App將要銷毀和進入后臺的通知与斤,接到通知以后會自動清理內(nèi)存,調(diào)用刪除文件的方法:

- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];

        // 確定文件的查詢鍵肪康,是AccessDate 還是ModificationDate
        NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
        switch (self.config.diskCacheExpireType) {
            case SDImageCacheConfigExpireTypeAccessDate:
                cacheContentDateKey = NSURLContentAccessDateKey;
                break;

            case SDImageCacheConfigExpireTypeModificationDate:
                cacheContentDateKey = NSURLContentModificationDateKey;
                break;

            default:
                break;
        }
        //查詢?nèi)齻€信息,是否是文件夾撩穿,存入緩存的時間磷支,文件大小
        NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];

        // 枚舉當(dāng)前路徑下的所有文件
        NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];

        //過期時間,SD默認(rèn)的文件過期時間是一個星期食寡,如果想自定義的話雾狈,可以在SDImageCacheConfig中修改
        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
        NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
        NSUInteger currentCacheSize = 0;

        // 這里的for循環(huán)有兩個目的
        //  1. 將每個過期文件的URL添加到urlsToDelete數(shù)組中,后續(xù)統(tǒng)一移除對應(yīng)的文件
        //  2. 將每個文件的信息跟URL對應(yīng)抵皱,存到cacheFiles字典中善榛,后面會用到
        NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSError *error;
            NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];

            if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }

            NSDate *modifiedDate = resourceValues[cacheContentDateKey];
            if ([[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }
            
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
            cacheFiles[fileURL] = resourceValues;
        }
        //移除對應(yīng)的文件
        for (NSURL *fileURL in urlsToDelete) {
            [self.fileManager removeItemAtURL:fileURL error:nil];
        }

        // 如果用戶設(shè)置了最大磁盤緩存尺寸辩蛋,且當(dāng)前緩存尺寸超過了設(shè)置的最大值。注意SD默認(rèn)是沒有設(shè)置最大磁盤緩存的锭弊。
        if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
            // 設(shè)置此次的目標(biāo)尺寸為最大尺寸的一半
            const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;

            // 根據(jù)文件修改時間將其排序堪澎,最老的文件在最前面,也就是說最先刪除
            NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                     usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                         return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                                     }];

            // Delete files until we fall below our desired cache size.
            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;
                    }
                }
            }
        }
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

所以每次App進入后臺味滞,SD都會檢查樱蛤,如果磁盤中存在過期文件則刪除;同時如果用戶設(shè)置了最大磁盤緩存尺寸剑鞍,且已經(jīng)使用的磁盤大小超過了這個閾值昨凡,會以最大值的一半作為此次清理的目標(biāo),從最老的文件開始刪蚁署,直到達到目標(biāo)尺寸便脊。
說完緩存查詢這部分,我們回到operation生成downloadToken這里光戈。其實調(diào)用的是SDWebImageDownloader這個工具類來生成downloadToken:

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

該方法中最主要的是實現(xiàn)了名叫createCallback的回調(diào)block哪痰,因為其實一個URL對應(yīng)一個下載操作,如果多個控件使用了同一個URL下載久妆,是沒有必要下載多次的晌杰。所以SD會在addProgressCallback:completedBlock:forURL:createCallback:方法中判斷是否已經(jīng)存在了當(dāng)前URL對應(yīng)的下載操作,不存在的話再調(diào)用createCallback創(chuàng)建筷弦。創(chuàng)建下載操作其實就是按部就班的設(shè)置超時時間(SD默認(rèn)15秒)肋演、根據(jù)緩存策略生成request、設(shè)置request的頭信息烂琴;然后根據(jù)request創(chuàng)建對應(yīng)的SDWebImageDownloaderOperation爹殊,然后把SDWebImageDownloader這個工具類的證書驗證及操作優(yōu)先級等屬性賦值給它生成的每個SDWebImageDownloaderOperation。在方法的最后奸绷,有這樣一個if判斷:

if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // 用戶可以設(shè)置下載操作的執(zhí)行順序梗夸,如果設(shè)置了LIFO(Last In First Out)的話,會將前一個下載操作依賴當(dāng)前下載操作号醉,保證了最后生成的下載操作會最先執(zhí)行
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }

這里稍微引申一下反症,iOS中最常用的多線程編程方法應(yīng)該就是NSOperation和GCD了吧,這里可以看到NSOperation對比GCD的一個優(yōu)點:添加依賴非常方便扣癣。當(dāng)然還有另外的優(yōu)點比如提交的操作可以取消惰帽,可以設(shè)置操作的優(yōu)先級等。所以要根據(jù)不同的應(yīng)用場景選擇最合適的工具父虑。我們接著看addProgressCallback:completedBlock:forURL:createCallback:方法:

- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {
    // 因為URL后續(xù)會當(dāng)做保存下載操作的字典查詢的key该酗,所以必須保證不為空。
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }
    //同步查詢字典中是否存在該url對應(yīng)的操作
    LOCK(self.operationsLock);
    SDWebImageDownloaderOperation *operation = [self.URLOperations objectForKey:url];
    //如果不存在 或者 操作已經(jīng)標(biāo)記為完成
    if (!operation || operation.isFinished) {
        operation = createCallback();
        __weak typeof(self) wself = self;
        operation.completionBlock = ^{
            __strong typeof(wself) sself = wself;
            if (!sself) {
                return;
            }
            //operation的完成回調(diào)中會將自己從URLOperations字典中移除
            LOCK(sself.operationsLock);
            [sself.URLOperations removeObjectForKey:url];
            UNLOCK(sself.operationsLock);
        };
        [self.URLOperations setObject:operation forKey:url];

        [self.downloadQueue addOperation:operation];
    }
    UNLOCK(self.operationsLock);
    //將progressBlock和completeBlock賦值給cancelToken
    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    //生成downloadToken
    SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
    token.downloadOperation = operation;
    token.url = url;
    token.downloadOperationCancelToken = downloadOperationCancelToken;

    return token;
}

可以看到,從最外層傳入的progressBlock 和 completeBlock 最終都賦值給了cancelToken呜魄,我們進入該方法看看:

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

就是將兩個回調(diào)先用字典裝起來悔叽,然后添加到callbackBlocks數(shù)組中。這里我剛開始看的時候有個疑問爵嗅,不是一個url對應(yīng)一個下載嗎娇澎,為什么SDWebImageDownloaderOperation內(nèi)部的回調(diào)會是個數(shù)組呢?后來才想明白睹晒,就跟上面說的一樣趟庄,如果多個控件短時間內(nèi)加載同一個url,先加載的那個控件生成了一個下載操作伪很,后續(xù)就沒必要再生成下載操作了戚啥,但是回調(diào)是必須區(qū)分的,因為每個控件的完成回調(diào)中锉试,會把圖片賦值給當(dāng)前控件猫十。所以內(nèi)部的回調(diào)要用一個數(shù)組來裝載,圖片下載完成以后依次調(diào)用每個回調(diào)呆盖。

最終的下載操作都是SDWebImageDownloaderOperation這個類實現(xiàn)的拖云。我們知道使用NSOperation實現(xiàn)多線程的話,只有兩種方法应又,一是使用它的子類:NSInvocationOperation 或者 NSBlockOperation宙项;另外就是自定義一個類,繼承自NSOperation丁频,覆寫它的start方法杉允∫靥可以看到SD使用的是后面一個方法席里。我們看看它的start方法里都做了些什么:

- (void)start {
     //一般都是在start方法開始的時候就檢測當(dāng)前操作是否被取消。
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

//iOS和tvOS都可以在App進入后臺后向系統(tǒng)申請額外的操作時間
#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:^{
                __strong __typeof (wself) sself = wself;

                if (sself) {
                    [sself cancel];

                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif
        NSURLSession *session = self.unownedSession;
        if (!session) {
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            //如果外部沒有賦值session給它拢驾,那么就自己在內(nèi)部生成一個奖磁,并賦值給ownedSession,
            session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                    delegate:self
                                               delegateQueue:nil];
            self.ownedSession = session;
        }
        
        if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
            // Grab the cached data for later check
            NSURLCache *URLCache = session.configuration.URLCache;
            if (!URLCache) {
                URLCache = [NSURLCache sharedURLCache];
            }
            NSCachedURLResponse *cachedResponse;
            // SD特別指明了 URLCache 的 cachedResponseForRequest:方法不是線程安全的
            @synchronized (URLCache) {
                cachedResponse = [URLCache cachedResponseForRequest:self.request];
            }
            if (cachedResponse) {
                self.cachedData = cachedResponse.data;
            }
        }
        
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }

    if (self.dataTask) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
        if ([self.dataTask respondsToSelector:@selector(setPriority:)]) {
            //設(shè)定dataTask的優(yōu)先級
            if (self.options & SDWebImageDownloaderHighPriority) {
                self.dataTask.priority = NSURLSessionTaskPriorityHigh;
            } else if (self.options & SDWebImageDownloaderLowPriority) {
                self.dataTask.priority = NSURLSessionTaskPriorityLow;
            }
        }
#pragma clang diagnostic pop
        [self.dataTask resume];
        //調(diào)用progress回調(diào)
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        __weak typeof(self) weakSelf = self;
        //回調(diào)主線程發(fā)送開始下載的通知
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:weakSelf];
        });
    } else {
        //如果沒有生成dataTask繁疤,則調(diào)用完成回調(diào)咖为,并傳遞error
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
        [self done];
        return;
    }

#if SD_UIKIT
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
        UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
        [app endBackgroundTask:self.backgroundTaskId];
        self.backgroundTaskId = UIBackgroundTaskInvalid;
    }
#endif
}

最后,我們再來看看SDWebImageManager中調(diào)用imageDownloader工具類生成downloadToken的完成回調(diào)中做了什么事情:

__weak typeof(strongOperation) weakSubOperation = strongOperation;
            strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong typeof(weakSubOperation) strongSubOperation = weakSubOperation;
                if (!strongSubOperation || strongSubOperation.isCancelled) {
                    // 如果strongSubOperation為空稠腊,或者被取消了躁染,什么都不做
                } else if (error) {
                  //如果產(chǎn)生錯誤,則執(zhí)行完成回調(diào)架忌,并回傳錯誤
                    [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url];
                    BOOL shouldBlockFailedURL;
                    // 檢查是否需要將當(dāng)前的url放到請求失敗的url數(shù)組中
                    if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
                        shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
                    } else {
                        shouldBlockFailedURL = (   error.code != NSURLErrorNotConnectedToInternet
                                                && error.code != NSURLErrorCancelled
                                                && error.code != NSURLErrorTimedOut
                                                && error.code != NSURLErrorInternationalRoamingOff
                                                && error.code != NSURLErrorDataNotAllowed
                                                && error.code != NSURLErrorCannotFindHost
                                                && error.code != NSURLErrorCannotConnectToHost
                                                && error.code != NSURLErrorNetworkConnectionLost);
                    }
                    
                    if (shouldBlockFailedURL) {
                        LOCK(self.failedURLsLock);
                        [self.failedURLs addObject:url];
                        UNLOCK(self.failedURLsLock);
                    }
                }
                else {
                    //一切正常的話就走到這里
                    if ((options & SDWebImageRetryFailed)) {
                    //如果options設(shè)置了SDWebImageRetryFailed選項吞彤,就把當(dāng)前url從failedURLs中移除。因為有可能多次請求一個url,前面請求失敗的話饰恕,就被添加到這個數(shù)組中了挠羔。請求成功的時候需要移除。
                        LOCK(self.failedURLsLock);
                        [self.failedURLs removeObject:url];
                        UNLOCK(self.failedURLsLock);
                    }
                    
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
                    
                    // SD自己的manager是默認(rèn)實現(xiàn)了縮放處理的埋嵌,如果使用的是用戶自己的manager就走下面這步進行縮放處理
                    if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
                        downloadedImage = [self scaledImageForKey:key image:downloadedImage];
                    }

                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                            //異步執(zhí)行圖片轉(zhuǎn)換操作
                            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

                            if (transformedImage && finished) {
                                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                NSData *cacheData;
                                // pass nil if the image was transformed, so we can recalculate the data from the image
                                if (self.cacheSerializer) {
                                    cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
                                } else {
                                    cacheData = (imageWasTransformed ? nil : downloadedData);
                                }
                                [self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                            
                            [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        });
                    } else {
                        if (downloadedImage && finished) {
                            //如果用戶自定義了圖片的緩存處理方法
                            if (self.cacheSerializer) {
                                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                                    NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
                                    [self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
                                });
                            } else {
                                //將圖片存入緩存
                                [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                        }
                        //調(diào)用最終的完成回調(diào)
                        [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }

                if (finished) {
                    //如果操作完成破加,將當(dāng)前操作從保存的正在運行的操作數(shù)組中移除
                    [self safelyRemoveOperationFromRunning:strongSubOperation];
                }
            }];

至此,SD的所有主流程我們都梳理了一遍雹嗦。這一路的參數(shù)傳遞及各種block回調(diào)范舀,剛開始看的時候確實會比較懵逼。但是只要靜下心來了罪,對照著源碼耐心研讀尿背,最終一定會融會貫通的。當(dāng)然捶惜,SD還有一些其他的模塊田藐,我自己也沒有仔細去看,就不班門弄斧了吱七。第三方源碼的解讀確實是比較花時間汽久,特別是想自己寫一篇比較全面的總結(jié)得時候就更加需要耐心了。一不小心這篇總結(jié)就差不多花了我周末兩天時間踊餐,已經(jīng)周日下午三點多景醇,是時候抓住周末的尾巴啦~就醬,溜了溜了吝岭。三痰。。


囂張.jpg

參考資料:
https://knightsj.github.io/2018/02/03/SDWebImage%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/
http://www.reibang.com/p/9e97c11aeea9

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末窜管,一起剝皮案震驚了整個濱河市散劫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌幕帆,老刑警劉巖获搏,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異失乾,居然都是意外死亡常熙,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門碱茁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來裸卫,“玉大人,你說我怎么就攤上這事纽竣∧够撸” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長募壕。 經(jīng)常有香客問我调炬,道長,這世上最難降的妖魔是什么舱馅? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任缰泡,我火速辦了婚禮,結(jié)果婚禮上代嗤,老公的妹妹穿的比我還像新娘棘钞。我一直安慰自己,他們只是感情好干毅,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布宜猜。 她就那樣靜靜地躺著,像睡著了一般硝逢。 火紅的嫁衣襯著肌膚如雪姨拥。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天渠鸽,我揣著相機與錄音叫乌,去河邊找鬼。 笑死徽缚,一個胖子當(dāng)著我的面吹牛憨奸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播凿试,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼排宰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了那婉?” 一聲冷哼從身側(cè)響起板甘,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎吧恃,沒想到半個月后虾啦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體麻诀,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡痕寓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蝇闭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呻率。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖呻引,靈堂內(nèi)的尸體忽然破棺而出礼仗,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布元践,位于F島的核電站韭脊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏单旁。R本人自食惡果不足惜沪羔,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望象浑。 院中可真熱鬧蔫饰,春花似錦、人聲如沸愉豺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蚪拦。三九已至杖剪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間驰贷,已是汗流浹背摘盆。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留饱苟,地道東北人孩擂。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像箱熬,于是被迫代替她去往敵國和親类垦。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

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

  • 技術(shù)無極限城须,從菜鳥開始蚤认,從源碼開始。 由于公司目前項目還是用OC寫的項目糕伐,沒有升級swift 所以暫時SDWebI...
    充滿活力的早晨閱讀 12,622評論 0 2
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,089評論 1 32
  • 5.SDWebImageDownloader 下面分析這個類看這個類的結(jié)構(gòu) 這個類的屬性比較多砰琢。 先看這個類的pu...
    充滿活力的早晨閱讀 1,075評論 0 0
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時...
    歐辰_OSR閱讀 29,321評論 8 265
  • 至今還在回身尋你,然而良瞧,忘了你已離去 靜靜地停頓片刻 疼痛緩緩襲來 如果你還在【如果你還在...
    陳輝123閱讀 303評論 1 1