【轉(zhuǎn)】SDWebImage源碼閱讀(四)

1. 前言


SDWebImage中主要實(shí)現(xiàn)了NSURLConnectionDataDelegate的以下方法:

  • (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;

  • (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;

  • (void)connectionDidFinishLoading:(NSURLConnection *)connection;

  • (nullable NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
    以及NSURLConnectionDelegate的以下方法:

  • (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;

  • (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection;

  • (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
    足足有7個(gè)函數(shù)需要實(shí)現(xiàn),好多啊辐真。具體來看看每個(gè)代理方法大概是做什么的。

2. - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response


我們都知道HTTP報(bào)文是面向文本的必逆,報(bào)文中的每一個(gè)字段都是一些ASCII碼串吗氏。HTTP有兩類報(bào)文芽偏,分別是Request和Response。HTTP的Response報(bào)文由三個(gè)部分所組成弦讽,分別是:狀態(tài)行污尉、消息報(bào)頭、響應(yīng)正文往产。

此處代理實(shí)現(xiàn)的方法中被碗,只使用了Response的狀態(tài)碼,即statusCode仿村。注意HTTP的statusCode小于400表示正常碼锐朴。但是304碼表示文檔的內(nèi)容(自上次訪問以來或者根據(jù)請(qǐng)求的條件)并沒有改變,這里我們?cè)讷@取圖片時(shí)考慮直接使用Cache蔼囊,所以statusCode為304時(shí)會(huì)單獨(dú)處理焚志。

于是有了下面的框架:

if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) {
        // ...
}
else {
    if (code == 304) {
        // ...
    } else {
        // ...
    }
}

如果Response返回正常碼,并且不為304畏鼓,即if語句中的內(nèi)容:

// 根據(jù)response中的expectedContentLength來給self.expectedSize進(jìn)行賦值
// 而self.expectedSize此處表示響應(yīng)的數(shù)據(jù)體(此處為imageData)期望大小
// 注意expectedContentLength為-1時(shí)娩嚼,expectedSize賦值為0
NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0;
self.expectedSize = expected;
// 使用用戶自定義的progressBlock
if (self.progressBlock) {
    self.progressBlock(0, expected);
}
// expected大小此處表示的就是imageData的期望大小,也就是說imageData最后下載完成大概會(huì)這么大
// 所以收到響應(yīng)后滴肿,就初始化一個(gè)NSMutableData,用來存儲(chǔ)image數(shù)據(jù)
self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
// 不解釋佃迄,因?yàn)槲野l(fā)現(xiàn)SDWebImage只在此處使用了self.response
// 應(yīng)該是暴露給用戶使用的
self.response = response;
// 不過好像SDWebImage中并沒有addObserver這個(gè)SDWebImageDownloadReceiveResponseNotification
// 可能需要用戶自己去使用addObserver
dispatch_async(dispatch_get_main_queue(), ^{
    [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
});

如果response返回錯(cuò)誤碼泼差,即else中的語句:

// 獲取statusCode
NSUInteger code = [((NSHTTPURLResponse *)response) statusCode];
    
if (code == 304) {
    // 當(dāng)服務(wù)器端返回statusCode為“304 Not Modified”,意味著服務(wù)器端的image并沒有改變呵俏,
// 此時(shí)堆缘,我們只需取消connection,然后返回緩存中的image
    // 此時(shí)返回碼是正確碼(小于400)普碎,只是不需要進(jìn)行多余的connection網(wǎng)絡(luò)操作了吼肥,所以單獨(dú)調(diào)用
    // cancelInternal     
    [self cancelInternal];
} else {
    [self.connection cancel];
} 
// 同SDWebImageDownloadStartNotification 
dispatch_async(dispatch_get_main_queue(), ^{
    [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
});
// 因?yàn)槌鲥e(cuò)了,所以直接調(diào)用completedBlock并返回錯(cuò)誤狀態(tài)碼
if (self.completedBlock) {
    self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES);
}
// 出錯(cuò)了麻车,所以停止這個(gè)RunLoop
// 我們會(huì)自然想到start函數(shù)中的CFRunLoopRun函數(shù)會(huì)結(jié)束
CFRunLoopStop(CFRunLoopGetCurrent());
// 最后在done中調(diào)用reset回收資源 并置finished為YES缀皱,executing為NO
[self done];

3. - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data


這一步是實(shí)實(shí)在在的獲取到了數(shù)據(jù),第一步先將獲取到的data串到self.imageData上动猬。因?yàn)槿绻鹖mage比較大的話啤斗,會(huì)多次調(diào)用didReceiveData,這樣一個(gè)image就分成很多塊了赁咙,所以每次receive到data钮莲,就串起來:

[self.imageData appendData:data];

但是我們發(fā)現(xiàn)這個(gè)函數(shù)總體套在一個(gè)if語句中:

if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
    // ...
}

為什么會(huì)出現(xiàn)這個(gè)選項(xiàng)了免钻?我覺得主要是為了單獨(dú)處理SDWebImageDownloaderProgressiveDownload,回顧一下崔拥,這個(gè)選項(xiàng)是在SDWebImageManager中的downloadImageWithURL中賦值的:

if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;

SDWebImageProgressiveDownload表示image的顯示過程是隨著下載的進(jìn)度一點(diǎn)點(diǎn)進(jìn)行的极舔,而不是下載完成后,一次顯示完成链瓦。這就可以理解了拆魏,因?yàn)橐S著下載進(jìn)度顯示,所以每接收到新的data澡绩,就要顯示一下稽揭。為什么還需要completedBlock呢?因?yàn)樵赿idReceiveData中只是獲取到了imageData肥卡,但是還需要顯示在imageView上呢溪掀?那就得使用completedBlock來進(jìn)行處理。所以SDWebImageProgressiveDownload默認(rèn)的圖片顯示是交給用戶進(jìn)行處理的步鉴。至于expectedSize為什么要大于0我就不是很清楚了揪胃。

所以在函數(shù)結(jié)尾處,我們可以看到:

dispatch_main_sync_safe(^{
    if (self.completedBlock) {
        // 處理此時(shí)獲得到的image
        self.completedBlock(image, nil, nil, NO);
    }
});

那么image是怎么產(chǎn)生的呢氛琢?可以看到上層包裹著一個(gè)if語句:

// partialImageRef是一個(gè)CGImageRef類型的值喊递,本質(zhì)還是self.imageData
if (partialImageRef) {
    // 從CGImageRef轉(zhuǎn)化為UIImage,scale你可以理解為圖片后綴為@1x,@2x,@3x需要放大的倍數(shù)
    // 至于orientation后面會(huì)講阳似,暫時(shí)理解圖片的朝向
    UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
    // 有時(shí)候你不想直接把圖片的url作為cache的key骚勘,因?yàn)橛锌赡軋D片的url是動(dòng)態(tài)變化的
    // 所以你可以自定義一個(gè)cache key filter
    // 我還沒使用過filter,所以這里一般來說就是獲得到了image的url
    NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
    // scaledImageForKey是SDWebImageCompat的一個(gè)函數(shù)撮奏,主要是根據(jù)image名稱中
    // @2x,@3x來設(shè)置scale俏讹,并通過initWithCGImage來獲得image,下面會(huì)詳解
    UIImage *scaledImage = [self scaledImageForKey:key image:image];
    // 判斷是否要壓縮圖片畜吊,初始化默認(rèn)是要壓縮圖片的
    if (self.shouldDecompressImages) {
        // 下面會(huì)詳解decodedImageWithImage
        image = [UIImage decodedImageWithImage:scaledImage];
    }
    else {
        image = scaledImage;
    }
    // 釋放資源
    CGImageRelease(partialImageRef);
    // 上面解釋過了
    dispatch_main_sync_safe(^{
        if (self.completedBlock) {
            self.completedBlock(image, nil, nil, NO);
        }
    });
}

3.1 scaledImageForKey


因?yàn)閟caledImageForKey就是封裝了SDScaledImageForKey泽疆,所以我們?cè)斀釹DScaledImageForKey:

// 這是一個(gè)C++函數(shù)
inline UIImage *SDScaledImageForKey(NSString *key, UIImage *image) {
    // 細(xì)節(jié)考慮
    if (!image) {
        return nil;
    }
    
    // 注釋中說出現(xiàn)這種情況的是animated images旨别,也就是動(dòng)圖
    // 我們常見的是gif圖片空执,所以此處我們就當(dāng)做gif圖片去理解
    // 可以理解gif圖片是一張張靜態(tài)的圖片構(gòu)成的動(dòng)畫
    if ([image.images count] > 0) {
        NSMutableArray *scaledImages = [NSMutableArray array];
        // 使用了遞歸的方式车柠,構(gòu)建一組圖片動(dòng)畫
for (UIImage *tempImage in image.images) {
            [scaledImages addObject:SDScaledImageForKey(key, tempImage)];
        }
        // 根據(jù)這些images構(gòu)成我們所需的animated image
        return [UIImage animatedImageWithImages:scaledImages duration:image.duration];
    }
    else {
        if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
            // 比如屏幕為320x480時(shí)描沟,scale為1镜雨,屏幕為640x960時(shí)鹏氧,scale為2
            CGFloat scale = [UIScreen mainScreen].scale;
            // “@2x.png”的長(zhǎng)度為7炒瘟,所以此處添加了這個(gè)判斷黎比,很巧妙
            if (key.length >= 8) {
                // 這個(gè)不用解釋了延窜,很簡(jiǎn)單恋腕。就是根據(jù)后綴給scale賦值
                NSRange range = [key rangeOfString:@"@2x."];
                if (range.location != NSNotFound) {
                    scale = 2.0;
                }
                
                range = [key rangeOfString:@"@3x."];
                if (range.location != NSNotFound) {
                    scale = 3.0;
                }
            }
            // 使用initWithCGImage來根據(jù)Core Graphics的圖片構(gòu)建UIImage。
            // 這個(gè)函數(shù)可以使用scale和orientation
            UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
            image = scaledImage;
        }
        return image;
    }
}

3.2 decodedImageWithImage


+ (UIImage *)decodedImageWithImage:(UIImage *)image {
    // 當(dāng)下載大量的圖片逆瑞,產(chǎn)生內(nèi)存警告時(shí)
    // 自動(dòng)釋放bitmap上下文環(huán)境和所有變量
    // 來釋放系統(tǒng)內(nèi)存空間
    // 在iOS7中荠藤,不要忘記添加
    // [[SDImageCache sharedImageCache] clearMemory];
    @autoreleasepool{
        // 對(duì)于animated images伙单,不需要解壓縮
        if (image.images) { return image; }
    
        CGImageRef imageRef = image.CGImage;
        // 感覺下面的操作就是為了將image本身的alpha去除
        // 然后創(chuàng)建bitmap后,重新加上alpha
    
        // 圖片如果有alpha通道哈肖,就返回原始image吻育,因?yàn)閖pg圖片有alpha的話,就不壓縮
        CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
        BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
                         alpha == kCGImageAlphaLast ||
                         alpha == kCGImageAlphaPremultipliedFirst ||
                         alpha == kCGImageAlphaPremultipliedLast);
    
        if (anyAlpha) { return image; }
    
        // 圖片寬高
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        
        CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
        CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);
        
        // 圖片的ColorSpaceModel為kCGColorSpaceModelUnknown淤井,kCGColorSpaceModelMonochrome
        // 和kCGColorSpaceModelIndexed時(shí)布疼,說明該ColorSpace不受支持
        bool unsupportedColorSpace = (imageColorSpaceModel == 0 || imageColorSpaceModel == -1 || imageColorSpaceModel == kCGColorSpaceModelIndexed);
        // 如果屬于上述不支持的ColorSpace,ColorSpace就使用RGB
        if (unsupportedColorSpace)
            colorspaceRef = CGColorSpaceCreateDeviceRGB();
    
        // 當(dāng)你調(diào)用這個(gè)函數(shù)的時(shí)候币狠,Quartz創(chuàng)建一個(gè)位圖繪制環(huán)境游两,也就是位圖上下文。
        // 當(dāng)你向上下文中繪制信息時(shí)漩绵,Quartz把你要繪制的信息作為位圖數(shù)據(jù)繪制到指定的內(nèi)存塊贱案。
        // 一個(gè)新的位圖上下文的像素格式由三個(gè)參數(shù)決定:
        // 每個(gè)組件的位數(shù),顏色空間止吐,alpha選項(xiàng)宝踪。alpha值決定了繪制像素的透明性。
        CGContextRef context = CGBitmapContextCreate(NULL, width,
                                                     height,
                                                     CGImageGetBitsPerComponent(imageRef),
                                                     0,
                                                     colorspaceRef,
                                                     kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
    
        // 在上面創(chuàng)建的context繪制image碍扔,并以此獲取image瘩燥,而該image也將擁有alpha通道
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
        CGImageRef imageRefWithAlpha = CGBitmapContextCreateImage(context);
        UIImage *imageWithAlpha = [UIImage imageWithCGImage:imageRefWithAlpha scale:image.scale orientation:image.imageOrientation];
    
        // 開始釋放資源
        if (unsupportedColorSpace)
            CGColorSpaceRelease(colorspaceRef);
        
        CGContextRelease(context);
        CGImageRelease(imageRefWithAlpha);
        
        return imageWithAlpha;
    }
}

回到didReceiveData的剩余部分,也就是剛才那個(gè)if語句的最最外層if語句(if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock)):

// 獲取當(dāng)前已經(jīng)下載的數(shù)據(jù)大小
const NSInteger totalSize = self.imageData.length;
// 使用最新下載后的圖片數(shù)據(jù)來創(chuàng)建一個(gè)CGImageSourceRef變量imageSource
// 注意創(chuàng)建使用的數(shù)據(jù)是CoreFoundation的data不同,而self.imageData是NSData厉膀,所以要做如下轉(zhuǎn)化
// (__bridge CFDataRef)self.imageData
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);

有了imageSource后,就要根據(jù)imageSource獲取image的各種屬性二拐。主要是Core Graphics框架提供了很多方便的工具站蝠。所以要講imageData先轉(zhuǎn)化為CF框架下的變量,然后創(chuàng)建CG框架下的CGImageSource卓鹿。

接著是:

// width + height == 0在此處其實(shí)就是表示width==0&&height==0
// 初始條件下,也就是第一次執(zhí)行時(shí)留荔,width和height均為0
if (width + height == 0) {
    // 從imageSource中獲取圖片的一些屬性吟孙,比如長(zhǎng)寬等等,是一個(gè)dictionary變量
    // 這里獲取imageSource屬性聚蝶,直接傳入imageSource就行杰妓,為啥還要傳入一個(gè)index?
    // 因?yàn)閷?duì)于gif圖片碘勉,一個(gè)imageSource對(duì)應(yīng)的CGImage會(huì)有多個(gè)巷挥,需要使用index
    // 底下會(huì)使用CGImageSourceCreateImageAtIndex來根據(jù)imageSource創(chuàng)建一個(gè)帶index的CGImageRef
    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);
        // CoreFoundation對(duì)象類型不在ARC范圍內(nèi),所以要手動(dòng)釋放資源
        CFRelease(properties);
        
        // 還記得我們上面講的一段代碼验靡,要使用Core Graphics框架繪制image
        // 其實(shí)就是initWithCGImage這個(gè)函數(shù)倍宾,但是使用這個(gè)函數(shù)有時(shí)候會(huì)產(chǎn)生
         // 圖片的朝向錯(cuò)誤(不像在connectionDidFinishLoading中使用initWithData所產(chǎn)生的image)
        // 所以在這里保存朝向信息雏节,下面有些函數(shù)需要朝向信息,就傳給它
        orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
    }
    
}

然后就是接收到圖片數(shù)據(jù)后高职,width和height有值了:

// width和height更新過了钩乍,并且還沒有獲取到完整的圖片數(shù)據(jù)(totalSize < self.expectedSize)
// 不過為什么獲取到完整的圖片數(shù)據(jù)就不執(zhí)行了?(totalSize == self.expectedSize)
// 因?yàn)橐獔?zhí)行connectionDidFinishLoading函數(shù)了
if (width + height > 0 && totalSize < self.expectedSize) {
    // ......
}

當(dāng)前這個(gè)if里面有兩個(gè)if語句怔锌,第二個(gè)我們講過了寥粹,就是用completedBlock去顯示已下載的image。我們下面著重解釋第一個(gè)if

// 創(chuàng)建圖片
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
#ifdef TARGET_OS_IPHONE
// 解決iOS平臺(tái)圖片失真問題
// 因?yàn)槿绻螺d的圖片是非png格式埃元,圖片會(huì)出現(xiàn)失真
// 為了解決這個(gè)問題涝涤,先將圖片在bitmap的context下渲染
// 然后在傳回partialImageRef
if (partialImageRef) {
    // 下面代碼和decodedImageWithImage差不多
    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

最后一步是調(diào)用progressBlock,我們很少見到調(diào)用progressBlock的情況岛杀,其實(shí)也跟didReceiveData這個(gè)函數(shù)有關(guān)阔拳,因?yàn)橐话憔褪窃跀?shù)據(jù)量比較大的時(shí)候,需要一份一份接受數(shù)據(jù)楞件,并拼接組裝衫生,所以此處可以使用progressBlock。

4. - (void)connectionDidFinishLoading:(NSURLConnection *)connection


如果成功獲取服務(wù)端返回的所有數(shù)據(jù)土浸,則代理會(huì)收到connectionDidFinishLoading:消息
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
    SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
    @synchronized(self) {
        // 停止當(dāng)前的RunLoop
        CFRunLoopStop(CFRunLoopGetCurrent());
        // 回收資源
        self.thread = nil;
        self.connection = nil;
        // 前面說過
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
        });
    }
    
    // 發(fā)送的request罪针,服務(wù)器會(huì)返回一個(gè)response,就像獲取服務(wù)器端的圖片一樣黄伊,
    // 如果圖片沒有改變泪酱,第二次獲取的時(shí)候,最好直接從緩存中獲取还最,這會(huì)省不少時(shí)間墓阀。
    // response也一樣,也弄一個(gè)緩存拓轻,就是NSURLCache斯撮。
    // 根據(jù)你的request,看看是不是緩存中能直接獲取到對(duì)應(yīng)的response扶叉。
    if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) {
        // 為NO表示沒有從NSURLCache中獲取到response
        responseFromCached = NO;
    }
/*
    如果options中有SDWebImageDownloaderIgnoreCachedResponse表示對(duì)應(yīng)的SDWebImageOptions的options為
    SDWebImageRefreshCached勿锅。而有了SDWebImageRefreshCached,就表示downloaderOptions肯定包含
    SDWebImageDownloaderUseNSURLCache/SDWebImageDownloaderIgnoreCachedResponse
    (大家搜一下SDWebImageRefreshCached就知道了)枣氧,但是SDWebImageDownloaderUseNSURLCache和
    SDWebImageDownloaderIgnoreCachedResponse又不是一定同時(shí)存在于options中溢十。因?yàn)橹挥衖mage從
    SDImageCache中獲取到了才會(huì)有SDWebImageDownloaderIgnoreCachedResponse,為什么要特意提
    SDImageCache达吞?因?yàn)镾DWebImage有兩種緩存方式张弛,一個(gè)是SDImageCache,一個(gè)就是NSURLCache,所以知道
    為什么這個(gè)選項(xiàng)是Ignore了吧吞鸭,因?yàn)橐呀?jīng)從SDImageCache獲取了image寺董,就忽略NSURLCache了。
    此處我的理解就是如果已經(jīng)從SDImageCache獲取到了image瞒大,并且選項(xiàng)為了SDWebImageRefreshCached螃征,就要
    設(shè)置SDWebImageDownloaderIgnoreCachedResponse。我們也看到了透敌,即使responseCached為YES了盯滚,
    completedBlock的image和data參數(shù)也為nil。
    我看網(wǎng)上對(duì)這一塊的眾說風(fēng)云酗电,而且這一塊好像也出過不少問題魄藕,懂得大神可以私信我。好好探討一下撵术!
 
    我們看看這兩個(gè)選項(xiàng)的注釋:
    /**
     * 默認(rèn)情況下背率,request請(qǐng)求使用NSURLRequestReloadIgnoringLocalCacheData作為默認(rèn)策略
     * 使用了這個(gè)選項(xiàng),那么request使用NSURLRequestUseProtocolCachePolicy作為默認(rèn)策略
     */
    SDWebImageDownloaderUseNSURLCache 
= 1 << 2, /** * 如果要從NSURLCache讀取image嫩与,并且還要強(qiáng)制刷新NSURLCache寝姿,如果有此選項(xiàng)后 * 就調(diào)用image和data參數(shù)為nil的completedBlock * (有該選項(xiàng)就一定有`SDWebImageDownloaderUseNSURLCache`). */
    SDWebImageDownloaderIgnoreCachedResponse 
= 1 << 3
,
*/
    if (completionBlock) {
        if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) {
            completionBlock(nil, nil, nil, YES);
        } else if (self.imageData) {
          // 因?yàn)閕mage可能是gif,可能是webp划滋,所以需要通過sd_imageWithData轉(zhuǎn)化為UIImage類型饵筑,具體實(shí)現(xiàn)后面會(huì)說
            UIImage *image = [UIImage sd_imageWithData:self.imageData];
            // 前面說過
            NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
            image = [self scaledImageForKey:key image:image];
            
            // 注意對(duì)于gif圖片,不需要解壓縮
            if (!image.images) {
                if (self.shouldDecompressImages) {
                    image = [UIImage decodedImageWithImage:image];
                }
            }
            if (CGSizeEqualToSize(image.size, CGSizeZero)) {
                // 圖片大小為0处坪,報(bào)錯(cuò)
                completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES);
            }
            else {
                completionBlock(image, self.imageData, nil, YES);
            }
        } else {
            // image為空根资,報(bào)錯(cuò)
            completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES);
        }
    }
    // 釋放資源
    self.completionBlock = nil;
    // 置NSConnection為完成狀態(tài)
    [self done];
}

4.1 sd_imageWithData

+ (UIImage *)sd_imageWithData:(NSData *)data {
    // 沒有數(shù)據(jù),細(xì)節(jié)
    if (!data) {
        return nil;
    }
    
    UIImage *image;
    // 根據(jù)data的前面幾個(gè)字節(jié)同窘,判斷出圖片類型玄帕,是jepg,png想邦,gif還是...后面詳解
    NSString *imageContentType = [NSData sd_contentTypeForImageData:data];
    // 如果是gif圖片或webp圖片裤纹,是需要單獨(dú)處理的。后面詳解gif和webp圖片處理
    if ([imageContentType isEqualToString:@"image/gif"]) {
        image = [UIImage sd_animatedGIFWithData:data];
    }
#ifdef SD_WEBP
    else if ([imageContentType isEqualToString:@"image/webp"])
    {
        image = [UIImage sd_imageWithWebPData:data];
    }
#endif
    else {
        image = [[UIImage alloc] initWithData:data];
        // 獲取朝向信息丧没,后面詳解
        UIImageOrientation orientation = [self sd_imageOrientationFromImageData:data];
        // 我估計(jì)默認(rèn)朝向就是向上的服傍,所以如果不是向上的圖片,才進(jìn)行調(diào)整骂铁,省時(shí)間,優(yōu)化
        if (orientation != UIImageOrientationUp) {
            image = [UIImage imageWithCGImage:image.CGImage
                                        scale:image.scale
                                  orientation:orientation];
        }
    }
    return image;
}

4.1.1 sd_contentTypeForImageData

// NSData+ImageContentType
// 每張圖片的開頭會(huì)存儲(chǔ)圖片的類型信息
// 很簡(jiǎn)單的代碼罩抗,不贅述了
+ (NSString *)sd_contentTypeForImageData:(NSData *)data {
    uint8_t c;
    [data getBytes:&c length:1];
    switch (c) {
        case 0xFF:
            return @"image/jpeg";
        case 0x89:
            return @"image/png";
        case 0x47:
            return @"image/gif";
        case 0x49:
        case 0x4D:
            return @"image/tiff";
        case 0x52:
            // R as RIFF for WEBP
            if ([data length] < 12) {
                return nil;
            }
            NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
            if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
                return @"image/webp";
            }
            return nil;
    }
    return nil;
}

4.1.2 sd_animatedGIFWithData

+ (UIImage *)sd_animatedGIFWithData:(NSData *)data {
    if (!data) {
        return nil;
    }
    // 根據(jù)data創(chuàng)建一個(gè)CG下的imageSource
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
    // 返回imageSource中的image數(shù)目拉庵,為后面創(chuàng)建CGImage提供index
    size_t count = CGImageSourceGetCount(source);
    UIImage *animatedImage;
    // count<=1的時(shí)候,就當(dāng)單張圖片
    if (count <= 1) {
        animatedImage = [[UIImage alloc] initWithData:data];
    }
    else {
        // 多張圖片套蒂,每幀0.1秒
        NSMutableArray *images = [NSMutableArray array];
        NSTimeInterval duration = 0.0f;
        // 
        for (size_t i = 0; i < count; i++) {
            // 根據(jù)指定的index創(chuàng)建CGImage
            CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
            // 根據(jù)imageSource和指定的index獲取該CGImage的duration钞支,后面詳解
            duration += [self sd_frameDurationAtIndex:i source:source];
            // 往images添加單張圖片
            [images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];
            CGImageRelease(image);
        }
        // 如果image中沒有duration信息茫蛹,就自己計(jì)算。每幀0.1秒烁挟,算出gif動(dòng)畫所需的duration
        if (!duration) {
            duration = (1.0f / 10.0f) * count;
        }
        
        animatedImage = [UIImage animatedImageWithImages:images duration:duration];
    }
    // 釋放資源
    CFRelease(source);
    return animatedImage;
}

4.1.2.1 sd_frameDurationAtIndex

+ (float)sd_frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
    float frameDuration = 0.1f;
    // 根據(jù)imageSource和index獲取到image的屬性
    CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil);
    // 轉(zhuǎn)化CFDictionaryRef為NSDictionary
    NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties;
    // 因?yàn)閕mage是gif婴洼,所以根據(jù)kCGImagePropertyGIFDictionary獲取到image的gif的屬性
    NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary];
    // 從gifProperties根據(jù)kCGImagePropertyGIFUnclampedDelayTime獲取到該張image的duration,
    // 如果該gif沒有unclamped delay time撼嗓,就是用kCGImagePropertyGIFDelayTime獲取delay time作為duration
    NSNumber *delayTimeUnclampedProp = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime];
    if (delayTimeUnclampedProp) {
        frameDuration = [delayTimeUnclampedProp floatValue];
    }
    else {
        NSNumber *delayTimeProp = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime];
        if (delayTimeProp) {
            frameDuration = [delayTimeProp floatValue];
        }
    }
    // 許多煩人的gif的廣告柬采,每張圖片的duration是0,這樣達(dá)到快速刷新圖片的效果
    // 這里我們根據(jù)Firefox的做法且警,對(duì)已duration小于等于100ms的每幀圖片粉捻,指定幀率為10ms
    if (frameDuration < 0.011f) {
        frameDuration = 0.100f;
    }
    CFRelease(cfFrameProperties);
    return frameDuration;
}

4.1.3 sd_imageWithWebPData

// WebP 是 Google 在 2010 年發(fā)布的圖片格式,希望以更高的壓縮比替代 JPEG斑芜。
// 它用 VP8 視頻幀內(nèi)編碼作為其算法基礎(chǔ)肩刃,取得了不錯(cuò)的壓縮效果。
// 它支持有損和無損壓縮杏头、支持完整的透明通道盈包、也支持多幀動(dòng)畫,并且沒有版權(quán)問題醇王,是一種非常理想的圖片格式呢燥。
// 借由 Google 在網(wǎng)絡(luò)世界的影響力,WebP 在幾年的時(shí)間內(nèi)已經(jīng)得到了廣泛的應(yīng)用厦画。
// 看看你手機(jī)里的 App:微博疮茄、微信、QQ根暑、淘寶力试、網(wǎng)易新聞等等,每個(gè) App 里都有 WebP 的身影排嫌。Facebook 則更進(jìn)一步畸裳,用 WebP 來顯示聊天界面的貼紙動(dòng)畫。
// WebP 標(biāo)準(zhǔn)是 Google 定制的淳地,迄今為止也只有 Google 發(fā)布的 libwebp 實(shí)現(xiàn)了該的編解碼 怖糊。 所以這個(gè)庫也是該格式的事實(shí)標(biāo)準(zhǔn)。
+ (UIImage *)sd_imageWithWebPData:(NSData *)data {
    // 具體算法我不是很清楚
    // 大概就是根據(jù)data設(shè)置WebPDecoderConfig類型變量config
    WebPDecoderConfig config;
    if (!WebPInitDecoderConfig(&config)) {
        return nil;
    }
    if (WebPGetFeatures(data.bytes, data.length, &config.input) != VP8_STATUS_OK) {
        return nil;
    }
    config.output.colorspace = config.input.has_alpha ? MODE_rgbA : MODE_RGB;
    config.options.use_threads = 1;
    // 注意此處又一點(diǎn)瑕疵颇象,就是不支持WebP的動(dòng)圖
    // 此處默認(rèn)是WebP的靜態(tài)圖片伍伤,所以直接使用WebPDecode
    //

大牛們可以添加代碼,增加支持WebP動(dòng)圖的功能遣钳,提示一下扰魂, // 首先用WebPDemuxer拆包,之后拆出來的單幀用WebPDecode解碼

if (WebPDecode(data.bytes, data.length, &config) != VP8_STATUS_OK) {
        return nil;
    }
    int width = config.input.width;
    int height = config.input.height;
    if (config.options.use_scaling) {
        width = config.options.scaled_width;
        height = config.options.scaled_height;
    }
    // 根據(jù)decode出來的rgba數(shù)組,即config.output.u.RGBA構(gòu)建UIImage
    CGDataProviderRef provider =
    CGDataProviderCreateWithData(NULL, config.output.u.RGBA.rgba, config.output.u.RGBA.size, FreeImageData);
    CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
    CGBitmapInfo bitmapInfo = config.input.has_alpha ? kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast : 0;
    // rgba是4bytes劝评,rgb是3bytes
    size_t components = config.input.has_alpha ? 4 : 3;
    CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
    // 根據(jù)provider創(chuàng)建image
    CGImageRef imageRef = CGImageCreate(width, height, 8, components * 8, components * width, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
    CGColorSpaceRelease(colorSpaceRef);
    CGDataProviderRelease(provider);
    UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
    CGImageRelease(imageRef);
    return image;
}

4.1.4 sd_imageOrientationFromImageData

+(UIImageOrientation)sd_imageOrientationFromImageData:(NSData *)imageData {
    // 保證如果imageData中獲取不到朝向信息姐直,就默認(rèn)UIImageOrientationUp
    UIImageOrientation result = UIImageOrientationUp;
    CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
    if (imageSource) {
        CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
        if (properties) {
            CFTypeRef val;
            int exifOrientation;
            val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
            if (val) {
                // 這個(gè)kCGImagePropertyOrientation先轉(zhuǎn)化為int值
        // 然后用一個(gè)switch case語句將int轉(zhuǎn)化為朝向的enum值(sd_exifOrientationToiOSOrientation)
                CFNumberGetValue(val, kCFNumberIntType, &exifOrientation);
                result = [self sd_exifOrientationToiOSOrientation:exifOrientation];
            } // else - if it's not set it remains at up
            CFRelease((CFTypeRef) properties);
        } else {
            //NSLog(@"NO PROPERTIES, FAIL");
        }
        CFRelease(imageSource);
    }
    return result;
}

5. - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error


- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    // 一開始的代碼和connectionDidFinishLoading代碼類似,除了少了SDWebImageDownloadFinishNotification
    @synchronized(self) {
        CFRunLoopStop(CFRunLoopGetCurrent());
        self.thread = nil;
        self.connection = nil;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
        });
    }
    // 使用completedBlock報(bào)錯(cuò)error
    if (self.completedBlock) {
        self.completedBlock(nil, nil, error, YES);
    }
    self.completionBlock = nil;
    [self done];
}

6. - (nullable NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse


// 如果我們需要對(duì)緩存做更精確的控制蒋畜,我們可以實(shí)現(xiàn)一些代理方法來允許應(yīng)用來確定請(qǐng)求是否應(yīng)該緩存
// 如果不實(shí)現(xiàn)此方法声畏,NSURLConnection 就簡(jiǎn)單地使用本來要傳入 -connection:willCacheResponse: 的那個(gè)緩存對(duì)象,
// 所以除非你需要改變一些值或者阻止緩存姻成,否則這個(gè)代理方法不必實(shí)現(xiàn)
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse {
    responseFromCached = NO; // 如果該方法被調(diào)用插龄,說明該Response不是從cache讀取的,因?yàn)闀?huì)會(huì)響應(yīng)該方法佣渴,說明這個(gè)cacheResponse是剛從服務(wù)端獲取的新鮮Response辫狼,需要進(jìn)行緩存。
    if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) {
        // 如果request的緩存策略是NSURLRequestReloadIgnoringLocalCacheData辛润,就不緩存了
        return nil;
    }
    else {
        // 否則使用默認(rèn)cacheResponse
        return cachedResponse;
    }
}

7. - (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection


// 在構(gòu)建connection會(huì)被響應(yīng)膨处。如果這個(gè)connection需要根據(jù)NSURLCredentialStorage中的權(quán)限進(jìn)行構(gòu)建,那么就返回YES
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection __unused *)connection {
    // 默認(rèn)是YES砂竖,想要修改真椿,需要用戶自己指定self.shouldUseCredentialStorage值
    return self.shouldUseCredentialStorage;
}

8. - (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge


// 當(dāng)客戶端向目標(biāo)服務(wù)器發(fā)送請(qǐng)求時(shí)。服務(wù)器會(huì)使用401進(jìn)行響應(yīng)乎澄⊥幌酰客戶端收到響應(yīng)后便開始認(rèn)證挑戰(zhàn)(Authentication Challenge),而且是通過willSendRequestForAuthenticationChallenge:函數(shù)進(jìn)行的置济。
// willSendRequestForAuthenticationChallenge:函數(shù)中的challenge對(duì)象包含了protectionSpace(NSURLProtectionSpace)實(shí)例屬性解恰,在此進(jìn)行protectionSpace的檢查。當(dāng)檢查不通過時(shí)既取消認(rèn)證浙于,這里需要注意下的是取消是必要的护盈,因?yàn)閣illSendRequestForAuthenticationChallenge:可能會(huì)被調(diào)用多次。
// 具體過程見下面附圖
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge{
    // NSURLProtectionSpace主要有Host羞酗、port腐宋、protocol、realm檀轨、authenticationMethod等屬性胸竞。
    // 為了進(jìn)行認(rèn)證,程序需要使用服務(wù)端期望的認(rèn)證信息創(chuàng)建一個(gè)NSURLCredential對(duì)象参萄。我們可以調(diào)用authenticationMethod來確定服務(wù)端的認(rèn)證方法卫枝,這個(gè)認(rèn)證方法是在提供的認(rèn)證請(qǐng)求的保護(hù)空間(protectionSpace)中。
    // 服務(wù)端信任認(rèn)證(NSURLAuthenticationMethodServerTrust)需要一個(gè)由認(rèn)證請(qǐng)求的保護(hù)空間提供的信任讹挎。使用credentialForTrust:來創(chuàng)建一個(gè)NSURLCredential對(duì)象校赤。
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        // SDWebImageDownloaderAllowInvalidSSLCertificates表示允許不受信任SSL認(rèn)證
        // 注釋中提示盡量作為test使用腺占,不要在最終production使用。
        // 所以此處使用performDefaultHandlingForAuthenticationChallenge痒谴,即使用系統(tǒng)提供的默認(rèn)行為
        if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates) &&
            [challenge.sender respondsToSelector:@selector(performDefaultHandlingForAuthenticationChallenge:)]) {
            [challenge.sender performDefaultHandlingForAuthenticationChallenge:challenge];
        } else {
            NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
        }
    } else {
        // 每次認(rèn)證失敗,previousFailureCount就會(huì)加1
        // 第一次認(rèn)證(previousFailureCount == 0)并且有Credential铡羡,使用Credential認(rèn)證
        // 非第一次認(rèn)證或者第一次認(rèn)證沒有Credential积蔚,對(duì)于認(rèn)證挑戰(zhàn),不提供Credential就去download一個(gè)request烦周,但是如果這里challenge是需要Credential的challenge尽爆,那么使用這個(gè)方法是徒勞的
        if ([challenge previousFailureCount] == 0) {
            if (self.credential) {
                [[challenge sender] useCredential:self.credential forAuthenticationChallenge:challenge];
            } else {
                [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
            }
        } else {
            [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
        }
    }
}

9. 參考文章


本文轉(zhuǎn)載polobymulberry-博客園

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市读慎,隨后出現(xiàn)的幾起案子漱贱,更是在濱河造成了極大的恐慌,老刑警劉巖夭委,帶你破解...
    沈念sama閱讀 222,807評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件幅狮,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡株灸,警方通過查閱死者的電腦和手機(jī)崇摄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來慌烧,“玉大人逐抑,你說我怎么就攤上這事∫傥茫” “怎么了厕氨?”我有些...
    開封第一講書人閱讀 169,589評(píng)論 0 363
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)汹粤。 經(jīng)常有香客問我命斧,道長(zhǎng),這世上最難降的妖魔是什么玄括? 我笑而不...
    開封第一講書人閱讀 60,188評(píng)論 1 300
  • 正文 為了忘掉前任冯丙,我火速辦了婚禮,結(jié)果婚禮上遭京,老公的妹妹穿的比我還像新娘胃惜。我一直安慰自己,他們只是感情好哪雕,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評(píng)論 6 398
  • 文/花漫 我一把揭開白布船殉。 她就那樣靜靜地躺著,像睡著了一般斯嚎。 火紅的嫁衣襯著肌膚如雪利虫。 梳的紋絲不亂的頭發(fā)上挨厚,一...
    開封第一講書人閱讀 52,785評(píng)論 1 314
  • 那天,我揣著相機(jī)與錄音糠惫,去河邊找鬼疫剃。 笑死,一個(gè)胖子當(dāng)著我的面吹牛硼讽,可吹牛的內(nèi)容都是我干的巢价。 我是一名探鬼主播,決...
    沈念sama閱讀 41,220評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼固阁,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼壤躲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起备燃,我...
    開封第一講書人閱讀 40,167評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤碉克,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后并齐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體漏麦,經(jīng)...
    沈念sama閱讀 46,698評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評(píng)論 3 343
  • 正文 我和宋清朗相戀三年冀膝,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了唁奢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,912評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡窝剖,死狀恐怖麻掸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情赐纱,我是刑警寧澤脊奋,帶...
    沈念sama閱讀 36,572評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站疙描,受9級(jí)特大地震影響诚隙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜起胰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評(píng)論 3 336
  • 文/蒙蒙 一久又、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧效五,春花似錦地消、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至戒劫,卻和暖如春半夷,著一層夾襖步出監(jiān)牢的瞬間婆廊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工巫橄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留淘邻,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,359評(píng)論 3 379
  • 正文 我出身青樓湘换,卻偏偏與公主長(zhǎng)得像列荔,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子枚尼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評(píng)論 2 361

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