ios UITableView 异步加载图片并防止错位

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

ios UITableView 异步加载图片并防止错位

时间: 2024-10-12 16:29:27

ios UITableView 异步加载图片并防止错位的相关文章

IOS中UITableView异步加载图片的实现

本文转载至 http://blog.csdn.net/enuola/article/details/8639404 最近做一个项目,需要用到UITableView异步加载图片的例子,看到网上有一个EGOImageView的很好的例子. 但是由于,EGOImageView的实现比较复杂,于是自己就动手做了一个AsynImageView,同样可以实现EGOImageView的效果. 而且自己写的代码比较清晰,容易理解,同样可以实现指定placehoderImage以及指定imageURL,来进行图片

android listview 异步加载图片并防止错位

网上找了一张图, listview 异步加载图片之所以错位的根本原因是重用了 convertView 且有异步操作. 如果不重用 convertView 不会出现错位现象, 重用 convertView 但没有异步操作也不会有问题. 我简单分析一下: 当重用 convertView 时,最初一屏显示 7 条记录, getView 被调用 7 次,创建了 7 个 convertView. 当 Item1 划出屏幕, Item8 进入屏幕时,这时没有为 Item8 创建新的 view 实例, Ite

android listview 异步加载图片并防止错位+双缓存

网上找了一张图, listview 异步加载图片之所以错位的根本原因是重用了 convertView 且有异步操作. 如果不重用 convertView 不会出现错位现象, 重用 convertView 但没有异步操作也不会有问题. 我简单分析一下: 当重用 convertView 时,最初一屏显示 7 条记录, getView 被调用 7 次,创建了 7 个 convertView. 当 Item1 划出屏幕, Item8 进入屏幕时,这时没有为 Item8 创建新的 view 实例, Ite

android listview 异步加载图片并防止错位 解决办法

网上找了一张图, listview 异步加载图片之所以错位的根本原因是重用了 convertView 且有异步操作. 如果不重用 convertView 不会出现错位现象, 重用 convertView 但没有异步操作也不会有问题. 我简单分析一下: 当重用 convertView 时,最初一屏显示 7 条记录, getView 被调用 7 次,创建了 7 个 convertView. 当 Item1 划出屏幕, Item8 进入屏幕时,这时没有为 Item8 创建新的 view 实例, Ite

iOS NSOperation 异步加载图片 封装NSOperation 代理更新

#import <Foundation/Foundation.h> @class MYOperation; @protocol MYOperationDelecate <NSObject> -(void)operationWithStr:(UIImage*)str; @end @interface MYOperation : NSOperation @property(nonatomic,copy)NSString *imageURL; @property(nonatomic,we

Android ListView异步加载图片乱序问题,原因分析及解决方案

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/45586553 在Android所有系统自带的控件当中,ListView这个控件算是用法比较复杂的了,关键是用法复杂也就算了,它还经常会出现一些稀奇古怪的问题,让人非常头疼.比如说在ListView中加载图片,如果是同步加载图片倒还好,但是一旦使用异步加载图片那么问题就来了,这个问题我相信很多Android开发者都曾经遇到过,就是异步加载图片会出现错位乱序的情况.遇到这个问题时,不

【iOS】网络加载图片缓存与SDWebImage

加载网络图片可以说是网络应用中必备的.如果单纯的去下载图片,而不去做多线程.缓存等技术去优化,加载图片时的效果与用户体验就会很差. 一.自己实现加载图片的方法 tips: *iOS中所有网络访问都是异步的.(自己开线程去下载) *普通为模型增加UIImage属性的方法做的是内存缓存(下次启动还需要从网络重新加载), 而要做本地缓存的话,还要自己手动存储网络上下载的图片. *为了加快访问, 还需要自己去弄缓存.(内存缓存或者本地缓存) *当图片没有下载完成时,还要设置占位图片. 以下代码用NSOp

Android:ViewPager扩展详解——带有导航的ViewPagerIndicator(附带图片缓存,异步加载图片)

大家都用过viewpager了, github上有对viewpager进行扩展,导航风格更加丰富,这个开源项目是ViewPagerIndicator,很好用,但是例子比较简单,实际用起来要进行很多扩展,比如在fragment里进行图片缓存和图片异步加载. 下面是ViewPagerIndicator源码运行后的效果,大家也都看过了,我多此一举截几张图: 下载源码请点击这里 ===========================================华丽的分割线==============

软引用SoftReference异步加载图片

HashMap<String, SoftReference<Drawable>> imageCache 关于SoftReference这个类多少知道些机制,会用就ok了. 机制:简单来说,她会帮助我们管理内存,防止内存溢出,另外一点也就相当于map,临时缓存些图片drawable让我们可以直接引用,很好了解决了OOM异常. 实现代码片段: [java]package com.Tianyou.Mobile.Common;  import java.io.IOException; im