您的位置:首页 > 产品设计 > UI/UE

UITableView 重用 UITableViewCell 并异步加载图片时会出现图片错乱的情况

2015-07-07 16:38 501 查看
UITableView 重用 UITableViewCell 并异步加载图片时会出现图片错乱的情况

对错位原因不明白的同学请参考我的另外一篇随笔:http://www.cnblogs.com/lesliefang/p/3619223.html 。

当然大多数情况下可以用 SDWebImage, 这个库功能强大,封装的很好。但自己重头来写可能对问题理解的更深。

SDWebImage 有点复杂,很多人也会参考一下封装出一套适合自己的类库。

基本思路如下:

1 扩展(category) UIImageView, 这样写出的代码更整洁

2 GCD 异步下载 

3 重用 UITableViewCell 加异步下载会出现图片错位,所以每次 cell 渲染时都要预设一个图片 (placeholder),

以覆盖先前由于 cell 重用可能存在的图片, 同时要给 UIImageView 设置 tag 以防止错位。

4 内存 + 文件 二级缓存, 内存缓存基于 NSCache

暂时没有考虑 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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: