UITableView 重用 UITableViewCell 并异步加载图片时会出现图片错乱的情况
对错位原因不明白的同学请参考我的另外一篇随笔:http://www.cnblogs.com/lesliefang/p/3619223.html 。
当然大多数情况下可以用 SDWebImage, 这个库功能强大,封装的很好。但自己重头来写可能对问题理解的更深。
SDWebImage 有点复杂,很多人也会参考一下封装出一套适合自己的类库。
基本思路如下:
1 扩展(category) UIImageView, 这样写出的代码更整洁
2 大家都知道图片要开子线程异步下载,这里使用 GCD
3 重用 UITableViewCell 加异步下载会出现图片错位,所以每次 cell 渲染时都要预设一个图片 (placeholder),以覆盖先前由于 cell 重用可能存在的图片
4 内存 + 文件 二级缓存, 内存缓存基于 NSCache
暂时没有考虑 cell 划出屏幕的情况,一是没看明白 SDWebImage 是怎么判断滑出屏幕并 cancel 掉队列中对应的请求的
二是我觉得用户很多情况下滑下去一般还会滑回来,预加载一下也挺好。坏处是对当前页图片加载性能上有点小影响。
关键代码如下:
1 扩展 UIImageView
@implementation UIImageView (AsyncDownload) - (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder{ // 预设一个图片,可以为 nil self.image = placeholder; if (url) { // 异步下载图片 LeslieAsyncImageDownloader *imageLoader = [LeslieAsyncImageDownloader sharedImageLoader]; [imageLoader downloadImageWithURL:url complete:^(UIImage *image, NSError *error, NSURL *imageURL) { if (image) { // 下载完成后设置图片 self.image = image; }else{ NSLog(@"error when download:%@", error); } }]; } }
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