1. 程式人生 > >轉 UITableView 重用 UITableViewCell 並非同步載入圖片時會出現圖片錯亂的情況

轉 UITableView 重用 UITableViewCell 並非同步載入圖片時會出現圖片錯亂的情況

UITableView 重用 UITableViewCell 並非同步載入圖片時會出現圖片錯亂的情況

當然大多數情況下可以用 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;
}

複製程式碼