UITableView 重用 UITableViewCell 並異步加載圖片時會出現圖片錯亂的狀況html
對錯位緣由不明白的同窗請參考個人另一篇隨筆:http://www.cnblogs.com/lesliefang/p/3619223.html 。git
固然大多數狀況下能夠用 SDWebImage, 這個庫功能強大,封裝的很好。但本身重頭來寫可能對問題理解的更深。github
SDWebImage 有點複雜,不少人也會參考一下封裝出一套適合本身的類庫。緩存
基本思路以下:網絡
1 擴展(category) UIImageView, 這樣寫出的代碼更整潔app
2 GCD 異步下載 異步
3 重用 UITableViewCell 加異步下載會出現圖片錯位,因此每次 cell 渲染時都要預設一個圖片 (placeholder),async
以覆蓋先前因爲 cell 重用可能存在的圖片, 同時要給 UIImageView 設置 tag 以防止錯位。性能
4 內存 + 文件 二級緩存, 內存緩存基於 NSCacheatom
暫時沒有考慮 cell 劃出屏幕的狀況,一是沒看明白 SDWebImage 是怎麼判斷滑出屏幕並 cancel 掉隊列中對應的請求的
二是我以爲用戶不少狀況下滑下去通常還會滑回來,預加載一下也挺好。壞處是對當前頁圖片加載性能上有點小影響。
關鍵代碼以下:
1 擴展 UIImageView
@interface UIImageView (AsyncDownload) // 經過爲 ImageView 設置 tag 防止錯位 // tag 指向的永遠是當前可見圖片的 url, 這樣經過 tag 就能夠過濾掉已經滑出屏幕的圖片的 url @property NSString *tag; - (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder; @end #import "UIImageView+AsyncDownload.h" @implementation UIImageView (AsyncDownload) - (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder{ // 給 ImageView 設置 tag, 指向當前 url self.tag = [url absoluteString]; // 預設一個圖片,能夠爲 nil // 主要是爲了清除因爲複用之前可能存在的圖片 self.image = placeholder; if (url) { // 異步下載圖片 LeslieAsyncImageDownloader *imageLoader = [LeslieAsyncImageDownloader sharedImageLoader]; [imageLoader downloadImageWithURL:url complete:^(UIImage *image, NSError *error, NSURL *imageURL) { // 經過 tag 保證圖片被正確的設置 if (image && [self.tag isEqualToString:[imageURL absoluteString]]) { self.image = image; }else{ NSLog(@"error when download:%@", error); } }]; } } @end
2 GCD 異步下載, 封裝了一個 單例 下載類
@implementation LeslieAsyncImageDownloader +(id)sharedImageLoader{ static LeslieAsyncImageDownloader *sharedImageLoader = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedImageLoader = [[self alloc] init]; }); return sharedImageLoader; } - (void)downloadImageWithURL:(NSURL *)url complete:(ImageDownloadedBlock)completeBlock{ LeslieImageCache *imageCache = [LeslieImageCache sharedCache]; NSString *imageUrl = [url absoluteString]; UIImage *image = [imageCache getImageFromMemoryForkey:imageUrl]; // 先從內存中取 if (image) { if (completeBlock) { NSLog(@"image exists in memory"); completeBlock(image,nil,url); } return; } // 再從文件中取 image = [imageCache getImageFromFileForKey:imageUrl]; if (image) { if (completeBlock) { NSLog(@"image exists in file"); completeBlock(image,nil,url); } // 從新加入到 NSCache 中 [imageCache cacheImageToMemory:image forKey:imageUrl]; return; } // 內存和文件中都沒有再從網絡下載 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSError * error; NSData *imgData = [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:&error]; dispatch_async(dispatch_get_main_queue(), ^{ UIImage *image = [UIImage imageWithData:imgData]; if (image) { // 先緩存圖片到內存 [imageCache cacheImageToMemory:image forKey:imageUrl]; // 再緩存圖片到文件系統 NSString *extension = [[imageUrl substringFromIndex:imageUrl.length-3] lowercaseString]; NSString *imageType = @"jpg"; if ([extension isEqualToString:@"jpg"]) { imageType = @"jpg"; }else{ imageType = @"png"; } [imageCache cacheImageToFile:image forKey:imageUrl ofType:imageType]; } if (completeBlock) { completeBlock(image,error,url); } }); }); } @end
3 內存 + 文件 實現二級緩存,封裝了一個 單例 緩存類
@implementation LeslieImageCache +(LeslieImageCache*)sharedCache { static LeslieImageCache *imageCache = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ imageCache = [[self alloc] init]; }); return imageCache; } -(id)init{ if (self == [super init]) { ioQueue = dispatch_queue_create("com.leslie.LeslieImageCache", DISPATCH_QUEUE_SERIAL); memCache = [[NSCache alloc] init]; memCache.name = @"image_cache"; fileManager = [NSFileManager defaultManager]; NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); cacheDir = [paths objectAtIndex:0]; } return self; } -(void)cacheImageToMemory:(UIImage*)image forKey:(NSString*)key{ if (image) { [memCache setObject:image forKey:key]; } } -(UIImage*)getImageFromMemoryForkey:(NSString*)key{ return [memCache objectForKey:key]; } -(void)cacheImageToFile:(UIImage*)image forKey:(NSString*)key ofType:(NSString*)imageType{ if (!image || !key ||!imageType) { return; } dispatch_async(ioQueue, ^{ // @"http://lh4.ggpht.com/_loGyjar4MMI/S-InbXaME3I/AAAAAAAADHo/4gNYkbxemFM/s144-c/Frantic.jpg" // 從 url 中分離出文件名 Frantic.jpg NSRange range = [key rangeOfString:@"/" options:NSBackwardsSearch]; NSString *filename = [key substringFromIndex:range.location+1]; NSString *filepath = [cacheDir stringByAppendingPathComponent:filename]; NSData *data = nil; if ([imageType isEqualToString:@"jpg"]) { data = UIImageJPEGRepresentation(image, 1.0); }else{ data = UIImagePNGRepresentation(image); } if (data) { [data writeToFile:filepath atomically:YES]; } }); } -(UIImage*)getImageFromFileForKey:(NSString*)key{ if (!key) { return nil; } NSRange range = [key rangeOfString:@"/" options:NSBackwardsSearch]; NSString *filename = [key substringFromIndex:range.location+1]; NSString *filepath = [cacheDir stringByAppendingPathComponent:filename]; if ([fileManager fileExistsAtPath:filepath]) { UIImage *image = [UIImage imageWithContentsOfFile:filepath]; return image; } return nil; } @end
4 使用
自定義 UITableViewCell
@interface LeslieMyTableViewCell : UITableViewCell @property UIImageView *myimage; @end @implementation LeslieMyTableViewCell - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { self.myimage = [[UIImageView alloc] init]; self.myimage.frame = CGRectMake(10, 10, 60, 60); [self addSubview:self.myimage]; } return self; }
cell 被渲染時調用
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *mycellId = @"mycell"; LeslieMyTableViewCell *mycell = [tableView dequeueReusableCellWithIdentifier:mycellId]; if (mycell == nil) { mycell = [[LeslieMyTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:mycellId]; } NSString *imageUrl = data[indexPath.row]; if (imageUrl!=nil && ![imageUrl isEqualToString:@""]) { NSURL *url = [NSURL URLWithString:imageUrl]; [mycell.myimage setImageWithURL:url placeholderImage:nil]; } return mycell; }
demo 地址:https://github.com/lesliebeijing/LeslieAsyncImageLoader.git