本文是學(xué)習(xí) SDWebImage 的產(chǎn)物,如果有不對(duì)的地方,歡迎指正。
客戶端開發(fā)中,圖片下載控件一定是工程里比不可少的,它的重要性不亞于網(wǎng)絡(luò)庫。下面將一步一步的從開始最簡單的圖片下載到最后的完整控件,來剖析下載控件之實(shí)現(xiàn)。
-
牛刀小試
圖片下載需要一個(gè)url,根據(jù)url取到內(nèi)容,最后展示到控件上。將這個(gè)通過url下載圖片的過程封裝成為一個(gè)UIImageView的擴(kuò)展方法再合適不過了。代碼如下:
@interface UIImageView (XXXCatorgory) - (void)XXX_setImageWithURL:(NSURL *)URL; @end @implementation UIImageView (XXXCatorgory) - (void)XXX_setImageWithURL:(NSURL *)URL { NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:URL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (!error && data) { self.image = [UIImage imageWithData:data]; } }]; [task resume]; } @end
-
漸入佳境
上面的牛刀小試,已經(jīng)完成了最簡單的圖片下載封裝。有了這個(gè)擴(kuò)展,只要有url,就可以完成下載功能了。但是,是不是就這么簡單呢,這樣的封裝又會(huì)有什么樣的問題?
一個(gè)很明顯的,是所有的操作,都在主線程上完成的。程序的主線程是如此寶貴,以至于它一旦忙碌起來,可能會(huì)造成界面卡頓。雖然NSURLSession的下載是異步的,可是如果界面上有很多張圖片要獲取,就會(huì)在主線程上起多個(gè)任務(wù),肯定是不合適的。下面我們將下載操作放到異步線程,只把必要的UI操作放回主線程中。
- (void)XXX_setImageWithURL:(NSURL *)URL { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:URL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (!error && data) { dispatch_async(dispatch_get_main_queue(), ^{ self.image = [UIImage imageWithData:data]; }); } }]; [task resume]; }); }
問題依然存在,這樣的任務(wù)一旦開始就無法取消了,而且多個(gè)任務(wù)可能會(huì)起很多線程。因此有必要將所有的下載任務(wù)放到一個(gè)獨(dú)立的地方從而方便控制——下載器應(yīng)運(yùn)而生,它擁有一個(gè)隊(duì)列,能夠控制并發(fā),并且返回下載操作。由于代碼較多,下面只放頭文件了,完整代碼放在文末的鏈接里:
@interface XXXDownloader : NSObject @property(nonatomic) NSInteger maxConcurrent; + (instancetype)sharedDownloader; - (NSOperation *)downloadOperationWithURL:(NSURL *)URL completionHandler:(void(^)(NSData *data, NSError *error))handler; @end
將下載操作設(shè)置為圖片的關(guān)聯(lián)對(duì)象,此時(shí)UIImageView的方法就變成下面這樣:
- (void)XXX_setImageWithURL:(NSURL *)URL {
NSOperation *op = self.downloadOperation;
if (op && !op.finished) {
[op cancel];
}
op = [[XXXDownloader sharedDownloader] downloadOperationWithURL:URL completionHandler:^(NSData *data, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (!error && data) {
self.image = [UIImage imageWithData:data];
}
});
}];
[self setDownloadOperation:op];
}
至此,已經(jīng)完成下載控件的初步封裝。所有跟下載相關(guān)的操作,比如對(duì)不合格的URL進(jìn)行過濾,設(shè)置下載超時(shí)時(shí)間等,都可以放到下載器里完成。
-
鋒芒畢露
完成以上兩步是不是就大功告成了呢?并不是,我們還沒有使用緩存呢!怎么可以容忍同一個(gè)圖片下載兩次相同的url呢。緩存分為兩種,內(nèi)存緩存和磁盤緩存。下面我們?yōu)橄螺d控件加入緩存,很明顯,它也應(yīng)該是個(gè)獨(dú)立的系統(tǒng),這樣能夠方便的控制緩存大小,從而可以在適當(dāng)?shù)臅r(shí)機(jī)清除。
@interface XXXImageCache : NSObject + (instancetype)sharedCache; - (void)cacheData:(UIImage *)image forKey:(NSString *)key; - (NSData *)cachedData:(NSString *)key; - (void)clearMemCache; - (void)clearDiskCache; @end
可以在下載器下載完成后進(jìn)行緩存,而在UIImageView的方法里判斷緩存是否存在,代碼如下:
- (void)XXX_setImageWithURL:(NSURL *)URL { NSData *data = [[XXXImageCache sharedCache] cachedData:[URL absoluteString]]; if (image) { self.image = [UIImage imageWithData:data]; return; } NSOperation *op = self.downloadOperation;; if (op && !op.finished) { [task cancel]; } op = [[TSDDownloader sharedDownloader] downloadOperationWithURL:URL completionHandler:^(NSData *data, NSError *error) { dispatch_async(dispatch_get_main_queue(), ^{ if (!error && data) { self.image = [UIImage imageWithData:data]; } }); }]; [self setDownloadOperation:op]; }
總結(jié)
從上面可以看出,實(shí)現(xiàn)一個(gè)基本的圖片下載控件的思路,先是簡單的下載封裝,然后考慮異步操作并且控制并發(fā),最后加入緩存系統(tǒng)。SDWebImage實(shí)現(xiàn)了更豐富的功能,如placeholder、下載進(jìn)度、支持更多圖片類型等,但是道理應(yīng)該是相通的。
這篇文章只是簡單的介紹了從0到1的過程,從1到100,還有很長的路要走。希望后面還有機(jī)會(huì)繼續(xù)來完善這篇文章。