SDWebImage源碼解析(二)

SDWebImage源碼解析(一)中图张,我從宏觀上介紹了SDWebImage項(xiàng)目赠橙,并詳細(xì)介紹了UIImageView+WebCacheSDWebImageManager兩個(gè)類〈细唬現(xiàn)在我們繼續(xù)研究SDWebImageDownloaderSDImageCache肝谭。

SDWebImageDownloader

Asynchronous downloader dedicated and optimized for image loading.

SDWebImageDownloader是專用的且優(yōu)化的圖片異步加載器特笋。先了解一下下載選項(xiàng):

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    //默認(rèn)的使用模式,前往下載,返回進(jìn)度block信息,完成時(shí)調(diào)用completedBlock
    SDWebImageDownloaderLowPriority = 1 << 0,
    // 漸進(jìn)式下載,如果設(shè)置了這個(gè)選項(xiàng),會(huì)在下載過程中,每次接收到一段返回?cái)?shù)據(jù)就會(huì)調(diào)用一次完成回調(diào),回調(diào)中的image參數(shù)為未下載完成的部分圖像,可以實(shí)現(xiàn)將圖片一點(diǎn)點(diǎn)顯示出來的功能
    SDWebImageDownloaderProgressiveDownload = 1 << 1,
    // 默認(rèn)情況下請(qǐng)求不使用NSURLCache甘有,如果設(shè)置該選項(xiàng)律适,則以默認(rèn)的緩存策略來使用NSURLCache
    SDWebImageDownloaderUseNSURLCache = 1 << 2,
    // 如果從NSURLcache緩存中讀取圖片腔长,則在調(diào)用完成block的時(shí)候,傳遞空的image或者imageData
    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
    // 在iOS 4+系統(tǒng)上袭祟,允許程序進(jìn)入后臺(tái)后繼續(xù)下載圖片。該操作通過向系統(tǒng)申請(qǐng)額外時(shí)間來完成后臺(tái)下載捞附。如果后臺(tái)任務(wù)終止巾乳,則操作將被取消
    SDWebImageDownloaderContinueInBackground = 1 << 4,  
    //通過設(shè)置NSMutableURLRequest.HTTPShouldHandleCookies = YES來處理存儲(chǔ)在NSHTTPCookieStore中的cookie
    SDWebImageDownloaderHandleCookies = 1 << 5,
    // 允許不受信任的SSL證書。主要用于測(cè)試目的(生產(chǎn)環(huán)境慎用)
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
    // 將圖片下載放到高優(yōu)先級(jí)隊(duì)列中
    SDWebImageDownloaderHighPriority = 1 << 7,
};

再看看下載順序:

typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
    //默認(rèn)的下載順序鸟召,先進(jìn)先出
    SDWebImageDownloaderFIFOExecutionOrder,
    //后進(jìn)先出
    SDWebImageDownloaderLIFOExecutionOrder
};

SDWebImageDownloader也定義了三個(gè)block:

// 下載進(jìn)度回調(diào)(返回已經(jīng)接收的圖片數(shù)據(jù)的大小,未接收的圖片數(shù)據(jù)的大小)
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
// 下載完成回調(diào)胆绊,返回圖片數(shù)據(jù)或錯(cuò)誤
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
//過濾HTTP請(qǐng)求的Header
typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers);

類方法分別是:

//給每個(gè)HTTP下載請(qǐng)求頭的指定field設(shè)置值。
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
//返回HTTP特定field的值
- (NSString *)valueForHTTPHeaderField:(NSString *)field;
//設(shè)置一個(gè)SDWebImageDownloaderOperation的子類作為下載請(qǐng)求的默認(rèn)NSOperation
- (void)setOperationClass:(Class)operationClass;
//創(chuàng)建一個(gè)SDWebImageDownloader異步下載實(shí)例欧募,圖片下載完成或錯(cuò)誤時(shí)压状,通知delegate回調(diào)。方法返回一個(gè) SDWebImageOperation
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageDownloaderOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
// 設(shè)置下載隊(duì)列為掛起狀態(tài)
- (void)setSuspended:(BOOL)suspended;
//取消隊(duì)列中的所有操作跟继。
- (void)cancelAllDownloads;

實(shí)際上种冬,SDWebImageDownloader管理一個(gè)下載隊(duì)列downloadQueue,默認(rèn)最大的并行操作個(gè)數(shù)是6舔糖。隊(duì)列中每一個(gè)SDWebImageDownloaderOperation實(shí)例才是真正的下載請(qǐng)求執(zhí)行者娱两。
我們重點(diǎn)研究核心下載方法

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageDownloaderOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

該方法就是調(diào)用了另外一個(gè)關(guān)鍵方法:

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock 
             completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock 
                      forURL:(NSURL *)url 
              createCallback:(SDWebImageNoParamsBlock)createCallback {
    // url作為URLCallbacks的key,如果為nil ,直接調(diào)用completedBlock金吗。
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return;
    }
    //將所有下載任務(wù)的網(wǎng)絡(luò)響應(yīng)處理放到barrierQueue隊(duì)列中十兢。
    //并設(shè)置柵欄來確保同一時(shí)間只有一個(gè)線程操作URLCallbacks屬性
    dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }

        // Handle single download of simultaneous download request for the same URL
        //修改url對(duì)應(yīng)的URLCallbacks
        //URLCallbacks是一個(gè)字典: key是url, value是數(shù)組
        //數(shù)組的元素是字典趣竣,key是callback類型字符串,value是callback的block
        NSMutableArray *callbacksForURL = self.URLCallbacks[url];
        NSMutableDictionary *callbacks = [NSMutableDictionary new];
        if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
        if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
        [callbacksForURL addObject:callbacks];
        self.URLCallbacks[url] = callbacksForURL;
        //第一次請(qǐng)求這個(gè)url 才去真正做http請(qǐng)求
        if (first) {
            createCallback();
        }
    });
}

該方法為下載的操作添加回調(diào)的塊, 在下載進(jìn)行時(shí), 或者在下載結(jié)束時(shí)執(zhí)行一些操作旱物。圖片下載的progressBlockcompletedBlock回調(diào)由一個(gè)字典URLCallbacks管理遥缕。字典的key是圖片的url,value 是一個(gè)數(shù)組宵呛,數(shù)組只包含一個(gè)元素,這個(gè)元素的類型是NSMutableDictionary類型,這個(gè)字典的key為NSString類型代表著回調(diào)類型,value為block,是對(duì)應(yīng)的回調(diào)通砍。由于允許多個(gè)圖片同時(shí)下載,因此可能會(huì)有多個(gè)線程同時(shí)操作URLCallbacks屬性烤蜕。為了保證線程安全封孙,將下載操作作為一個(gè)個(gè)任務(wù)放到barrierQueue隊(duì)列中,并設(shè)置柵欄來確保同一時(shí)間只有一個(gè)線程操作URLCallbacks屬性
兩個(gè)回調(diào)對(duì)應(yīng)的key分別是

static NSString *const kProgressCallbackKey = @"progress";
static NSString *const kCompletedCallbackKey = @"completed";

如果URLCallbacks沒有url這個(gè)key讽营,說明是第一次請(qǐng)求這個(gè)url虎忌,需要調(diào)用createCallback創(chuàng)建下載任務(wù),即使用

- (id)initWithRequest:(NSURLRequest *)request
            inSession:(NSURLSession *)session
              options:(SDWebImageDownloaderOptions)options
             progress:(SDWebImageDownloaderProgressBlock)progressBlock
            completed:(SDWebImageDownloaderCompletedBlock)completedBlock
            cancelled:(SDWebImageNoParamsBlock)cancelBlock

初始化SDWebImageDownloaderOperation實(shí)例橱鹏。
下載任務(wù)使用NSMutableURLRequest膜蠢,默認(rèn)超時(shí)時(shí)間是15秒。
在progress block中我們?nèi)〕龃鎯?chǔ)在URLCallbacks中的progressBlock

SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_sync(sself.barrierQueue, ^{
    callbacksForURL = [sself.URLCallbacks[url] copy];
});
for (NSDictionary *callbacks in callbacksForURL) {
    //異步提交莉兰, 當(dāng)前線程直接返回
    //callbacks在main_queue中并行執(zhí)行
    dispatch_async(dispatch_get_main_queue(), ^{
        SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
        if (callback) 
            callback(receivedSize, expectedSize);
    });
}

對(duì)已經(jīng)接收到的大小和期待的大小調(diào)用callback挑围;
在completed block中我們?nèi)〕龃鎯?chǔ)在URLCallbacks中的completedBlock

SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_barrier_sync(sself.barrierQueue, ^{
    callbacksForURL = [sself.URLCallbacks[url] copy];
    if (finished) {
        [sself.URLCallbacks removeObjectForKey:url];
    }
 });
for (NSDictionary *callbacks in callbacksForURL) {                             
    SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
    if (callback) 
        callback(image, data, error, finished);
}

對(duì)image和data調(diào)用callback;
在cancelled block中,我們移除存儲(chǔ)在URLCallbacks的數(shù)組糖荒。
初始化完成后杉辙,再設(shè)置operation的參數(shù):

//是否解壓下載的圖片,默認(rèn)是YES,但是會(huì)消耗掉很多內(nèi)存捶朵,如果遇到內(nèi)存不足的crash時(shí)蜘矢,將值設(shè)為NO。
 operation.shouldDecompressImages = wself.shouldDecompressImages;
//設(shè)置證書
if (wself.urlCredential) {
    operation.credential = wself.urlCredential;
} else if (wself.username && wself.password) {
    operation.credential = [NSURLCredential credentialWithUser:wself.username
                                                      password:wself.password
                                                   persistence:NSURLCredentialPersistenceForSession];
}
//設(shè)置隊(duì)列優(yōu)先級(jí)        
if (options & SDWebImageDownloaderHighPriority) {
    operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
    operation.queuePriority = NSOperationQueuePriorityLow;
}

最后將這個(gè)SDWebImageDownloaderOperation實(shí)例添加到downloadQueue隊(duì)列中去综看。如果下載執(zhí)行順序是LIFO品腹,還要加上任務(wù)的依賴

//加入操作隊(duì)列后, operation 真正開始執(zhí)行start
//所有的下載任務(wù)放在downloadQueue隊(duì)列中
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
    //加上任務(wù)的依賴红碑,也就是說依賴的任務(wù)都完成后舞吭,才能執(zhí)行當(dāng)前任務(wù)
    [wself.lastAddedOperation addDependency:operation];
    wself.lastAddedOperation = operation;
}

SDWebImageDownloaderOperation

現(xiàn)在我們來研究一下上面提到的SDWebImageDownloaderOperation類。
SDWebImageDownloaderOperation是NSOperation的子類析珊,遵循SDWebImageOperation羡鸥, NSURLSessionTaskDelegateNSURLSessionDataDelegate協(xié)議唾琼,并重寫了start方法兄春。在start方法中真正處理HTTP請(qǐng)求和URL鏈接澎剥。
首先監(jiān)測(cè)下載狀態(tài):

 //管理下載狀態(tài)锡溯,如果已取消赶舆,則重置當(dāng)前下載并設(shè)置完成狀態(tài)為YES
if (self.isCancelled) {
    self.finished = YES;
    [self reset];
    return;
}

如果是iOS4.0以上的版本,還需要考慮是否在后臺(tái)執(zhí)行:

Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
    //如果設(shè)置了在后臺(tái)執(zhí)行祭饭,則進(jìn)行后臺(tái)執(zhí)行
     __weak __typeof__ (self) wself = self;
    UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
    self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
        // 如果在系統(tǒng)規(guī)定時(shí)間內(nèi)任務(wù)還沒有完成(一般是10分鐘)芜茵,結(jié)束后臺(tái)任務(wù)
        __strong __typeof (wself) sself = wself;
        if (sself) {
            [sself cancel];
            [app endBackgroundTask:sself.backgroundTaskId];
            sself.backgroundTaskId = UIBackgroundTaskInvalid;
        }
    }];
} 

Version3.8中,下載已經(jīng)由原先的NSURLConnection切換到了NSURLSession了:

NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
    NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    sessionConfig.timeoutIntervalForRequest = 15;
    //為任務(wù)創(chuàng)建會(huì)話倡蝙,我們給delegateQueue設(shè)置nil來創(chuàng)建一個(gè)順序操作隊(duì)列去執(zhí)行所有的代理方法和完成回調(diào)九串。
    self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                      delegate:self
                                                 delegateQueue:nil];
    session = self.ownedSession;
}
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
self.thread = [NSThread currentThread];

創(chuàng)建好任務(wù)后開始執(zhí)行請(qǐng)求。 如果任務(wù)創(chuàng)建成功寺鸥,可能需要調(diào)用progressBlock回調(diào)并發(fā)送下載開始的通知猪钮;如果創(chuàng)建失敗,直接執(zhí)行完成回調(diào),并傳遞一個(gè)connection沒有初始化的錯(cuò)誤:

//開啟任務(wù)
[self.dataTask resume];
if (self.dataTask) {
    if (self.progressBlock) {
        self.progressBlock(0, NSURLResponseUnknownLength);
    }
    dispatch_async(dispatch_get_main_queue(), ^{
        // 在主線程中發(fā)送開始下載的通知
        [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
     });
} else {
    //如果session創(chuàng)建失敗,直接執(zhí)行完成回調(diào),并傳遞一個(gè)connection沒有初始化的錯(cuò)誤
    if (self.completedBlock) {
        self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
    }
}

任務(wù)開始后胆建,我們需要關(guān)注NSURLSessionDataDelegate的幾個(gè)代理方法烤低。
首先是

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler

此代理方法告訴delegate已經(jīng)接受到服務(wù)器的初始應(yīng)答, 準(zhǔn)備接下來的數(shù)據(jù)任務(wù)的操作。這里主要可講的是對(duì)返回碼為304的處理笆载。在HTTP的返回碼中扑馁,304表示服務(wù)端資源未改變,可直接使用客戶端未過期的資源凉驻,我們需要取消operation并返回緩存中的image腻要。
其次是

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data

此代理方法告訴delegate已經(jīng)接收到部分?jǐn)?shù)據(jù),拼接數(shù)據(jù)涝登。

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    //添加新收到的部分?jǐn)?shù)據(jù)
    [self.imageData appendData:data];
    //如果SDWebImageDownloaderOptions選擇了逐步下載模式而且還在下載中雄家,需要實(shí)時(shí)更新下載的資源
    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
        //已經(jīng)下載的總大小
        const NSInteger totalSize = self.imageData.length;
        // 利用現(xiàn)有的數(shù)據(jù)創(chuàng)建一個(gè)CGImageSourceRef對(duì)象
        CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
        //首次進(jìn)入,從這些包含圖像信息的數(shù)據(jù)中取出圖像的長(zhǎng)胀滚、寬咳短、方向等信息以備使用
        if (width + height == 0) {
            CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
            if (properties) {
                NSInteger orientationValue = -1;
                CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
                if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
                val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
                if (val) CFNumberGetValue(val, kCFNumberLongType, &width);
                val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
                if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
                CFRelease(properties);

                //繪制到Core Graphics時(shí),會(huì)丟失方向信息蛛淋,這意味著有時(shí)候由initWithCGIImage創(chuàng)建的圖片                //    的方向會(huì)不對(duì)咙好,所以在這邊先保存這個(gè)信息并在后面使用
                orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
            }
        }
        //下載未完成
        if (width + height > 0 && totalSize < self.expectedSize) {
            // 使用現(xiàn)有的數(shù)據(jù)創(chuàng)建部分圖片對(duì)象,如果數(shù)據(jù)中存有多張圖片褐荷,則取第一張
            CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);

#ifdef TARGET_OS_IPHONE
            // Workaround for iOS anamorphic image
           // 對(duì)下載下來的圖片做個(gè)顏色空間轉(zhuǎn)換等處理
            if (partialImageRef) {
                const size_t partialHeight = CGImageGetHeight(partialImageRef);
                CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
                CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
                CGColorSpaceRelease(colorSpace);
                if (bmContext) {
                    CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
                    CGImageRelease(partialImageRef);
                    partialImageRef = CGBitmapContextCreateImage(bmContext);
                    CGContextRelease(bmContext);
                }
                else {
                    CGImageRelease(partialImageRef);
                    partialImageRef = nil;
                }
            }
#endif

            if (partialImageRef) {
                UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                // 對(duì)圖片進(jìn)行縮放
                UIImage *scaledImage = [self scaledImageForKey:key image:image];
                if (self.shouldDecompressImages) {
                    // 對(duì)圖片解壓縮
                    image = [UIImage decodedImageWithImage:scaledImage];
                }
                else {
                    image = scaledImage;
                }
                CGImageRelease(partialImageRef);
                dispatch_main_sync_safe(^{
                    if (self.completedBlock) {
                        self.completedBlock(image, nil, nil, NO);
                    }
                });
            }
        }

        CFRelease(imageSource);
    }
    //調(diào)用progressBlock勾效,實(shí)時(shí)更新圖像信息
    if (self.progressBlock) {
        self.progressBlock(self.imageData.length, self.expectedSize);
    }
}

另外還有NSURLSessionTaskDelegate的兩個(gè)代理方法:

//告訴delegate, task已經(jīng)完成,直接調(diào)用completedBlock叛甫,刷新UIImageView层宫。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error 
//需要請(qǐng)求認(rèn)證
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler

這里就不再一一贅述了。

SDImageCache

現(xiàn)在我們研究緩存部分其监,即SDImageCache類萌腿。

SDImageCache maintains a memory cache and an optional disk cache. Disk cache write operations are performed asynchronous so it doesn’t add unnecessary latency to the UI.

SDImageCache維持了一個(gè)內(nèi)存緩存memCache和一個(gè)可選的磁盤緩存fileManager,磁盤緩存的寫操作時(shí)異步的抖苦。
內(nèi)存緩存是用NSCache實(shí)現(xiàn)的毁菱,以Key-Value的形式存儲(chǔ)圖片米死,當(dāng)內(nèi)存不夠的時(shí)候會(huì)清除所有緩存圖片。磁盤緩存則是緩存到沙盒中贮庞,文件替換方式是以時(shí)間為單位峦筒,剔除時(shí)間大于一周的圖片文件。
先來看看幾個(gè)重要的屬性:

//同SDWebImageDownloader的屬性
//是否解壓下載的圖片窗慎,默認(rèn)是YES,但是會(huì)消耗掉很多內(nèi)存物喷,如果遇到內(nèi)存不足的crash時(shí),將值設(shè)為NO遮斥。
@property (assign, nonatomic) BOOL shouldDecompressImages;
//是否使用內(nèi)存緩存峦失,默認(rèn)YES
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
//內(nèi)存緩存的最大像素量
@property (assign, nonatomic) NSUInteger maxMemoryCost;
//內(nèi)存緩存的最大對(duì)象數(shù)
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;
//圖片在緩存中的最長(zhǎng)壽命,默認(rèn)1周术吗,超期刪除
@property (assign, nonatomic) NSInteger maxCacheAge;
//最大緩存大小
@property (assign, nonatomic) NSUInteger maxCacheSize;

再看看幾個(gè)重要的方法:

//用指定的命名空間來初始化一個(gè)cache
//創(chuàng)建磁盤緩存路徑宠进,調(diào)用initWithNamespace:diskCacheDirectory方法
- (id)initWithNamespace:(NSString *)ns;
//創(chuàng)建memCache和fileManager,初始化diskCachePath等屬性
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory;
//將key對(duì)應(yīng)的image存儲(chǔ)到內(nèi)存緩存和磁盤緩存中
- (void)storeImage:(UIImage *)image forKey:(NSString *)key;
//將key對(duì)應(yīng)的image存儲(chǔ)到內(nèi)存緩存藐翎,是否同時(shí)存入磁盤中由參數(shù)toDisk決定
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
//功能同上材蹬,參數(shù)recalculate指明imageData是否可用或者應(yīng)該從UIImage重新構(gòu)造;參數(shù)imageData是由服務(wù)器返回吝镣,可以用于磁盤存儲(chǔ)堤器,這樣可以避免將image轉(zhuǎn)換為一個(gè)可存儲(chǔ)/壓縮的圖片以節(jié)省CPU。
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
//真正將key對(duì)應(yīng)的image存儲(chǔ)到磁盤緩存中
- (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key;
//異步查詢disk cache
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
//同步查詢memory cache
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
//先檢測(cè)memory cache,再監(jiān)測(cè)disk cache并存到memory cache里
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
//從memory cache 中刪除image末贾,并從disk cache中異步刪除
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
//清空memory cache闸溃,收到內(nèi)存警告時(shí)調(diào)用
- (void)clearMemory;
// 清空disk cache
- (void)clearDisk;
//清空disk cache ,非阻塞方法拱撵,立即返回
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
//清除disk cache中所有過期image
- (void)cleanDisk;
//清除disk cache中所有過期image辉川,非阻塞方法,立即返回
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
//同步獲取disk cache 使用的cache 大小拴测,利用NSFileManager的enumeratorAtPath方法遍歷disk cache文件累計(jì)fileSize
- (NSUInteger)getSize;
//同步獲取disk cache的圖片數(shù)量
- (NSUInteger)getDiskCount;
//異步獲取disk cache的圖片數(shù)量和緩存大小
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;
//監(jiān)測(cè)key對(duì)應(yīng)的圖片是否在disk cache中乓旗,方法先按照defaultCachePathForKey生成的path尋找,如果沒有則對(duì)path刪除擴(kuò)展名集索,再尋找屿愚。
- (BOOL)diskImageExistsWithKey:(NSString *)key;
//功能同上,異步的
- (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
//根據(jù)指定的key生成cache 路徑务荆,為disk cache 使用
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path;
//指定的key默認(rèn)的cache 路徑妆距,調(diào)用上面的方法,第二個(gè)參數(shù)為self.diskCachePath
- (NSString *)defaultCachePathForKey:(NSString *)key;

disk cache的文件名是key做MD5后的字符串:

- (NSString *)cachedFileNameForKey:(NSString *)key {
    const char *str = [key UTF8String];
    if (str == NULL) {
        str = "";
    }
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                          r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                          r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]];

    return filename;
}

我們重點(diǎn)研究怎么存儲(chǔ)到緩存中的函匕,storeImage:forKey:storeImage:forKey:toDisk:最終都是調(diào)用storeImage:recalculateFromImage:imageData:forKey:toDisk:方法的娱据。
如果需要存儲(chǔ)到memory cache中,首先存入memory cache盅惜。

if (self.shouldCacheImagesInMemory) {
    NSUInteger cost = SDCacheCostForImage(image);
    [self.memCache setObject:image forKey:key cost:cost];
}

如果需要存儲(chǔ)到disk cache中剩,在子線程中串行存儲(chǔ)到disk cache中:

dispatch_async(self.ioQueue, ^{//串行隊(duì)列
    NSData *data = imageData;
    if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
        // 確定圖片是png還是jpeg. imageData為nil而且有alapha通道忌穿,當(dāng)作png處理
       // PNG圖片的前八個(gè)字節(jié)是137 80 78 71 13 10 26 10
       int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
       BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                         alphaInfo == kCGImageAlphaNoneSkipFirst ||
                         alphaInfo == kCGImageAlphaNoneSkipLast);
       BOOL imageIsPng = hasAlpha;

       // 如果imageData有值,查看前綴
       if ([imageData length] >= [kPNGSignatureData length]) {
           imageIsPng = ImageDataHasPNGPreffix(imageData);
       }
       if (imageIsPng) {
          // PNG
          data = UIImagePNGRepresentation(image);
       }
       else {
           //JPEGP
           data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
        }
#else
        data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
    }
    //真正存儲(chǔ)到磁盤中 
    [self storeImageDataToDisk:data forKey:key];
});

最終真正存儲(chǔ)到磁盤中的方法是:

- (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key {
    //監(jiān)測(cè)imageData
    if (!imageData) {
        return;
    }
    //創(chuàng)建目錄
    if (![_fileManager fileExistsAtPath:_diskCachePath]) {
        [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    
    // 獲取默認(rèn)的緩存路徑
    NSString *cachePathForKey = [self defaultCachePathForKey:key];
    // 將路徑轉(zhuǎn)化為 NSUrl
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    //存儲(chǔ)文件
    [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
    //禁用iCloud備份
    if (self.shouldDisableiCloud) {
        [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}

再來看看圖片查詢的幾個(gè)方法咽安。在SDWebImageManager中的downloadImageWithURL:options:progress:completed方法中使用到了imageCache的queryDiskCacheForKey:done方法伴网。這是SDImageCache里面查詢圖片的入口蓬推。
首先妆棒,從memory cache中查詢,如果找到圖片就直接使用并返回:

 UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }

否則沸伏,去disk cache中查詢糕珊,同樣是在子線程的同步隊(duì)列中執(zhí)行。如果找到毅糟,還需要監(jiān)測(cè)是否需要存儲(chǔ)到memory cache中:

NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
    if (operation.isCancelled) {
        return;
    }

    @autoreleasepool {
        UIImage *diskImage = [self diskImageForKey:key];
        if (diskImage && self.shouldCacheImagesInMemory) {
            NSUInteger cost = SDCacheCostForImage(diskImage);
            [self.memCache setObject:diskImage forKey:key cost:cost];
        }

        dispatch_async(dispatch_get_main_queue(), ^{
            doneBlock(diskImage, SDImageCacheTypeDisk);
        });
      }
});

其中红选,在memory cache 中查詢很簡(jiǎn)單,直接使用字典方法objectForKey:

- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
    return [self.memCache objectForKey:key];
}

在disk cache中查詢姆另,需要根據(jù)key構(gòu)造各種可能的路徑喇肋。最后如果找到,需要縮放或者解壓縮:

// 構(gòu)造各種可能路徑去查詢
- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {
    NSString *defaultPath = [self defaultCachePathForKey:key];
    NSData *data = [NSData dataWithContentsOfFile:defaultPath];
    if (data) {
        return data;
    }
    // 考慮文件擴(kuò)展名
    data = [NSData dataWithContentsOfFile:[defaultPath stringByDeletingPathExtension]];
    if (data) {
        return data;
    }

    NSArray *customPaths = [self.customPaths copy];
    for (NSString *path in customPaths) {
        NSString *filePath = [self cachePathForKey:key inPath:path];
        NSData *imageData = [NSData dataWithContentsOfFile:filePath];
        if (imageData) {
            return imageData;
        }
        imageData = [NSData dataWithContentsOfFile:[filePath stringByDeletingPathExtension]];
        if (imageData) {
            return imageData;
        }
    }

    return nil;
}
//從disk cache中查詢
- (UIImage *)diskImageForKey:(NSString *)key {
    NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
    if (data) {
        UIImage *image = [UIImage sd_imageWithData:data];
        //縮放
        image = [self scaledImageForKey:key image:image];
        if (self.shouldDecompressImages)
            //解壓縮
            image = [UIImage decodedImageWithImage:image];
        }
        return image;
    }
    else {
        return nil;
    }
}

最后來看看圖片的清理方式迹辐。移除指定key對(duì)應(yīng)的圖片有一系列方法蝶防,最終調(diào)用的方法是:

- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion {
    if (key == nil) {
        return;
    }
    //先從memory cache 中移除
    if (self.shouldCacheImagesInMemory) {
        [self.memCache removeObjectForKey:key];
    }
    //再?gòu)膁isk cache 中移除
    if (fromDisk) {
        dispatch_async(self.ioQueue, ^{
            [_fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil];
            
            if (completion) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completion();
                });
            }
        });
    } else if (completion){
        completion();
    }
}

而清空cache有兩種方式,即完全清空與部分清空明吩。對(duì)于memory cache是完全清空的:

- (void)clearMemory {
    [self.memCache removeAllObjects];
}

對(duì)于disk cache间学,兩種方式都有可能。完全清空的方式是直接把文件夾移除掉:

- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion
{
    dispatch_async(self.ioQueue, ^{
        [_fileManager removeItemAtPath:self.diskCachePath error:nil];
        [_fileManager createDirectoryAtPath:self.diskCachePath
                withIntermediateDirectories:YES
                                 attributes:nil
                                      error:NULL];

        if (completion) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion();
            });
        }
    });
}

部分清空是根據(jù)參數(shù)配置移除文件印荔,使文件的總大小小于最大使用空間低葫。清理策略有兩個(gè):

  1. 文件的緩存有效期:默認(rèn)是一周。如果文件的緩存時(shí)間超過這個(gè)時(shí)間值仍律,則將其移除嘿悬。
  2. 最大緩存空間大小:如果所有緩存文件的總大小超過最大緩存空間水泉,則會(huì)按照文件最后修改時(shí)間的逆序鹊漠,以每次一半的遞歸來移除那些過早的文件,直到緩存的實(shí)際大小小于我們?cè)O(shè)置的最大使用空間茶行。
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
        NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

        // 通過文件的枚舉器來獲取緩存文件的有用的屬性
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];

        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
        NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
        NSUInteger currentCacheSize = 0;

        //遍歷cache 目錄躯概,刪除過期文件,存儲(chǔ)文件屬性
        NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
            //跳過文件夾
            if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }

            //記錄待刪除的過期文件 并continue
            NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
            if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }
            //沒刪除的文件畔师,存儲(chǔ)文件的資源屬性 計(jì)算文件總大小
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
            [cacheFiles setObject:resourceValues forKey:fileURL];
        }
        //刪除過期文件
        for (NSURL *fileURL in urlsToDelete) {
            [_fileManager removeItemAtURL:fileURL error:nil];
        }
        //剩下的cache總大小依然超出配置的cache最大值娶靡,執(zhí)行第二次清理
        //首先清除最老的文件,每次清理一半看锉,遞歸
        if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
            const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
            // 所有文件按照修改時(shí)間排序
            NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                            usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                            }];
            //刪除文件 直到desiredCacheSize大小
            for (NSURL *fileURL in sortedFiles) {
                if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                    NSDictionary *resourceValues = cacheFiles[fileURL];
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];

                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

至此姿锭,我們已經(jīng)把SDWebImage最主要的幾個(gè)模塊分析清楚了塔鳍,我們可以繪制一個(gè)流程圖來對(duì)各個(gè)模塊的工作流做個(gè)總結(jié):

SDWebImage_流程圖.png

延伸

最后,我們延伸一點(diǎn)知識(shí)呻此,講講前面提到的dispatch_main_sync_safe宏轮纫、dispatch_main_async_safe宏以及SDWebImageDecoder的作用。

這兩個(gè)宏比較簡(jiǎn)單焚鲜,直接看代碼:

#define dispatch_main_sync_safe(block)\
    if ([NSThread isMainThread]) {\
        block();\
    } else {\
        dispatch_sync(dispatch_get_main_queue(), block);\
    }

#define dispatch_main_async_safe(block)\
    if ([NSThread isMainThread]) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }

即保證當(dāng)前代碼在主線程中執(zhí)行掌唾,上面是同步調(diào)用,下面是異步調(diào)用忿磅。
判斷主線程的目的是避免出現(xiàn)死鎖問題:
*** 如果在主線程中執(zhí)行dispatch_sync(dispatch_get_main_queue(), block) 同步操作時(shí)糯彬,會(huì)出現(xiàn)死鎖問題,因?yàn)橹骶€程正在執(zhí)行當(dāng)前代碼葱她,根本無法將block添加到主隊(duì)列中 ***

SDWebImageDecoder用來解壓縮圖片撩扒,關(guān)于為什么從磁盤讀取image后要做一次解壓縮,參考了v2panda的解釋吨些,僅供大家參考搓谆。

因?yàn)橥ㄟ^ imageNamed 創(chuàng)建 UIImage 時(shí),系統(tǒng)實(shí)際上只是在 Bundle 內(nèi)查找到文件名豪墅,然后把這個(gè)文件名放到 UIImage 里返回泉手,并沒有進(jìn)行實(shí)際的文件讀取和解碼。當(dāng) UIImage 第一次顯示到屏幕上時(shí)但校,其內(nèi)部的解碼方法才會(huì)被調(diào)用螃诅,同時(shí)解碼結(jié)果會(huì)保存到一個(gè)全局緩存去。在圖片解碼后状囱,App 第一次退到后臺(tái)和收到內(nèi)存警告時(shí)术裸,該圖片的緩存才會(huì)被清空,其他情況下緩存會(huì)一直存在亭枷。具體的說就是一個(gè)UIImage加載了jpeg或者png袭艺,當(dāng)UIImageView將要顯示這個(gè)UIImage的時(shí)候會(huì)先把png和jpeg解碼成未壓縮格式,所以SDWebImage有一個(gè)decodeImage方法叨粘,就是把這一步放在了異步線程做猾编,防止tableViewCell中的imageView加載圖片的時(shí)候在主線程解碼圖片,導(dǎo)致滑動(dòng)卡頓升敲。這樣效率很低答倡,但是只有瞬時(shí)的內(nèi)存需求。為了提高效率通過SDWebImageDecoder將包裝在Data下的資源解壓驴党,然后畫在另外一張圖片上瘪撇,這樣這張新圖片就不再需要重復(fù)解壓了,這種做法是典型的空間換時(shí)間的做法,如下從硬盤中去圖片時(shí),分別對(duì)圖片進(jìn)行了縮放和解壓縮操作倔既。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末恕曲,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子渤涌,更是在濱河造成了極大的恐慌佩谣,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件实蓬,死亡現(xiàn)場(chǎng)離奇詭異茸俭,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)瞳秽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門瓣履,熙熙樓的掌柜王于貴愁眉苦臉地迎上來率翅,“玉大人练俐,你說我怎么就攤上這事∶岢簦” “怎么了腺晾?”我有些...
    開封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)辜贵。 經(jīng)常有香客問我悯蝉,道長(zhǎng),這世上最難降的妖魔是什么托慨? 我笑而不...
    開封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任鼻由,我火速辦了婚禮,結(jié)果婚禮上厚棵,老公的妹妹穿的比我還像新娘蕉世。我一直安慰自己,他們只是感情好婆硬,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開白布狠轻。 她就那樣靜靜地躺著,像睡著了一般彬犯。 火紅的嫁衣襯著肌膚如雪向楼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天谐区,我揣著相機(jī)與錄音湖蜕,去河邊找鬼。 笑死宋列,一個(gè)胖子當(dāng)著我的面吹牛昭抒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼戈鲁,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼仇参!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起婆殿,我...
    開封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤诈乒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后婆芦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體怕磨,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年消约,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肠鲫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡或粮,死狀恐怖导饲,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情氯材,我是刑警寧澤渣锦,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站氢哮,受9級(jí)特大地震影響袋毙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜冗尤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一听盖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧裂七,春花似錦皆看、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至捉兴,卻和暖如春蝎困,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背倍啥。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工禾乘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人虽缕。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓始藕,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子伍派,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

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