zoukankan      html  css  js  c++  java
  • ios UITableView 异步加载图片并防止错位

    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

  • 相关阅读:
    vue + element-ui实现动态多级表头
    Linux 系统编程 学习:11-线程:线程同步
    Linux 系统编程 学习:10-线程:线程的属性
    Linux 系统编程 学习:9-线程:线程的创建、回收与取消
    Linux 网络编程的5种IO模型:信号驱动IO模型
    Linux 系统编程 学习:8-基于socket的网络编程3:基于 TCP 的通信
    Linux 系统编程 学习:6-基于socket的网络编程1:有关概念
    Linux 系统编程 学习:7-基于socket的网络编程2:基于 UDP 的通信
    Linux 系统编程 学习:5-进程间通信2:System V IPC
    Linux 系统编程 学习:2-进程间通信1:Unix IPC(1)管道
  • 原文地址:https://www.cnblogs.com/lesliefang/p/3906708.html
Copyright © 2011-2022 走看看