iOS UITabelView性能优化之远程图片懒加载(简单讲解Apple官方Demo)

我们都知道iOS开发时UITabelView是使用频率非常高的一个控件,围绕它的性能优化也有很多方法,这里先介绍其中一个优化的点,苹果官方也对这种优化方法给出了一个示例工程,大家也可以先下载下来看看效果:下载地址

官方实例工程运行效果

先简单总结一下这个方法思路就是:在tableView还在滚动未停止的状态下,不发起网络请求去加载图片。只有在tableView滚动停止了才会去发起网络请求加载当前可见的cell里面的图片,当然如果这个图片是已经加载过的那也就不用再发起网络请求了。

其实大家之前在tableView的cell加载url image的时候肯定都有使用SDWebImage或者其他‘远程图片子线程加载’库或方法,这些库或方法都会在子线程去下载指定url的图片资源,下载完成再回主线程刷新界面。这已经能够将相对耗时的图片网络请求都放到子线程去做,不要影响主线程刷新界面。

而现在要介绍的图片懒加载的方式就是在此基础上更加优化一步,你想一想如果我的tableview里有很多行数据,在过高速滚动的时候系统会‘疯狂’调用UITableViewDataSource那些方法,我们就疯狂地在子线程发起一大堆图片网络请求,虽然说是在子线程里,但是一下加入大量的请求道子线程对性能多少还是会有点影响,另外用户在滚动的时候也说明用户对当前显示行的内容不感兴趣,所以也没有必要去发起请求浪费用户流量。只有当用户停止滚动了才去请求当前显示行的图片资源。

工程讲解

接下来我们就用上面提到的苹果官方示例工程来简单解释一下这个方法的实现过程。打开上面刚下载的LazyTableImages工程,直接进到RootViewController这个类去看就可以了。

RootViewController就是这个示例程序的主控制器,在RootViewController.h头文件中定义了一个叫entries数组

@property (nonatomic, strong) NSArray *entries;

这个entries数组就放着tableview每一行row的模型数据,它是在AppDelegate的程序启动时的方法里就发起的请求,请求回来之后就将数据转模型然后传给这个Controller。

每一行的模型数据是AppRecord类示例,AppRecord类属性如下:

@interface AppRecord : NSObject
@property (nonatomic, strong) NSString *appName;
@property (nonatomic, strong) UIImage *appIcon;
@property (nonatomic, strong) NSString *artist;
@property (nonatomic, strong) NSString *imageURLString;
@property (nonatomic, strong) NSString *appURLString;
@end

刚请求回来的时候只有这行数据图片的url地址叫做imageURLString,当这个图片被请求回来了就将图片对象设置给appIcon属性里,下次再显示这个行时候看到有图片对象就不用请求了。

好的,现在就将目标放到最重要的几个tableView的方法上去,先看到最主要cellForRowAtIndexPath方法,这个方法大家都知道在tableView滚动时候系统会疯狂的调用,让你返回要显示的cell对象给他。请看下面方法和注解,只要重点看那个if else的else里面的代码和注释就可以了,这个if else只是用来判断模型数据是否请求回来了,没有则显示一个正在loading的cell

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
  UITableViewCell *cell = nil;
  
  //计算当前数据模型的数组entries的数量
  NSUInteger nodeCount = self.entries.count;
    
  if (nodeCount == 0 && indexPath.row == 0) {
      cell = [tableView dequeueReusableCellWithIdentifier:PlaceholderCellIdentifier forIndexPath:indexPath];
  } else {
      //重点是这个else后面的!!!!!!!!!!!!!!!
      cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

      if (nodeCount > 0)
      {
          //取出在这个位置cell对象的模型数据
          AppRecord *appRecord = (self.entries)[indexPath.row];
          
          //将数据文字传给cell的Label显示
          cell.textLabel.text = appRecord.appName;
          cell.detailTextLabel.text = appRecord.artist;
          
          //判断当前模型数据是否已经有图片对象了(即判断是否这个数据的图片已经请求过了)
          if (!appRecord.appIcon)
          {
              //判断当前tableView是否在滚动中(这个方法里最重要的一句判断)
              if (self.tableView.dragging == NO && 
                  self.tableView.decelerating == NO) 
             {
                  //如果tableView又未再滚动中,即在停止住状态下则调用开始下载图片的方
                  [self startIconDownload:appRecord forIndexPath:indexPath];
              }
              //不管tableView是否在滚动中,是否要去下载图片,都先将本地的默认占位图显示上去
              cell.imageView.image = [UIImage imageNamed:@"Placeholder.png"];                
          }
          else
          {
            //如果这个数据的图片已经请求过了,那么直接显示图片即可
             cell.imageView.image = appRecord.appIcon;
          }
      }
  }
    
    return cell;
}

好的,看完了这个方法我们就去看看在上面这个方法里调用的那个开始下载图片的方法是怎么实现的,请看下面方法和注解:

- (void)startIconDownload:(AppRecord *)appRecord forIndexPath:(NSIndexPath *)indexPath
{
  //控制器有一个imageDownloadsInProgress的字典属性,用来保存对应indexPath位置的IconDownloader(图片下载器)对象
  //先判断是否已经有当前indexPath的图片下载器对象,如果有则说明这个位置之前已经开始了下载动作,不用重复开始了
  //IconDownloader是这个工程自定义的下载图片的类,你可以去看看它的实现,也可以不用管
  IconDownloader *iconDownloader = (self.imageDownloadsInProgress)[indexPath];

  if (iconDownloader == nil) 
  {
    //如果没有则创建一个IconDownloader
    iconDownloader = [[IconDownloader alloc] init];

    //将当前位置的数据模型传给IconDownloader
    //IconDownloader内部一会就会根据这个模型对象的图片url地址去下载
    iconDownloader.appRecord = appRecord;

    //设置IconDownloader下载完成后的回调block
    [iconDownloader setCompletionHandler:^{
      
       //先取到对应indexPath位置的cell
        UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
        
        //让cell显示模型数据新下载到的图片对象
        //appRecord的appIcon图片对象的赋值在IconDownloader内部就自动完成了
        cell.imageView.image = appRecord.appIcon;
      
        //将这个完成下载的IconDownloader从控制器的imageDownloadsInProgress字典里移除掉
        [self.imageDownloadsInProgress removeObjectForKey:indexPath];
      
    }];
    //将这个准备开始下载图片的IconDownloader加入到控制器的imageDownloadsInProgress字典里
    (self.imageDownloadsInProgress)[indexPath] = iconDownloader;
    //开始下载图片
    [iconDownloader startDownload];  
  }
}

如果你觉得这样子就完工了那就错了!!!

因为根据现在这两个方法的实现还只能让tableView在滚动的时候不发起图片加载请求,还不能让tableView停止时候去加载当前显示行的图片,现在我们要想想在哪里可以知道tableView停止了呢?

肯定是UIScrollViewDelegate方法啦!UITableView是继承UIScrollView的嘛。请看下面两个代理方法实现和注释:

//用户停止拖动了scrollView(手指结束拖拽动作离开屏幕了),准备开始减速滚动时会调用
//由于惯性,用户手指离开屏幕后还会继续滚动一会,这个decelerate(减速)就是指这个后续的滚动状态
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    //判断是否已经停止减速了
    if (!decelerate)
    {
        //调用加载当前显示行图片方法
        [self loadImagesForOnscreenRows];
    }
}

//scrollView停止减速后会调用
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
     //调用加载当前显示行图片方法
    [self loadImagesForOnscreenRows];
}

可以看到这个两个代理方法很简单,我们就是为了定位到tableView停止滚动的那一刻,然后就简单调用了加载当前显示行图片的方法。

接下来再看最后一个方法和注释,就是加载当前显示行图片:

- (void)loadImagesForOnscreenRows
{
    //判断模型数据是否为空,为空说明模型数据都还没请求回来,也就不毕继续加载图片动作
    if (self.entries.count > 0)
    {
        //获取当前屏幕上可以见所有行对应的indexPath位置组成的数组
        NSArray *visiblePaths = [self.tableView indexPathsForVisibleRows];
        //遍历所有位置
        for (NSIndexPath *indexPath in visiblePaths)
        {
            //取到对应位置的模型数据
            AppRecord *appRecord = (self.entries)[indexPath.row];
          
            //判断这个模型数据是否已经有图片对象了,如果有说明已经下载过了
            if (!appRecord.appIcon)
            {
                //如果还未下载过则去开始下载对应行的图片
                [self startIconDownload:appRecord forIndexPath:indexPath];
            }
        }
    }
}

至此这个示例工程最主要的几个方法就讲解完了,大家应该对这个官方建议的优化方法理解了吧。当然tableView还有很多其他值得优化的地方和方法,不过这个其实也要具体情况具体分析,不用为了优化而优化。

这个内容来加载的方法也不一定要局限于UITableView或图片,我觉得很多用了UIScrollView复杂界面都可以借鉴使用这个方法

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,980评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,081评论 4 62
  • 总会有很多人对我们说过这样一句话,你还是个孩子。是啊,可能在父母、亲人、或者年长我们的朋友眼里,我们还是个为长大成...
    sw沐晨阅读 343评论 0 0
  • 虽早已入冬,乍寒还暖。天空罩着一层厚厚的迷雾,密不透气。不冷,也不暖心。厚重的大气层,压得人有点窒息。阴云霭霭,少...
    六六毛阅读 263评论 0 0
  • 大结局了,我只想说挺不错的,好久没有看国产剧了。叫好的人很多,批评的也不少。 比如那些批判说小时代电视剧版,三观不...
    不叫郑小某阅读 487评论 0 49