SDWebImage源碼解讀SDWebImageDownloader注意

圖片下載的這些回調(diào)信息存儲在SDWebImageDownloader類的URLOperations屬性中,該屬性是一個(gè)字典,key是圖片的URL地址,value則是一個(gè)SDWebImageDownloaderOperation對象,包含每個(gè)圖片的多組回調(diào)信息。由于我們允許多個(gè)圖片同時(shí)下載,因此可能會有多個(gè)線程同時(shí)操作URLOperations屬性。為了保證URLOperations操作(添加、刪除)的線程安全性,SDWebImageDownloader將這些操作作為一個(gè)個(gè)任務(wù)放到barrierQueue隊(duì)列中,并設(shè)置屏障來確保同一時(shí)間只有一個(gè)線程操作URLOperations屬性,我們以添加操作為例,如下代碼所示:

- (nullableSDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock

completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock

forURL:(nullableNSURL*)url

createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {

...

// 1. 以dispatch_barrier_sync操作來保證同一時(shí)間只有一個(gè)線程能對URLOperations進(jìn)行操作

dispatch_barrier_sync(self.barrierQueue, ^{

SDWebImageDownloaderOperation *operation =self.URLOperations[url];

if(!operation) {

//2. 處理第一次URL的下載

operation = createCallback();

self.URLOperations[url] = operation;

__weakSDWebImageDownloaderOperation *woperation = operation;

operation.completionBlock = ^{

SDWebImageDownloaderOperation *soperation = woperation;

if(!soperation)return;

if(self.URLOperations[url] == soperation) {

[self.URLOperations removeObjectForKey:url];

};

};

}

// 3. 處理同一URL的同步下載請求的單個(gè)下載

iddownloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];

token = [SDWebImageDownloadToken new];

token.url = url;

token.downloadOperationCancelToken = downloadOperationCancelToken;

});

returntoken;

}

整個(gè)下載管理器對于下載請求的管理都是放在downloadImageWithURL:options:progress:completed:方法里面來處理的,該方法調(diào)用了上面所提到的addProgressCallback:andCompletedBlock:forURL:createCallback:方法來將請求的信息存入管理器中,同時(shí)在創(chuàng)建回調(diào)的block中創(chuàng)建新的操作,配置之后將其放入downloadQueue操作隊(duì)列中,最后方法返回新創(chuàng)建的操作。其具體實(shí)現(xiàn)如下:

- (nullableSDWebImageDownloadToken *)downloadImageWithURL:(nullableNSURL*)url

options:(SDWebImageDownloaderOptions)options

progress:(nullableSDWebImageDownloaderProgressBlock)progressBlock

completed:(nullableSDWebImageDownloaderCompletedBlock)completedBlock {

__weakSDWebImageDownloader *wself =self;

return[selfaddProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{

__strong__typeof(wself) sself = wself;

//超時(shí)時(shí)間

NSTimeIntervaltimeoutInterval = sself.downloadTimeout;

if(timeoutInterval ==0.0) {

timeoutInterval =15.0;

}

// 1. 創(chuàng)建請求對象,并根據(jù)options參數(shù)設(shè)置其屬性

// 為了避免潛在的重復(fù)緩存(NSURLCache + SDImageCache),如果沒有明確告知需要緩存,則禁用圖片請求的緩存操作

NSMutableURLRequest*request = [[NSMutableURLRequestalloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ?NSURLRequestUseProtocolCachePolicy:NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];

request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);

request.HTTPShouldUsePipelining =YES;

if(sself.headersFilter) {

request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaderscopy]);

}

else{

request.allHTTPHeaderFields = sself.HTTPHeaders;

}

SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];

operation.shouldDecompressImages = sself.shouldDecompressImages;

if(sself.urlCredential) {

operation.credential = sself.urlCredential;

}elseif(sself.username && sself.password) {

operation.credential = [NSURLCredentialcredentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];

}

if(options & SDWebImageDownloaderHighPriority) {

operation.queuePriority =NSOperationQueuePriorityHigh;

}elseif(options & SDWebImageDownloaderLowPriority) {

operation.queuePriority =NSOperationQueuePriorityLow;

}

// 2. 將操作加入到操作隊(duì)列downloadQueue中

// 如果是LIFO順序,則將新的操作作為原隊(duì)列中最后一個(gè)操作的依賴,然后將新操作設(shè)置為最后一個(gè)操作

[sself.downloadQueue addOperation:operation];

if(sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {

// Emulate LIFO execution order by systematically adding new operations as last operation's dependency

[sself.lastAddedOperation addDependency:operation];

sself.lastAddedOperation = operation;

}

returnoperation;

}];

}

另外,每個(gè)下載操作的超時(shí)時(shí)間可以通過downloadTimeout屬性來設(shè)置,默認(rèn)值為15秒。

下載操作

每個(gè)圖片的下載操作都是一個(gè)Operation操作。。我們在上面分析過這個(gè)操作的創(chuàng)建及加入操作隊(duì)列的過程?,F(xiàn)在我們來看看單個(gè)操作的具體實(shí)現(xiàn)。

SDWebImage定義了一個(gè)協(xié)議,即SDWebImageOperation作為圖片下載操作的基礎(chǔ)協(xié)議。它只聲明了一個(gè)cancel方法,用于取消操作。協(xié)議的具體聲明如下:

@protocolSDWebImageOperation

- (void)cancel;

@end

SDWebImage還定義了一個(gè)下載協(xié)議,即SDWebImageDownloaderOperationInterface,它允許用戶自定義下載操作,當(dāng)然,SDWebImage也提供了自己的下載類,即SDWebImageDownloaderOperation,它繼承自NSOperation,并采用了SDWebImageOperation和SDWebImageDownloaderOperationInterface協(xié)議。并且實(shí)現(xiàn)他們的代理方法。

對于圖片的下載,SDWebImageDownloaderOperation完全依賴于URL加載系統(tǒng)中的NSURLSession類。我們先來分析一下SDWebImageDownloaderOperation類中對于圖片實(shí)際數(shù)據(jù)的下載處理,即NSURLSessionDataDelegate和NSURLSessionDataDelegate各個(gè)代理方法的實(shí)現(xiàn)。(ps 有關(guān)NSURLSession類的具體介紹請戳這里)

我們前面說過SDWebImageDownloaderOperation類是繼承自NSOperation類。它沒有簡單的實(shí)現(xiàn)main方法,而是采用更加靈活的start方法,以便自己管理下載的狀態(tài)。

在start方法中,創(chuàng)建了我們下載所使用的NSURLSession對象,開啟了圖片的下載,同時(shí)拋出一個(gè)下載開始的通知。當(dāng)然,如果我們期望下載在后臺處理,則只需要配置我們的下載選項(xiàng),使其包含SDWebImageDownloaderContinueInBackground選項(xiàng)。start方法的具體實(shí)現(xiàn)如下:

- (void)start {

@synchronized(self) {

// 管理下載狀態(tài),如果已取消,則重置當(dāng)前下載并設(shè)置完成狀態(tài)為YES

if(self.isCancelled) {

self.finished =YES;

[selfreset];

return;

}

...

NSURLSession*session =self.unownedSession;

if(!self.unownedSession) {

//如果session為空,創(chuàng)建session

NSURLSessionConfiguration*sessionConfig = [NSURLSessionConfigurationdefaultSessionConfiguration];

sessionConfig.timeoutIntervalForRequest =15;

self.ownedSession = [NSURLSessionsessionWithConfiguration:sessionConfig

delegate:self

delegateQueue:nil];

session =self.ownedSession;

}

//創(chuàng)建下載任務(wù)

self.dataTask = [session dataTaskWithRequest:self.request];

self.executing =YES;

}

//開啟下載任務(wù)

[self.dataTask resume];

if(self.dataTask) {

for(SDWebImageDownloaderProgressBlock progressBlockin[selfcallbacksForKey:kProgressCallbackKey]) {

progressBlock(0,NSURLResponseUnknownLength,self.request.URL);

}

// 2. 在主線程拋出下載開始通知

dispatch_async(dispatch_get_main_queue(), ^{

[[NSNotificationCenterdefaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];

});

}else{

[selfcallCompletionBlocksWithError:[NSErrorerrorWithDomain:NSURLErrorDomaincode:0userInfo:@{NSLocalizedDescriptionKey:@"Connection can't be initialized"}]];

}

...

}

我們先看看NSURLSessionDataDelegate代理的具體實(shí)現(xiàn):

- (void)URLSession:(NSURLSession*)session

dataTask:(NSURLSessionDataTask*)dataTask

didReceiveResponse:(NSURLResponse*)response

completionHandler:(void(^)(NSURLSessionResponseDispositiondisposition))completionHandler {

//接收到服務(wù)器響應(yīng)

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

//如果服務(wù)器狀態(tài)碼正常,并且不是304,(因?yàn)?04表示遠(yuǎn)程圖片并沒有改變,當(dāng)前緩存的圖片就可以使用)拿到圖片的大小。并進(jìn)度回調(diào)

NSIntegerexpected = response.expectedContentLength >0? (NSInteger)response.expectedContentLength :0;

self.expectedSize = expected;

for(SDWebImageDownloaderProgressBlock progressBlockin[selfcallbacksForKey:kProgressCallbackKey]) {

progressBlock(0, expected,self.request.URL);

}

//根據(jù)返回?cái)?shù)據(jù)大小創(chuàng)建一個(gè)數(shù)據(jù)Data容器

self.imageData = [[NSMutableDataalloc] initWithCapacity:expected];

self.response = response;

dispatch_async(dispatch_get_main_queue(), ^{

//發(fā)送接收到服務(wù)器響應(yīng)通知

[[NSNotificationCenterdefaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];

});

}

else{

//狀態(tài)碼錯(cuò)誤

NSUIntegercode = ((NSHTTPURLResponse*)response).statusCode;

//判斷是不是304

if(code ==304) {

[selfcancelInternal];

}else{

[self.dataTask cancel];

}

dispatch_async(dispatch_get_main_queue(), ^{

//發(fā)出停止下載通知

[[NSNotificationCenterdefaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];

});

//錯(cuò)誤回調(diào)

[selfcallCompletionBlocksWithError:[NSErrorerrorWithDomain:NSURLErrorDomaincode:((NSHTTPURLResponse*)response).statusCode userInfo:nil]];

//重置

[selfdone];

}

if(completionHandler) {

completionHandler(NSURLSessionResponseAllow);

}

}

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

//1. 接收服務(wù)器返回?cái)?shù)據(jù) 往容器中追加數(shù)據(jù)

[self.imageData appendData:data];

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

//2. 獲取已下載數(shù)據(jù)總大小

constNSIntegertotalSize =self.imageData.length;

// 3. 更新數(shù)據(jù)源,我們需要傳入所有數(shù)據(jù),而不僅僅是新數(shù)據(jù)

CGImageSourceRefimageSource =CGImageSourceCreateWithData((__bridgeCFDataRef)self.imageData,NULL);

// 4. 首次獲取到數(shù)據(jù)時(shí),從這些數(shù)據(jù)中獲取圖片的長、寬、方向?qū)傩灾?/p>

if(width + height ==0) {

CFDictionaryRefproperties =CGImageSourceCopyPropertiesAtIndex(imageSource,0,NULL);

if(properties) {

NSIntegerorientationValue =-1;

CFTypeRefval =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);

// 5. 當(dāng)繪制到Core Graphics時(shí),我們會丟失方向信息,這意味著有時(shí)候由initWithCGIImage創(chuàng)建的圖片

//? ? 的方向會不對,所以在這邊我們先保存這個(gè)信息并在后面使用。

#if SD_UIKIT || SD_WATCH

orientation = [[selfclass] orientationFromPropertyValue:(orientationValue ==-1?1: orientationValue)];

#endif

}

}

// 6. 圖片還未下載完成

if(width + height >0&& totalSize

// 7. 使用現(xiàn)有的數(shù)據(jù)創(chuàng)建圖片對象,如果數(shù)據(jù)中存有多張圖片,則取第一張

CGImageRefpartialImageRef =CGImageSourceCreateImageAtIndex(imageSource,0,NULL);

#if SD_UIKIT || SD_WATCH

// 8. 適用于iOS變形圖像的解決方案。我的理解是由于iOS只支持RGB顏色空間,所以在此對下載下來的圖片做個(gè)顏色空間轉(zhuǎn)換處理。

if(partialImageRef) {

constsize_t partialHeight =CGImageGetHeight(partialImageRef);

CGColorSpaceRefcolorSpace =CGColorSpaceCreateDeviceRGB();

CGContextRefbmContext =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

// 9. 對圖片進(jìn)行縮放、解碼操作

if(partialImageRef) {

#if SD_UIKIT || SD_WATCH

UIImage*image = [UIImageimageWithCGImage:partialImageRef scale:1orientation:orientation];

#elif SD_MAC

UIImage*image = [[UIImagealloc] initWithCGImage:partialImageRef size:NSZeroSize];

#endif

NSString*key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];

UIImage*scaledImage = [selfscaledImageForKey:key image:image];

if(self.shouldDecompressImages) {

image = [UIImagedecodedImageWithImage:scaledImage];

}

else{

image = scaledImage;

}

CGImageRelease(partialImageRef);

[selfcallCompletionBlocksWithImage:image imageData:nilerror:nilfinished:NO];

}

}

CFRelease(imageSource);

}

for(SDWebImageDownloaderProgressBlock progressBlockin[selfcallbacksForKey:kProgressCallbackKey]) {

progressBlock(self.imageData.length,self.expectedSize,self.request.URL);

}

}

當(dāng)然,在下載完成或下載失敗后,會調(diào)用NSURLSessionTaskDelegate的- (void)URLSession: task: didCompleteWithError:代理方法,并清除連接,并拋出下載停止的通知。如果下載成功,則會處理完整的圖片數(shù)據(jù),對其進(jìn)行適當(dāng)?shù)目s放與解壓縮操作,以提供給完成回調(diào)使用。

小結(jié)

下載的核心其實(shí)就是利用NSURLSession對象來加載數(shù)據(jù)。每個(gè)圖片的下載都由一個(gè)Operation操作來完成,并將這些操作放到一個(gè)操作隊(duì)列中。這樣可以實(shí)現(xiàn)圖片的并發(fā)下載。

緩存

為了減少網(wǎng)絡(luò)流量的消耗,我們都希望下載下來的圖片緩存到本地,下次再去獲取同一張圖片時(shí),可以直接從本地獲取,而不再從遠(yuǎn)程服務(wù)器獲取。這樣做的一個(gè)好處是提升了用戶體驗(yàn),用戶第二次查看同一幅圖片時(shí),能快速從本地獲取圖片直接呈現(xiàn)給用戶。

SDWebImage提供了對圖片緩存的支持,而該功能是由SDImageCache類完成的。該類負(fù)責(zé)處理內(nèi)存緩存及一個(gè)可選的磁盤緩存。其中磁盤緩存的寫操作是異步的,這樣就不會對UI操作造成影響。

配置

另外說明,在4.0以后新添加一個(gè)緩存配置類SDImageCacheConfig,主要是一些緩存策略的配置。其頭文件定義如下:

/**

是否在緩存的時(shí)候解壓縮,默認(rèn)是YES 可以提高性能 但是會耗內(nèi)存。 當(dāng)使用SDWebImage 因?yàn)閮?nèi)存而崩潰 可以將其設(shè)置為NO

*/

@property(assign,nonatomic)BOOLshouldDecompressImages;

/**

* 是否禁用 iCloud 備份 默認(rèn)YES

*/

@property(assign,nonatomic)BOOLshouldDisableiCloud;

/**

* 內(nèi)存緩存? 默認(rèn)YES

*/

@property(assign,nonatomic)BOOLshouldCacheImagesInMemory;

/**

* 最大磁盤緩存時(shí)間 默認(rèn)一周 單位秒

*/

@property(assign,nonatomic)NSIntegermaxCacheAge;

/**

* 最大緩存容量 0 表示無限緩存? 單位字節(jié)

*/

@property(assign,nonatomic)NSUIntegermaxCacheSize;

內(nèi)存緩存及磁盤緩存

內(nèi)存緩存的處理使用NSCache對象來實(shí)現(xiàn)的。NSCache是一個(gè)類似與集合的容器。它存儲key-value對,這一點(diǎn)類似于NSDictionary類。我們通常使用緩存來臨時(shí)存儲短時(shí)間使用但創(chuàng)建昂貴的對象。重用這些對象可以優(yōu)化性能,因?yàn)樗鼈兊闹挡恍枰匦掠?jì)算。另外一方面,這些對象對于程序員來說不是緊要的,在內(nèi)存緊張時(shí)會被丟棄。

磁盤緩存的處理則是使用NSFileManager對象來實(shí)現(xiàn)的。圖片存儲的位置是位于Caches文件夾中的default文件夾下。另外,SDImageCache還定義了一個(gè)串行隊(duì)列,來異步存儲圖片。

內(nèi)存緩存與磁盤緩存相關(guān)變量的聲明及定義如下:

@interfaceSDImageCache()

#pragma mark - Properties

@property(strong,nonatomic,nonnull)NSCache*memCache;

@property(strong,nonatomic,nonnull)NSString*diskCachePath;

@property(strong,nonatomic,nullable)NSMutableArray *customPaths;

@property(SDDispatchQueueSetterSementics,nonatomic,nullable)dispatch_queue_tioQueue;

@end

@implementationSDImageCache{

NSFileManager*_fileManager;

}

- (nonnullinstancetype)initWithNamespace:(nonnullNSString*)ns

diskCacheDirectory:(nonnullNSString*)directory {

if((self= [superinit])) {

NSString*fullNamespace = [@"com.hackemist.SDWebImageCache."stringByAppendingString:ns];

// 隊(duì)列

_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);

//緩存配置

_config = [[SDImageCacheConfig alloc] init];

// 內(nèi)存緩存

_memCache = [[AutoPurgeCache alloc] init];

_memCache.name = fullNamespace;

// 初始化磁盤緩存路徑

if(directory !=nil) {

_diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];

}else{

NSString*path = [selfmakeDiskCachePath:ns];

_diskCachePath = path;

}

dispatch_sync(_ioQueue, ^{

_fileManager = [NSFileManagernew];

});

}

returnself;

}

@end

SDImageCache提供了大量方法來緩存、獲取、移除、及清空圖片。而對于每一個(gè)圖片,為了方便地在內(nèi)存或磁盤中對它進(jìn)行這些操作,我們需要一個(gè)key值來索引它。在內(nèi)存中,我們將其作為NSCache的key值,而在磁盤中,我們用作這個(gè)key作為圖片的文件名。對于一個(gè)遠(yuǎn)程服務(wù)器下載的圖片,其url實(shí)作為這個(gè)key的最佳選擇了。我們在后面會看到這個(gè)key值得重要性。

存儲圖片

我們先來看看圖片的緩存操作,該操作會在內(nèi)存中放置一份緩存,而如果確定需要緩存到磁盤,則將磁盤緩存操作作為一個(gè)task放到串行隊(duì)列中處理。在iOS中,會先檢測圖片是PNG還是JPEG,并將其轉(zhuǎn)換為相應(yīng)的圖片數(shù)據(jù),最后將數(shù)據(jù)寫入到磁盤中(文件名是對key值做MD5摘要后的串)。緩存操作的基礎(chǔ)方法是:-storeImage:imageData:forKey:toDisk:completion:,它的具體實(shí)現(xiàn)如下:

- (void)storeImage:(nullableUIImage*)image

imageData:(nullableNSData*)imageData

forKey:(nullableNSString*)key

toDisk:(BOOL)toDisk

completion:(nullableSDWebImageNoParamsBlock)completionBlock {

if(!image || !key) {

if(completionBlock) {

completionBlock();

}

return;

}

// 內(nèi)存緩存 將其存入NSCache中,同時(shí)傳入圖片的消耗值

if(self.config.shouldCacheImagesInMemory) {

NSUIntegercost = SDCacheCostForImage(image);

[self.memCache setObject:image forKey:key cost:cost];

}

// 如果確定需要磁盤緩存,則將緩存操作作為一個(gè)任務(wù)放入ioQueue中

if(toDisk) {

dispatch_async(self.ioQueue, ^{

NSData*data = imageData;

if(!data && image) {

//如果imageData為nil 需要確定圖片是PNG還是JPEG。PNG圖片容易檢測,因?yàn)橛幸粋€(gè)唯一簽名。PNG圖像的前8個(gè)字節(jié)總是包含以下值:137 80 78 71 13 10 26 10

//判斷 圖片是何種類型 使用 sd_imageFormatForImageData 來判斷

// SDImageFormat 是一個(gè)枚舉? 其定義如下:

//? ? ? ? ? ? ? ? typedef NS_ENUM(NSInteger, SDImageFormat) {

//? ? ? ? ? ? ? ? ? ? SDImageFormatUndefined = -1,

//? ? ? ? ? ? ? ? ? ? SDImageFormatJPEG = 0,

//? ? ? ? ? ? ? ? ? ? SDImageFormatPNG,

//? ? ? ? ? ? ? ? ? ? SDImageFormatGIF,

//? ? ? ? ? ? ? ? ? ? SDImageFormatTIFF,

//? ? ? ? ? ? ? ? ? ? SDImageFormatWebP

//? ? ? ? ? ? ? ? };

SDImageFormat imageFormatFromData = [NSDatasd_imageFormatForImageData:data];

//根據(jù)圖片類型 轉(zhuǎn)成data

data = [image sd_imageDataAsFormat:imageFormatFromData];

}

// 4. 創(chuàng)建緩存文件并存儲圖片

[selfstoreImageDataToDisk:data forKey:key];

if(completionBlock) {

dispatch_async(dispatch_get_main_queue(), ^{

completionBlock();

});

}

});

}else{

if(completionBlock) {

completionBlock();

}

}

}

查詢圖片

如果我們想在內(nèi)存或磁盤中查詢是否有key指定的圖片,則可以分別使用以下方法:

//快速查詢圖片是否已經(jīng)磁盤緩存 不返回圖片 只做快速查詢 異步操作

- (void)diskImageExistsWithKey:(nullableNSString*)key completion:(nullableSDWebImageCheckCacheCompletionBlock)completionBlock;

//異步查詢圖片 不管是內(nèi)存緩存還是磁盤緩存

- (nullableNSOperation*)queryCacheOperationForKey:(nullableNSString*)key done:(nullableSDCacheQueryCompletedBlock)doneBlock;

//從內(nèi)存中查詢圖片

- (nullableUIImage*)imageFromMemoryCacheForKey:(nullableNSString*)key;

//從磁盤中查詢圖片

- (nullableUIImage*)imageFromDiskCacheForKey:(nullableNSString*)key;

//同步查詢圖片,不管是內(nèi)存緩存還是磁盤緩存

- (nullableUIImage*)imageFromCacheForKey:(nullableNSString*)key;

其實(shí)- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key內(nèi)部實(shí)現(xiàn)是調(diào)用了- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key和- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key方法,如下:

- (nullableUIImage*)imageFromCacheForKey:(nullableNSString*)key {

// 從緩存中查找圖片

UIImage*image = [selfimageFromMemoryCacheForKey:key];

if(image) {

returnimage;

}

// 從磁盤中查找圖片

image = [selfimageFromDiskCacheForKey:key];

returnimage;

}

我們再來看看異步查詢圖片的具體實(shí)現(xiàn):

- (nullableNSOperation*)queryCacheOperationForKey:(nullableNSString*)key done:(nullableSDCacheQueryCompletedBlock)doneBlock {

...

// 1. 首先查看內(nèi)存緩存,如果查找到,則直接回調(diào)doneBlock并返回

UIImage*image = [selfimageFromMemoryCacheForKey:key];

if(image) {

NSData*diskData =nil;

//進(jìn)行了是否是GIF的判斷

if([image isGIF]) {

diskData = [selfdiskImageDataBySearchingAllPathsForKey:key];

}

if(doneBlock) {

doneBlock(image, diskData, SDImageCacheTypeMemory);

}

returnnil;

}

// 2. 如果內(nèi)存中沒有,則在磁盤中查找。如果找到,則將其放到內(nèi)存緩存,并調(diào)用doneBlock回調(diào)

NSOperation*operation = [NSOperationnew];

dispatch_async(self.ioQueue, ^{

if(operation.isCancelled) {

// do not call the completion if cancelled

return;

}

@autoreleasepool{

NSData*diskData = [selfdiskImageDataBySearchingAllPathsForKey:key];

UIImage*diskImage = [selfdiskImageForKey:key];

if(diskImage &&self.config.shouldCacheImagesInMemory) {

//進(jìn)行內(nèi)存緩存

NSUIntegercost = SDCacheCostForImage(diskImage);

[self.memCache setObject:diskImage forKey:key cost:cost];

}

if(doneBlock) {

dispatch_async(dispatch_get_main_queue(), ^{

doneBlock(diskImage, diskData, SDImageCacheTypeDisk);

});

}

}

});

returnoperation;

}

移除圖片

圖片的移除操作則可以使用以下方法:

//從內(nèi)存和磁盤中移除圖片

- (void)removeImageForKey:(nullableNSString*)key withCompletion:(nullableSDWebImageNoParamsBlock)completion;

//從內(nèi)存 或 可選磁盤中移除圖片

- (void)removeImageForKey:(nullableNSString*)key fromDisk:(BOOL)fromDisk withCompletion:(nullableSDWebImageNoParamsBlock)completion;

我們可以選擇同時(shí)移除內(nèi)存及磁盤上的圖片,或者只移除內(nèi)存中的圖片。

清理圖片

磁盤緩存圖片的操作可以分為完全清空和部分清理。完全清空操作是直接把緩存的文件夾移除,部分清理是清理掉過時(shí)的舊圖片,清空操作有以下方法:

//清除內(nèi)存緩存

- (void)clearMemory;

//完全清空磁盤緩存

- (void)clearDiskOnCompletion:(nullableSDWebImageNoParamsBlock)completion;

//清空舊圖片

- (void)deleteOldFilesWithCompletionBlock:(nullableSDWebImageNoParamsBlock)completionBlock;

而部分清理則是根據(jù)我們設(shè)定的一些參數(shù)來移除一些文件,這里主要有兩個(gè)指標(biāo):文件的緩存有效期及最大緩存空間大小。文件的緩存有效期可以通過SDImageCacheConfig類的maxCacheAge屬性來設(shè)置,默認(rèn)是1周的時(shí)間。如果文件的緩存時(shí)間超過這個(gè)時(shí)間值,則將其移除。而最大緩存空間大小是通過maxCacheSize屬性來設(shè)置的,如果所有緩存文件的總大小超過這一大小,則會按照文件最后修改時(shí)間的逆序,以每次一半的遞歸來移除那些過早的文件,直到緩存的實(shí)際大小小于我們設(shè)置的最大使用空間。清理的操作在-deleteOldFilesWithCompletionBlock:方法中,其實(shí)現(xiàn)如下:

- (void)deleteOldFilesWithCompletionBlock:(nullableSDWebImageNoParamsBlock)completionBlock {

dispatch_async(self.ioQueue, ^{

NSURL*diskCacheURL = [NSURLfileURLWithPath:self.diskCachePath isDirectory:YES];

NSArray *resourceKeys = @[NSURLIsDirectoryKey,NSURLContentModificationDateKey,NSURLTotalFileAllocatedSizeKey];

// 1. 該枚舉器預(yù)先獲取緩存文件的有用的屬性

NSDirectoryEnumerator*fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL

includingPropertiesForKeys:resourceKeys

options:NSDirectoryEnumerationSkipsHiddenFiles

errorHandler:NULL];

NSDate*expirationDate = [NSDatedateWithTimeIntervalSinceNow:-self.config.maxCacheAge];

NSMutableDictionary *> *cacheFiles = [NSMutableDictionarydictionary];

NSUIntegercurrentCacheSize =0;

// 2. 枚舉緩存文件夾中所有文件,該迭代有兩個(gè)目的:移除比過期日期更老的文件;存儲文件屬性以備后面執(zhí)行基于緩存大小的清理操作

NSMutableArray *urlsToDelete = [[NSMutableArrayalloc] init];

for(NSURL*fileURLinfileEnumerator) {

NSError*error;

NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];

// 3. 跳過文件夾

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

continue;

}

// 4. 移除早于有效期的老文件

NSDate*modificationDate = resourceValues[NSURLContentModificationDateKey];

if([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {

[urlsToDelete addObject:fileURL];

continue;

}

// 5. 存儲文件的引用并計(jì)算所有文件的總大小,以備后用

NSNumber*totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];

currentCacheSize += totalAllocatedSize.unsignedIntegerValue;

cacheFiles[fileURL] = resourceValues;

}

for(NSURL*fileURLinurlsToDelete) {

[_fileManager removeItemAtURL:fileURL error:nil];

}

//6.如果磁盤緩存的大小大于我們配置的最大大小,則執(zhí)行基于文件大小的清理,我們首先刪除最老的文件

if(self.config.maxCacheSize >0&& currentCacheSize >self.config.maxCacheSize) {

// 7. 以設(shè)置的最大緩存大小的一半作為清理目標(biāo)

constNSUIntegerdesiredCacheSize =self.config.maxCacheSize /2;

// 8. 按照最后修改時(shí)間來排序剩下的緩存文件

NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent

usingComparator:^NSComparisonResult(idobj1,idobj2) {

return[obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];

}];

// 9. 刪除文件,直到緩存總大小降到我們期望的大小

for(NSURL*fileURLinsortedFiles) {

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

});

}

});

}

小結(jié)

以上分析了圖片緩存操作,當(dāng)然,除了上面講的幾個(gè)操作,SDWebImage類還提供了一些輔助方法。如獲取緩存大小、緩存中圖片的數(shù)量、判斷緩存中是否存在某個(gè)key指定的圖片。另外,SDWebImage類提供了一個(gè)單例方法的實(shí)現(xiàn),所以我們可以將其當(dāng)做單例對象來處理。

SDWebImageManager

在實(shí)際的運(yùn)用中,我們并不直接使用SDWebImageDownloader類及SDImageCache類來執(zhí)行圖片的下載及緩存。為了方便用戶的使用,SDWebImage提供了SDWebImageManager對象來管理圖片的下載與緩存。而我們經(jīng)常用到的諸如UIImageView+WebCache等控件的分類都是基于SDWebImageManager對象的,該對象將一個(gè)下載器和一個(gè)圖片緩存綁定在一起,并對外提供兩個(gè)只讀屬性來獲取它們,如下代碼所示:

@interfaceSDWebImageManager:NSObject

@property(weak,nonatomic)id delegate;

@property(strong,nonatomic,readonly) SDImageCache *imageCache;

@property(strong,nonatomic,readonly) SDWebImageDownloader *imageDownloader;

...

@end

從上面的代碼中我們還可以看到一個(gè)delegate屬性,它是一個(gè)id 對象。SDWebImageManagerDelegate聲明了兩個(gè)可選實(shí)現(xiàn)的方法,如下所示:

// 控制當(dāng)圖片在緩存中沒有找到時(shí),應(yīng)該下載哪個(gè)圖片

- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL*)imageURL;

// 允許在圖片已經(jīng)被下載完成且被緩存到磁盤或內(nèi)存前立即轉(zhuǎn)換

- (UIImage*)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage*)image withURL:(NSURL*)imageURL;

這兩個(gè)代理方法會在SDWebImageManager的-downloadImageWithURL:options:progress:completed:方法中調(diào)用,而這個(gè)方法是SDWebImageManager類的核心所在。我們來看看它具體的實(shí)現(xiàn):

- (id)loadImageWithURL:(nullableNSURL*)url

options:(SDWebImageOptions)options

progress:(nullableSDWebImageDownloaderProgressBlock)progressBlock

completed:(nullableSDInternalCompletionBlock)completedBlock {

...

// 前面省略n行。主要作了如下處理:

// 1. 判斷url的合法性

// 2. 創(chuàng)建SDWebImageCombinedOperation對象

// 3. 查看url是否是之前下載失敗過的

// 4. 如果url為nil,或者在不可重試的情況下是一個(gè)下載失敗過的url,則直接返回操作對象并調(diào)用完成回調(diào)

operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage*cachedImage,NSData*cachedData, SDImageCacheType cacheType) {

if(operation.isCancelled) {

[selfsafelyRemoveOperationFromRunning:operation];

return;

}

//先去緩存中查找圖片,如果圖片不存在? 或者 當(dāng)前圖片的下載模式是 SDWebImageRefreshCached 開始下載

if((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:selfshouldDownloadImageForURL:url])) {

...

//下載

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

__strong__typeof(weakOperation) strongOperation = weakOperation;

if(!strongOperation || strongOperation.isCancelled) {

// 操作被取消,則不做任務(wù)事情

}elseif(error) {

// 如果出錯(cuò),則調(diào)用完成回調(diào),并將url放入下載失敗url數(shù)組中

...

}

else{

...

BOOLcacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

if(options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {

// Image refresh hit the NSURLCache cache, do not call the completion block

}elseif(downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {

// 在全局隊(duì)列中并行處理圖片的緩存

// 首先對圖片做個(gè)轉(zhuǎn)換操作,該操作是代理對象實(shí)現(xiàn)的

// 然后對圖片做緩存處理

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0), ^{

UIImage*transformedImage = [self.delegate imageManager:selftransformDownloadedImage:downloadedImage withURL:url];

if(transformedImage && finished) {

BOOLimageWasTransformed = ![transformedImage isEqual:downloadedImage];

// pass nil if the image was transformed, so we can recalculate the data from the image

[self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ?nil: downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];

}

...

});

}else{

if(downloadedImage && finished) {

[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];

}

...

}

}

// 下載完成并緩存后,將操作從隊(duì)列中移除

if(finished) {

[selfsafelyRemoveOperationFromRunning:strongOperation];

}

}];

operation.cancelBlock = ^{

[self.imageDownloader cancel:subOperationToken];

__strong__typeof(weakOperation) strongOperation = weakOperation;

[selfsafelyRemoveOperationFromRunning:strongOperation];

};

}elseif(cachedImage) {

...

}else{

...

}

}];

returnoperation;

}

對于這個(gè)方法,我們沒有做過多的解釋。其主要就是下載圖片并根據(jù)操作選項(xiàng)緩存圖片。上面這個(gè)下載方法的操作選項(xiàng)參數(shù)是由枚舉SDWebImageOptions來定義的,這個(gè)操作中的一些選項(xiàng)是與SDWebImageDownloaderOptions中的選項(xiàng)對應(yīng)的,我們來看看這個(gè)SDWebImageOptions選項(xiàng)都有哪些:

typedefNS_OPTIONS(NSUInteger, SDWebImageOptions) {

// 默認(rèn)情況下,當(dāng)URL下載失敗時(shí),URL會被列入黑名單,導(dǎo)致庫不會再去重試,該標(biāo)記用于禁用黑名單

SDWebImageRetryFailed =1<<0,

// 默認(rèn)情況下,圖片下載開始于UI交互,該標(biāo)記禁用這一特性,這樣下載延遲到UIScrollView減速時(shí)

SDWebImageLowPriority =1<<1,

// 該標(biāo)記禁用磁盤緩存

SDWebImageCacheMemoryOnly =1<<2,

// 該標(biāo)記啟用漸進(jìn)式下載,圖片在下載過程中是漸漸顯示的,如同瀏覽器一下。

// 默認(rèn)情況下,圖像在下載完成后一次性顯示

SDWebImageProgressiveDownload =1<<3,

// 即使圖片緩存了,也期望HTTP響應(yīng)cache control,并在需要的情況下從遠(yuǎn)程刷新圖片。

// 磁盤緩存將被NSURLCache處理而不是SDWebImage,因?yàn)镾DWebImage會導(dǎo)致輕微的性能下載。

// 該標(biāo)記幫助處理在相同請求URL后面改變的圖片。如果緩存圖片被刷新,則完成block會使用緩存圖片調(diào)用一次

// 然后再用最終圖片調(diào)用一次

SDWebImageRefreshCached =1<<4,

// 在iOS 4+系統(tǒng)中,當(dāng)程序進(jìn)入后臺后繼續(xù)下載圖片。這將要求系統(tǒng)給予額外的時(shí)間讓請求完成

// 如果后臺任務(wù)超時(shí),則操作被取消

SDWebImageContinueInBackground =1<<5,

// 通過設(shè)置NSMutableURLRequest.HTTPShouldHandleCookies = YES;來處理存儲在NSHTTPCookieStore中的cookie

SDWebImageHandleCookies =1<<6,

// 允許不受信任的SSL認(rèn)證

SDWebImageAllowInvalidSSLCertificates =1<<7,

// 默認(rèn)情況下,圖片下載按入隊(duì)的順序來執(zhí)行。該標(biāo)記將其移到隊(duì)列的前面,

// 以便圖片能立即下載而不是等到當(dāng)前隊(duì)列被加載

SDWebImageHighPriority =1<<8,

// 默認(rèn)情況下,占位圖片在加載圖片的同時(shí)被加載。該標(biāo)記延遲占位圖片的加載直到圖片已以被加載完成

SDWebImageDelayPlaceholder =1<<9,

// 通常我們不調(diào)用動畫圖片的transformDownloadedImage代理方法,因?yàn)榇蠖鄶?shù)轉(zhuǎn)換代碼可以管理它。

// 使用這個(gè)票房則不任何情況下都進(jìn)行轉(zhuǎn)換。

SDWebImageTransformAnimatedImage =1<<10,

};

大家再看-downloadImageWithURL:options:progress:completed:,可以看到兩個(gè)SDWebImageOptions與SDWebImageDownloaderOptions中的選項(xiàng)是如何對應(yīng)起來的,在此不多做解釋。

視圖擴(kuò)展

我們在使用SDWebImage的時(shí)候,使用最多的是UIImageView+WebCache中的針對UIImageView的擴(kuò)展方法,這些擴(kuò)展方法將UIImageView與WebCache集成在一起,來讓UIImageView對象擁有異步下載和緩存遠(yuǎn)程圖片的能力。在4.0.0版本以后,給UIView新增了好多方法,其中最之前UIImageView+WebCache最核心的方法-sd_setImageWithURL:placeholderImage:options:progress:completed:,現(xiàn)在使用的是UIView+WebCache中新增的方法sd_internalSetImageWithURL:placeholderImage:options:operationKey:setImageBlock:progress:completed:,其使用SDWebImageManager單例對象下載并緩存圖片,完成后將圖片賦值給UIImageView對象的image屬性,以使圖片顯示出來,其具體實(shí)現(xiàn)如下:

- (void)sd_internalSetImageWithURL:(nullableNSURL*)url

placeholderImage:(nullableUIImage*)placeholder

options:(SDWebImageOptions)options

operationKey:(nullableNSString*)operationKey

setImageBlock:(nullableSDSetImageBlock)setImageBlock

progress:(nullableSDWebImageDownloaderProgressBlock)progressBlock

completed:(nullableSDExternalCompletionBlock)completedBlock {

...

if(url) {

// check if activityView is enabled or not

if([selfsd_showActivityIndicatorView]) {

[selfsd_addActivityIndicator];

}

__weak__typeof(self)wself =self;

// 使用SDWebImageManager單例對象來下載圖片

id operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage*image,NSData*data,NSError*error, SDImageCacheType cacheType,BOOLfinished,NSURL*imageURL) {

__strong__typeof(wself) sself = wself;

[sself sd_removeActivityIndicator];

if(!sself) {

return;

}

dispatch_main_async_safe(^{

if(!sself) {

return;

}

if(image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {

completedBlock(image, error, cacheType, url);

return;

}elseif(image) {

// 圖片下載完后顯示圖片

[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];

[sself sd_setNeedsLayout];

}else{

if((options & SDWebImageDelayPlaceholder)) {

[sself sd_setImage:placeholder imageData:nilbasedOnClassOrViaCustomSetImageBlock:setImageBlock];

[sself sd_setNeedsLayout];

}

}

if(completedBlock && finished) {

completedBlock(image, error, cacheType, url);

}

});

}];

[selfsd_setImageLoadOperation:operation forKey:validOperationKey];

}else{

...

}

}

除了擴(kuò)展UIImageView之外,SDWebImage還擴(kuò)展了UIView、UIButton、MKAnnotationView等視圖類,大家可以參考源碼。

當(dāng)然,如果不想使用這些擴(kuò)展,則可以直接使用SDWebImageManager來下載圖片,這也是很OK的。

技術(shù)點(diǎn)

SDWebImage的主要任務(wù)就是圖片的下載和緩存。為了支持這些操作,它主要使用了以下知識點(diǎn):

dispatch_barrier_sync函數(shù):該方法用于對操作設(shè)置等待,確保在執(zhí)行完任務(wù)后才會執(zhí)行后續(xù)操作。該方法常用于確保類的線程安全性操作。

NSMutableURLRequest:用于創(chuàng)建一個(gè)網(wǎng)絡(luò)請求對象,我們可以根據(jù)需要來配置請求報(bào)頭等信息。

NSOperation及NSOperationQueue:操作隊(duì)列是Objective-C中一種高級的并發(fā)處理方法,現(xiàn)在它是基于GCD來實(shí)現(xiàn)的。相對于GCD來說,操作隊(duì)列的優(yōu)點(diǎn)是可以取消在任務(wù)處理隊(duì)列中的任務(wù),另外在管理操作間的依賴關(guān)系方面也容易一些。對SDWebImage中我們就看到了如何使用依賴將下載順序設(shè)置成后進(jìn)先出的順序。(有興趣的同學(xué)可以看看我這篇博客->聊一聊NSOperation的那些事)

NSURLSession:用于網(wǎng)絡(luò)請求及響應(yīng)處理。在iOS7.0后,蘋果推出了一套新的網(wǎng)絡(luò)請求接口,即NSURLSession類。(有興趣的同學(xué)可以看看我這篇博客->NSURLSession與NSURLConnection區(qū)別)

開啟一個(gè)后臺任務(wù)。

NSCache類:一個(gè)類似于集合的容器。它存儲key-value對,這一點(diǎn)類似于NSDictionary類。我們通常用使用緩存來臨時(shí)存儲短時(shí)間使用但創(chuàng)建昂貴的對象。重用這些對象可以優(yōu)化性能,因?yàn)樗鼈兊闹挡恍枰匦掠?jì)算。另外一方面,這些對象對于程序來說不是緊要的,在內(nèi)存緊張時(shí)會被丟棄。

清理緩存圖片的策略:特別是最大緩存空間大小的設(shè)置。如果所有緩存文件的總大小超過這一大小,則會按照文件最后修改時(shí)間的逆序,以每次一半的遞歸來移除那些過早的文件,直到緩存的實(shí)際大小小于我們設(shè)置的最大使用空間。

對圖片的解壓縮操作:這一操作可以查看SDWebImageDecoder.m中+decodedImageWithImage方法的實(shí)現(xiàn)。

對GIF圖片的處理

對WebP圖片的處理

對cell的重用機(jī)制的解決,利用runtime的關(guān)聯(lián)對象,會為imageView對象關(guān)聯(lián)一個(gè)下載列表,當(dāng)tableView滑動時(shí),imageView重設(shè)數(shù)據(jù)源(url)時(shí),會cancel掉下載列表中所有的任務(wù),然后開啟一個(gè)新的下載任務(wù)。這樣子就保證了只有當(dāng)前可見的cell對象的imageView對象關(guān)聯(lián)的下載任務(wù)能夠回調(diào),不會發(fā)生image錯(cuò)亂。

感興趣的同學(xué)可以深入研究一下這些知識點(diǎn)。當(dāng)然,這只是其中一部分,更多的知識還有待大家去發(fā)掘。

標(biāo)簽

源碼分析

上一篇

下一篇

網(wǎng)友跟貼

0人參與

留下你的??吧^_^

快速登錄:

發(fā)表跟貼

最新

最熱

網(wǎng)易云跟貼,有你更精彩

Copyrights ? 2017 貴永冬. All Rights Reserved.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容