UIImageView在线图片加载以及缓存优化

UIImageView是iOS开发中经常用到的控件,通过UIImageView来加载显示网络图片更是很常遇到的场景之一。如何加载显示网络图片,提高用户体验度,我们可以通过多种途径来实现:苹果原生api,SDWebImage,其他第三方库等,这里我结合NSCache以及磁盘缓存实现了一个自己封装的UIImageView库。


直接说重点,一个好的图片缓存库大概应该包含以下几点:

  1. 异步判断是否有缓存,有就解压缩图片,返回主线程刷新UI(不卡主线程,提高用户体验)。
  2. 无缓存,异步下载图像,尽可能减少使用主线程队列。
  3. 下载成功,后台解压缩图像,返回主线程刷新UI;同时在内存和磁盘上缓存图像,并且保存解压过的图片到内存中,以避免再次解压。
  4. 可使用GCD 和 blocks,使得代码更加高效和简单。
  5. 如果可以,最好在下载后以及存入到缓存前对图像进行处理(裁剪、压缩、拉伸等)

  • 缓存类的实现
    包括内存缓存和磁盘缓存,内存缓存通过NSCache实现。NSCache 是苹果官方提供的缓存类,用法与 NSMutableDictionary 的用法很相似, 在收到系统内存警告时,NSCache会自动释放一些对象,它是线程安全的,在多线程操作中,不需要对NSCache加锁,另外NSCache的Key只是做强引用,不需要实现NSCopying协议。
    // 初始化内存缓存 NSCache
    _memCache = [[NSCache alloc] init];
    //设置10兆的内存缓存作为二级缓存,先读内存,内存没有再读文件
    _memCache.totalCostLimit = 1024102410;
    totalCostLimit表示缓存空间的最大总成本,超出上限会自动回收对象(默认值是 0,表示没有限制)。
    从内存读取图片缓存:
    - (UIImage *)getImageFromMemoryCache:(NSString *)uri
    {
    UIImage *result = nil;
    result = [_memCache objectForKey:uri];
    return result;
    }
    存入内存缓存:
    - (void)saveMemoryCache:(NSData )data uri:(NSString )uri decoded:(BOOL)decoded
    {
    if (data==nil ||uri==nil){
    return;
    }
    //大于一兆的数据就不要放进内存缓存了,不然内存紧张会崩溃)
    if (data.length >1024
    1024
    1){
    return;
    }

         //对于jpg,png图片,将data转为UIImage再存到内存缓存,不用每次获时再执行imageWithData这个非常耗时的操作。
         if (data!=nil){
              @try {
                        UIImage *image=[UIImage imageWithData:data];
                        UIImage *resultImage = nil;
                        //png图片不执行decoded
                        if (decoded && ![CJImageViewCache isPNGImage:data]) {
                            resultImage = [CJImageViewCache decodedImageWithImage:image];
                        }
                        if (nil == resultImage) {
                            resultImage = image;
                        }
                        NSInteger dataSize= resultImage.size.width * resultImage.size.height * resultImage.scale;
                        [_memCache setObject:resultImage forKey:uri cost:dataSize];
              }
              @catch (NSException *exception) {
                        NSLog(@"exception=%@",exception);
              }
          }
    }
    

这里说一下,在图片存入内存前预先进行decodedImageWithImage:可以提高图片的显示效率,但是经测试发现如果图片是.png格式时,是无法decoded的,所以这里做多了一个判读,当图片是png格式时则忽略decoded。关于如何判断图片,我们可以通过根据文件头来判断图片是否为png格式。

  • 磁盘缓存
    从磁盘读取缓存
    - (UIImage *)getImageFromDiskCache:(NSString *)uri decoded:(BOOL)decoded
    {
    UIImage *image = nil;
    NSData *data = [NSData dataWithContentsOfFile:[self cachePath:uri]];
    if (data) {
    image = [UIImage imageWithData:data];
    //png图片不执行decoded
    if (decoded && ![CJImageViewCache isPNGImage:data]){
    image = [CJImageViewCache decodedImageWithImage:image];
    }
    if (image) {
    CGFloat cost = image.size.height * image.size.width * image.scale;
    [self.memCache setObject:image forKey:uri cost:cost];
    }
    }
    return image;
    }
    缓存路径的判断:
    - (NSString *)cachePath:(NSString *)relaPath
    {
    // nil 安全判断
    if (relaPath != nil && relaPath.length > 0) {
    const char *str = [relaPath UTF8String];
    if (str == NULL) {
    str = "";
    }
    unsigned char r[16];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
    r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], r[11], r[12], r[13], r[14], r[15]];
    return [_diskCachePath stringByAppendingPathComponent:filename];
    }
    return nil;
    }
    其中_diskCachePath为图片缓存路径(fullNameSpace是自定义字符串)
    //图片缓存路径
    _diskCachePath = [paths[0] stringByAppendingPathComponent:fullNamespace];
    将数据写入磁盘缓存
    - (void)saveDiskCache:(NSData *)imageData uri:(NSString *)uri
    {
    UIImage *image = [UIImage imageWithData:imageData];
    NSData *data = imageData;
    if ([CJImageViewCache isPNGImage:imageData]) {
    data = UIImagePNGRepresentation(image);
    }
    else {
    data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
    }

        if (data) {
            @synchronized(_fileManager) {
                if (![_fileManager fileExistsAtPath:_diskCachePath]) {
                    [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
                }
                if ([_fileManager fileExistsAtPath:[self cachePath:uri]]) {
                    [_fileManager removeItemAtPath:[self cachePath:uri] error:nil];
                }
                [_fileManager createFileAtPath:[self cachePath:uri] contents:data attributes:nil];
    //            NSLog(@"_diskCachePath = %@",_diskCachePath);
            }
        }
    }
    

_fileManager在初始化时实现
_fileManager = [NSFileManager defaultManager];
另外缓存类还实现了清除缓存,清除指定缓存,获取缓存大小等方法,这里就不再细说了,大家可以下载demo 看看。


  • CJImageView图片加载类
    图片加载过程:判断是否有缓存;有缓存直接显示缓存图片;无缓存先显示默认图片,开始请求网络,加载期间可选是否显示加载菊花,加载成功停止loading动画,显示网络图片,并保存缓存。
    /**
    * 加载图片,设置默认图,显示加载菊花,设置加载菊花样式,图片是否decoded
    *
    * @param uri
    * @param image
    * @param showIndicator
    * @param style 默认UIActivityIndicatorViewStyleGray
    * @param decoded 是否decoded
    */
    - (void)setUri:(NSString *)uri defaultImage:(UIImage *)image showIndicator:(BOOL)showIndicator style:(UIActivityIndicatorViewStyle)style decoded:(BOOL)decoded
    {
    __weak __typeof(self) wSelf = self;
    dispatch_async([self getImageOperatorQueue], ^(){
    UIImage * resultImage = [[CJImageViewCache sharedImageCache]getImageFromCache:uri decoded:decoded];
    if (resultImage != nil) {
    dispatch_async_main_queue(^{
    wSelf.image = resultImage;
    });
    }else{
    //获取缓存失败,请求网络
    [wSelf loadImageData:uri defaultImage:image showIndicator:showIndicator style:style decoded:decoded];
    }
    });
    }
    请求网络使用了我之前发表的CJHttpClient库,这里由于CJImageView实现了自己的网络缓存,所以网络请求时使用忽略缓存协议:CJRequestIgnoringLocalCacheData
    - (void)loadImageData:(NSString *)uri defaultImage:(UIImage *)image showIndicator:(BOOL)showIndicator style:(UIActivityIndicatorViewStyle)style decoded:(BOOL)decoded
    {
    self.style = style;
    self.url = uri;
    __weak typeof(self) wSelf = self;
    //先显示默认图片
    dispatch_async_main_queue(^{
    wSelf.image = image;
    });

        if (showIndicator) {
            if (!_loadingInducator) {
                UIActivityIndicatorView *tempIndicator =  [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:self.style];
                self.loadingInducator = tempIndicator;
          
                CGFloat minFloat = MIN(self.frame.size.width, self.frame.size.height);
                CGFloat inducatorMaxFloat = MAX(tempIndicator.frame.size.width, tempIndicator.frame.size.height);
                if (minFloat/inducatorMaxFloat < 2) {
                    self.loadingInducator.transform = CGAffineTransformScale(self.loadingInducator.transform, 0.6, 0.6);
                }
            }
            self.loadingInducator.activityIndicatorViewStyle = self.style;
            self.loadingInducator.center = CGPointMake(self.frame.size.width/2, self.frame.size.height/2);
            dispatch_async_main_queue(^{
                [wSelf addSubview:wSelf.loadingInducator];
                [wSelf bringSubviewToFront:wSelf.loadingInducator];
                [wSelf.loadingInducator startAnimating];
            });
         }
    
      [CJHttpClient getUrl:uri parameters:nil timeoutInterval:HTTP_DEFAULT_TIMEOUT cachPolicy:CJRequestIgnoringLocalCacheData completionHandler:^(NSData *data, NSURLResponse *response){
          __strong __typeof(wSelf)strongSelf = wSelf;
          //保存缓存
          [[CJImageViewCache sharedImageCache] saveCache:data uri:uri decoded:decoded];
          dispatch_async([self getImageOperatorQueue], ^(){
              if ([[response.URL absoluteString] isEqualToString:self.url]) {
                  UIImage * dataImage = [UIImage imageWithData:data];
                  UIImage * resultImage = nil;
                  if (decoded && ![CJImageViewCache isPNGImage:data]) {
                      resultImage = [CJImageViewCache decodedImageWithImage:dataImage];
                  }
                  dispatch_async_main_queue(^{
                      strongSelf.image = resultImage != nil?resultImage:(dataImage != nil?dataImage:image);
                      [strongSelf.loadingInducator stopAnimating];
                      [strongSelf sendSubviewToBack:strongSelf.loadingInducator];
                  });
               }
          });
      }errorHandler:^(NSError *error){
          __strong __typeof(wSelf)strongSelf = wSelf;
          dispatch_async_main_queue(^{
              strongSelf.image = image;
              [strongSelf.loadingInducator stopAnimating];
              [strongSelf sendSubviewToBack:strongSelf.loadingInducator];
          });
      }];
    }
    

最后是demo地址。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容