TableView 图片加载逻辑优化

table view 是 scroll view 的子类,我们先来研究一下 scroll view。

当 scroll view 开始滚动时,scroll view 的 delegate 会不断的收到很多回调消息,这些消息告诉我们 scroll view 处于什么状态。通过官方的文档,我们知道用户在和 scroll view 交互时有两种手势,一种是 flick(轻扫),一种是 drag(拖动)。

但是在实际的操作过程中,用户很可能会带来更复杂的交互,经过实践归纳总结,我将它们大概分成四种情况,如下图:

ScrollView.png
  1. 橙色线,用户用手指轻扫一下屏幕,然后释放手指。
  2. 橙色线->绿色线->蓝色线,用户拖动 scroll view,并在 scroll view 处于静止时释放手指。
  3. 橙色线->红色线->…->橙色线,用户在 scroll view 停止滚动前,不断的轻扫屏幕,最后释放手指。
  4. 橙色线->红色线->…->橙色线->绿色线->紫色线,用户在轻扫屏幕后,手指突然定住 scroll view,最后释放手指。

scroll view 还有三个属性:tracking、dragging 和 decelerating。这三个属性在 scroll view 的滚动中不断变化,但是它们的功能却和他们的名字不太一样。

首先当用户触摸屏幕时,tracking 和 dragging 的值都为 true,decelerating 的值为 false。

一旦用户的手指脱离屏幕,如果此时有动量,scroll view 开始减速,tracking 的值会变为 false,而 dragging 的值要等到 scroll view 停止滚动后才变为 false。所以 tracking 更像是记录拖动的属性,dragging 更像是记录滚动的属性。

至于 decelerating,当然是在减速过程中为 true,值得注意的是,如果 scrollViewDidEndDecelerating 一直未被调用,对应上面的情况三,此时 decelerating 一直为 true。

知道这些基础的东西之后,我们来尝试优化 table view 加载图片的逻辑。

Glow 的这篇博文中提到了当年 Tweetie 的方案以及自己的优化方案,大概的思路就是,当用户在快速滑动 table view 时,cell 不加载图片,对应上面的情况三。那么如何定义这个快速滑动的情况呢?

快速滑动就是用户不断的 flick 屏幕,也就是在上一次 scroll view 还未停止的时候开始下一次短暂拖动,这就导致 scroll view 一直处于滚动状态。

我们先来看看快速滑动过程中,scroll view 三个状态量的值,通过上图我们知道,tracking 只在每次用户触摸的时候才为 true,而 dragging 全程都为 true,decelerating 除了第一次拖动的过程中为 false,其余时间只要 scroll view 未停止滚动,都为 true。

如果我们想单纯的让 table view 在快速滚动的过程中不加载图片的话,我们会写出这样的代码:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        
        let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
        
        if tableView.decelerating {
            cell.imageView?.image = placeholder
        } else {
            setImageFor(cell, forRowAtIndexPath: indexPath)
        }
        
        return cell
}

Glow 在这里还用了一个 userDragging 的变量来表明用户是否在拖拽 scroll view,方法是在 willBeginDragging 中设为 YES,didEndDragging 中设为 NO。我们通过上面的分析知道,tracking 属性貌似也有同样的作用。经过测试,唯一的区别是,tracking 在手指触摸后立即变为 true,等到开始减速时才变为 false,不过这个时间相差的简直太短,基本可以忽略不计了。

继续谈到这个变量的作用,Glow 在这里的判断是这样写的:

if !tableView.tracking && tableView.decelerating {
    cell.imageView?.image = placeholde
} else {
    setImageFor(cell, forRowAtIndexPath: indexPath)
}

判断的意思是,如果用户没有拖拽并且 table view 处于减速中的话就不加载图片,反之加载图片。

但是这样做有一个问题,用户在快速滑动的过程中会不时的拖动屏幕一小段距离,这段距离过程中出现的 cell 会加载图片,这样看来是比较奇怪的,不太符合在快速滑动的过程中不在加载图片的设想。

只做 decelerating 的判断可以让 table view 在快速滚动的过程中做到完全不加载图片。

但是这样做还是会产生两个问题,

1,当 table view 在停止快速滑动正常减速结束后,当前屏幕中的 cell 不会被加载。

这个问题比较好解决,Glow 提到的解决方法是在 scrollViewDidEndDecelerating 里面加载当前可见 cell 的图片,代码如下:

func loadVisibleCellsImage() {
    for cell in tableView.visibleCells {
        let indexPath = tableView.indexPathForCell(cell)!
        setImageFor(cell, forRowAtIndexPath: indexPath)
    }
}

这样确实解决了问题,但是却带来了新的问题,必须要等到 scroll view 完全减速完成后才会加载图片,但是 scroll view 减速的那最后一段距离实在让人难等。Glow 通过 scrollViewWillEndDragging: withVelocity: targetContentOffset: 方法的 targetContentOffset 计算出 targetRect 来加载最后可见的那些 cell。

具体来说,就是用一个变量 targetRect 在 scrollViewWillEndDragging: withVelocity: targetContentOffset: 方法里面计算出值,在 scrollViewWillBeginDraggingscrollViewDidEndDecelerating 重置为 nil。这样一来,targetRect 还起到了之前 userDragging 的作用,可谓一举两得。

这里根据这个思路我也想到一个办法,通过构造一个 scrollViewWillEndDecelerating:scrollView:withTargetContentOffset 方法来让 table view 在最后的减速过程中提前加载图片,代码如下:

var targetContentOffset: CGPoint?
var space: CGFloat = 20

override func scrollViewDidScroll(scrollView: UIScrollView) {
        
        if let targetContentOffset = self.targetContentOffset where abs(scrollView.contentOffset.y - targetContentOffset.y) < space {
            scrollViewWillEndDecelerating(scrollView, withTargetContentOffset: targetContentOffset)
            self.targetContentOffset = nil
        }
        
    }

override func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        if velocity != CGPoint.zero {
            self.targetContentOffset = targetContentOffset.memory
        }
    }
    
    func scrollViewWillEndDecelerating(scrollView: UIScrollView, withTargetContentOffset targetContentOffset: CGPoint) {
        // 快要结束减速
    }

通过调整 space 的值,你可以调整方法的调用时机,来获得更好的体验。

问题 1 到这里算是解决了。

问题 2 是这样的,当 table view 在快速滑动中被用户手指点击强制结束滑动后,当前屏幕的 cell 不会被加载。

这个问题一开始看起来貌似很简单,因为点击滑动中的 scroll view,scrollViewWillBeginDragging 会被调用一次,直接在这里面调用之前的 loadVisibleCellsImage,不就好了吗?但问题是只有当你抬起手指的时候才会调用,如果用户一直不抬起手指,这个方法就一直不会被调用。况且,这个函数在你快速滑动 scroll view 的时候也会被调用,那么之前所做的工作完全就无用了。

我把问题集中在如何判断 table view 被点击上来,这里可以通过重写 table view 的 hitTest 来实现,尽管这样,还是会遇到快速滑动中被不断调用的问题。

后来我又想是否有方法判断 scroll view 停止滚动,速度为 0 的方法,找了很久,无一成功。

问题二到现在没办法解决。

当然,我这是极致强调 scroll view 只要在减速过程中就不允许加载图片的时候才会导致问题二没法解决,如果按照 Glow 的方法,虽然在滚动的过程中不会加载图片,但是每次拖动都会加载可见的图片。

到这里,Glow 的处理方法感觉还是值得一用,而且配合 SDWebImage 的图片缓存,感觉上还是会有一些提升。

后记

提到 SDWebImage,我突然想到在下载图片的时候,它首先会取消 imageView 还未完成的下载,然后再从缓存或从网络中加载图片。

我们知道 table view cell 的重用机制,cell 移出屏幕会加入到重用池,等到需要时,重用池的 cell 会被取出重用。我想这会不会已经解决了快速滑动过程中图片的加载问题呢?

思考一下,在快速滑动中,cell 不断的被重用,也就是 imageView 可能还没加载完图片当前的加载就被取消了,那不正好解决了问题吗?

如果这样可行,不再去管 scroll view 滚动过程中的各种回调,还可以值得优化的地方是,刚刚说的 imageView 的图片加载被取消,注意这里的取消是不管有没有图片的缓存都会被取消,所以在快速滚动的过程中之前有缓存的 cell 还是没有图片,所以最好的是先从缓存中尝试取图片,如果实在没有再加载图片。

结语

上面的所有文字仅仅基于我自己的思考和实践,但是没有在具体的项目工程中运用过,并且这里只是讨论了基于 scroll view 的优化,没涉及绘制方面的内容。如果你有更好的方法,欢迎评论分享。

更新计划

Runloop 貌似还可以在做很多事情,慢慢研究。

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

推荐阅读更多精彩内容

  • 当 scroll view 开始滚动时,scroll view 的 delegate 会不断的收到很多回调消息,这...
    Crazy2015阅读 597评论 0 0
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,098评论 4 62
  • UIScrollView (包括它的子类 UITableView和UICollectionView)是iOS开发中...
    博BlingBing阅读 2,187评论 0 7
  • 文/李立文 我是文学社的新社员,参加研讨会时初识孟老师。有一天在家学习写作时,遇到个问题,想请教孟老师,便从通讯录...
    启明星子阅读 295评论 0 0
  • 暖暖的阳光沉浸在 校园的鸟语花香里 更是陶醉于 那一张张写满了无奋斗不青春的脸上 明媚一点点蔓延 那些诗意的句子与...
    海梦夜眼阅读 306评论 0 1