关于 iOS 10 UICollectionView的新特性,主要仍是体如今以下3个方面数据库
众所周知,iOS设备已良好的用户体验赢得了广大的用户群。iOS系统在用户点击屏幕会当即作出响应。并且很大一部分的操做是来自于用户的滑动操做。因此滑动的顺滑是使用户沉浸在app中享受的必要条件。接下来咱们就谈谈iOS 10 中增长了那些新特性。api
咱们先来看一下以前 UICollectionView 的体验,假设咱们每一个cell都是简单的蓝色,实际开发app中,cell会比这复杂不少。 咱们先生成100个cell。当用户滑动不是很快的时候,还感受不出来卡顿,当用户大幅度滑动,整个UICollectionView的卡顿就很明显了。若是整个cell的DataSource又是从网络加载的,那就更加卡顿了。效果以下图。数组
若是这种app上架,用户使用事后,极可能就直接给1星评价了。可是为何会形成这种问题呢?咱们来分析一下,咱们模拟一下系统如何处理重用机制的,效果以下图网络
在上图中,咱们能够看出,当cell准备加载进屏幕的时候,整个cell都已经加载完成,等待在屏幕外面了。并且更重要的是,在屏幕外面等待加载的cell是整整一行!这一行的cell都已经加载完数据。这是UICollectionView在用户大幅度滑动时卡顿的根本缘由。用专业的术语来讲,掉帧。多线程
接下来咱们就来详细的说说掉帧的问题。 app
当今的用户是很挑剔的,用户须要一个很顺滑的体验,只要有一点卡顿,极可能一言不合就卸载app了。要想用户感受不到卡顿,那么咱们的app必须帧率达到60帧/秒。用数学换算一下就是每帧16毫秒就必须刷新一次。异步
咱们用图标来分析一下掉帧的问题。下面会出现2种不一样的帧。 布局
第一种状况,下图是当用户轻微的上下小幅度滑动。这个时候每一个cell的加载压力都不大,iOS针对这种状况,已经作了很好的优化了,因此用户感受不到任何卡顿。这种状况是不会掉帧,用户也但愿能使用如此顺滑的app。性能
第二种状况,当用户大幅度滑动,每一个cell加载的压力很大,也许须要网络请求,也许须要读取数据库,并且每次都加载一行cell出来,这样每一个cell的加载时间都增长了,加载一行的总时间也就大大增长了,以下图所示。这样,不只仅当前帧在加载cell,总的时间还会挤压到下一帧的时间里面去。这种状况下,用户就感受到了卡顿了。fetch
咱们换种方式在说明一下2种状况下掉帧的状况。咱们用下图的标准来衡量一下上面2种状况。下图分为2部分,上面红色的区域,就是表示掉帧的区域,由于高于16ms。红色和绿色区域的分界线就在16ms处。y轴咱们表示的是CPU在主线程中花费的时间。x轴表示的是在用户滑动中发生的刷新事件。
针对上述掉帧的状况,绘制出实验数据,以下图。值得咱们关注的是,曲线是很曲折的,很是的不平滑。当用户大幅度滑动的时候,峰值超过了16ms,当用户慢速滑动的时候,帧率又能保持在比较顺滑的区域。处于绿色区域内的cell加载压力都是很小的。这就是时而掉帧时而顺滑的场景。这种场景下,用户体验是很糟糕的。
那怎么解决这么问题的呢?咱们来看下图:
上图中的曲线咱们看着就很平缓了,并且这种状况也不会出现掉帧的状况了,每一个滑动中的时间都能达到60帧了。这是怎样作到的呢?由于把每一个cell的加载事件都平分了,每一个cell不会再出现很忙和很闲的两个极端。这样咱们就取消了以前的波峰和波谷。从而让该曲线达到近乎水平的直线。
如何让每一个cell都分摊加载任务的压力?这就要谈到新的cell的生命周期了。
先来看看老的 UICollectionViewCell的声明周期。当用户滑动屏幕,屏幕外有一个cell准备加载显示进来。
这个时候咱们把这个cell从reuse队列里面拿出来,而后调用prepareForReuse方法。这个方法就给了cell时间,用来重置cell,重置状态,刷新cell,加载新的数据。
再滑动,咱们就会调用cellForItemAtIndexPath方法了。这个方法里面就是咱们开发者自定义的填充cell的方式了。这里会填充data model,而后赋值给cell,再把cell返回给iOS系统。
当cell立刻要进入屏幕的时候,就会调用willDisplayCell的方法。这个方法给了咱们app最后一次机会,为cell进入屏幕作最后的准备工做。执行完willDisplayCell以后,cell就进入屏幕了。
当cell彻底离开屏幕以后,就会调用didEndDisplayingCell方法。以上就是在iOS10以前的整个UICollectionViewCell的生命周期。
接下来咱们就来看看iOS 10的UICollectionViewCell生命周期是怎么样的。
这里仍是和iOS9同样的,当用户滑动UICollectionView的时候,须要一个cell,咱们就从reuse队列里面拿出一个cell,并调用prepareForReuse方法。注意调用这个方法的时间,当cell尚未进入屏幕的时候,就已经提早调用这个方法了。注意对比和iOS 9的区别,iOS 9 是在cell上边缘立刻进入屏幕的时候才调用方法,而这里,cell整个生命周期都被提早了,提早到cell还在设备外面的时候。
这里仍是和以前同样,在cellForItemAtIndexPath中建立cell,填充数据,刷新状态等等操做。注意,这里生命周期也比iOS 9提早了。
用户继续滑动,这个时候就有不一样了!
这个时候咱们并不去调用willDisplayCell方法了!这里遵循的原则是,什么时候去显示,什么时候再去调用willDisplayCell。
当cell要立刻就须要显示的时候,咱们再调用willDisplayCell方法。
当整个cell要从UICollectionView的可见区域消失的时候,这个时候会调用didEndDisplayingCell方法。接下来发生的事情和iOS9同样,cell会进入重用队列中。
若是用户想要显示某个cell,在iOS 9 当中,cell只能从重用队列里面取出,再次走一遍生命周期。并调用cellForItemAtIndexPath去建立或者生成一个cell。
在iOS 10 当中,系统会把cell保持一段时间。在iOS中,若是用户把cell滑出屏幕后,若是忽然又想回来,这个时候cell并不须要再走一段的生命周期了。只须要直接调用willDisplayCell就能够了。cell就又会从新出如今屏幕中。这就是iOS 10 的整个UICollectionView的生命周期。
上面说的iOS 10里面的场景一样适用于多列的状况。 这时咱们每次只加载一个cell,而不是每次加载一行的cell。当第一个cell准备好以后再叫第二个cell准备。当2个cell都准备好了以后,接着咱们再调用willDisplayCell给每一个cell,发送完这个消息以后,cell就会出如今屏幕上了。
这虽然看起来是一个很小的改动,可是这小小的改动就提高了不少的用户体验!
让咱们来看看上述的改动对滑动的影响
滑动比iOS 9流程不少,这里能够看到整个过程都很平缓,不卡顿。
仍是和iOS 9同样,咱们来模拟一下系统是如何加载cell的状况。
咱们能够很明显的看到,iOS 系统是一个个的加载cell的,一个cell加载完以后再去加载下一个cell。这里和iOS 9 的有很大的不一样,iOS 9是加载整整一行的cell。
这是由于咱们用了新的 UICollectionViewCell的生命周期。整个app彻底没有加一行代码。如今iOS 10是丝滑的滑动体验实在是太棒了!!
当咱们编译iOS 10的app的时候,这个Pre-Fetching默认是enable的。固然,若是有一些缘由致使你必须用到iOS 10以前老的生命周期,你只须要给collectionView加入新的isPrefetchingEnabled属性便可。若是你不想用到Pre-Fetching,那么把这个属性变成false便可。
collectionView.isPrefetchingEnabled = false复制代码
为了最佳实践一下这个新特性。咱们先改变一下咱们加载cell的方式。咱们把很重的读取数据的操做,全部内容的建立都放到cellForItemAtIndexPath方法里面去完成。保证咱们在willDisplayCell 和 didEndDisplayCell这两个方法里面基本不作其余事情。最后,须要注意的是cellForItemAtIndexPath生成的某些cell,可能永远都不会被展现在屏幕上,有这样一种状况,当cell将要展现在屏幕上的时候,用户忽然滑动离开了这个界面。
若是这个时候当你用iOS 10编译出你的app,那么很是顺滑的用户体验就会自动的优化出来。
UICollectionView的流畅的滑动解决了,那么在UICollectionViewCell在加载的时候所花费的时间,怎么解决呢??
UICollectionViewCell加载的时间取决于DataModel。DataModel极可能回去加载图片,来自于网络或者来自于本地的数据库。这些操做大多数都是异步的操做。为了使data加载更快,iOS 10引入了新的API来解决这个问题。
UICollectionView有2个“小伙伴”,那就是data source和delegate。在iOS 10中,将会迎来第3个“小伙伴”。这个“小伙伴”叫prefetchDataSource。
protocol UICollectionViewDataSourcePrefetching {
func collectionView(_ collectionView: UICollectionView,
prefetchItemsAt indexPaths: [NSIndexPath])
optional func collectionView(_ collectionView: UICollectionView,
cancelPrefetchingForItemsAt indexPaths: [NSIndexPath])
}
class UICollectionView : UIScrollView {
weak var prefetchDataSource: UICollectionViewDataSourcePrefetching?
var isPrefetchingEnabled: Bool
}复制代码
这个协议里面只有一个必需要实现的方法——ColletionView prefetchItemsAt indexPaths。这个方法会在prefetchDataSource里面被调用,用来给你异步的预加载数据的。indexPaths数组是有序的,就是接下来item接收数据的顺序,让咱们model异步处理数据更加方便。
在这个协议里面还有第二个方法CollectionView cancelPrefetcingForItemsAt indexPaths,不过这个方法是optional的。咱们能够利用这个方法来处理在滑动中取消或者下降提早加载数据的优先级。
值得说明的是,新增长的这个“小伙伴”prefetchDataSource并不能代替原来的读取数据的方法,这个预加载仅仅只是辅助加载数据,并不能 删除原来咱们读取数据的方法。
至此,咱们来看看从文章开始到如今,UICollectionView的性能提高了多少。咱们仍是用掉帧的方法来看看UICollectionView的性能。
上图是iOS 9 UICollectionView的性能,很明显的看见,波峰波谷很明显,而且还掉了8帧,有明显的卡顿现象。
上图是iOS 10 UICollectionView的性能,咱们能够很明显的看到,通过iOS 10的优化,整个曲线很明显平缓了一些,没有极端的波峰掉帧现象。可是依旧存在少许的波峰快到16ms分界线了。
上图是iOS 10 + Pre-Fetching API 以后的性能,已经优化的效果很明显了!整条曲线基本都水平了。近乎完美。可是仍是能发现有个别波峰特别高。波峰特别高的地方就是那个cell加载压力大,时间花的比较长致使的。接下来咱们继续优化!
先来总结一下使用Pre-Fetching API须要注意的地方。
在咱们使用Pre-Fetching API的时候,咱们必定要保证整个预加载的过程都放在后台线程中进行。合理使用GCD 和 NSOperationQueue处理好多线程。
请切记,Pre-Fetching API是一种自适应的技术。何为自适应技术呢?当咱们滑动速度很慢的时候,在这种“安静”的时期,Pre-Fetching API会默默的在后台帮咱们预加载数据,可是一旦当咱们快速滑动,咱们须要频繁的刷新,咱们不会去执行Pre-Fetching API。
最后,用cancelPrefetchingAPI去迎合用户的滑动动做的变换,好比说用户在快速滑动忽然发现了有趣的感兴趣的事情,这个时候停下来滑动了,甚至快速反向滑动了,或者点击了事件,进去看详情了,这些时刻咱们都应该开启cancelPrefetchingAPI。
综上所述,Pre-Fetching API对于提升UICollectionView的性能提高是颇有帮助的,并且并不须要加入太多的代码。加入少许的代码就能够得到巨大的性能提高!
在iOS 10中,UITableViewCell也跟着UICollectionView一块儿获得了性能的提高,同样拥有了Pre-Fetching API。
protocol UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [NSIndexPath])
optional func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths:
[NSIndexPath])
}
class UITableView : UIScrollView {
weak var prefetchDataSource: UITableViewDataSourcePrefetching?
}复制代码
这里和上面 UICollectionView同样,会调用TableView prefetchRowsAt indexPaths方法。indexPaths仍是一个有序数字,顺序就是列表上可见的顺序。第二个可选的API仍是TableView cancelPrefetchingForRowsAt indexPaths,和以前提到的同样,也是用来取消预加载的。性能的提高和UICollectionView同样的,对UITableView的性能提高很大!
self-sizing API 第一次被引入是在iOS 8,然而如今在iOS 10中获得了一些改进。
在UICollectionView 中有一个固定的类,叫UICollectionViewFlowLayout,iOS已经在这个类中彻底支持了self-sizing。为了能开启这一特性,须要咱们开发者为一些不能为0的CGSize的cell设置一下estimated item size。
layout.estimatedItemSize = CGSize(width:50,height:50)复制代码
这会告诉UICollectionView咱们想要开启动态计算内容的布局。
至今,咱们能有3种方法来动态的布局。
第一种方法是使用autolayout 当咱们合理的加上了constrain,当cell加载的时候,就会根据内容动态的加载布局。
第二种方法,若是你不想使用autolayout的方法,想更加手动的控制它,那么咱们就须要重写sizeThatFits()方法。
第三种方法,终极的方法是重写preferredLayoutAttributesFittingAttributes()方法。在这个方法里面不只仅能够提供size的信息,更能够获得alpha和transform的信息。
因此想指定cell的大小,就能够用上面3个方法之一。
可是实际操做中,咱们能够发现,有时候设置一个合适的estimated item size,对于咱们来讲是很困难的事情。若是flow layout能够用数学的方法动态的计算布局,而不是根据咱们给的size去布局,那会是件很酷的事情。
iOS 10中就引入了新的API来解决上述的问题。
layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize复制代码
对于开发者,咱们须要作的事情,仅仅就是设置好flow layout ,而后给estimatedItemSize设定一个新的常数, 最后UICollectionViewFlowLayout 就会自动计算高度了。
系统会自动计算好全部的布局,包括已经定下来的size的cell,而且还会动态的给出接下来cell的大小的预测。
接下来看2个例子就能够很明显看出iOS 10针对self-sizing的改进了。
上图能够看到,iOS 9 的布局是针对单个cell计算的,当改变了单个的cell,其余的cell依旧没有变化,仍是须要从新计算。
这里例子就能够很明显的看出差异了。当咱们改变了第一个cell的size之后,系统会自动计算出全部的cell的size,而且每一行,每个section的size都会被动态的计算出来,而且刷新界面!
以上就是iOS 10针对self-sizing的改进。
谈到从新排列,这是咱们就须要类比一下UITableView了,UICollectionView的从新排列就如同UITableView 把cell上下移动,只不过UITableView的重排是针对垂直方向的。
在iOS 9中,引入了UICollectionView的Interactive Reordering,在今年的iOS 10中,又加入了一些新的API。
在上图中,咱们能够看到,咱们即便任意拖动cell,整个界面也会从新排列,而且咱们改变了cell的大小,整个 UICollectionView 也会从新动态的布局。
咱们先来看看iOS 9里面的API
class UICollectionView : UIScrollView {
func beginInteractiveMovementForItem(at indexPath: NSIndexPath) -> Bool
func updateInteractiveMovementTargetPosition(_ targetPosition: CGPoint)
func endInteractiveMovement()
func cancelInteractiveMovement()
}复制代码
要想开启interactive movement,咱们就须要调用beginInteractiveMovementForItem()方法,其中indexPath表明了咱们将要移动走的cell。接着每次手势的刷新,咱们都须要刷新cell的位置,去响应咱们手指的移动操做。这时咱们就须要调用updateInteractiveMovementTargetPosition()方法。咱们经过手势来传递坐标的变化。当咱们移动结束以后,就会调用endInteractiveMovement()方法。 UICollectionView 就会放下cell,处理完整个layout,此时你也能够从新刷新model或者处理数据model。若是中间忽然手势取消了,那么这个时候就应该调用cancelInteractiveMovement()方法。若是咱们从新把cell移动一圈以后又放回原位,其实就是取消了移动,那这个时候就应该在cancelInteractiveMovement()方法里面不用去刷新data source。
在iOS 10中,若是你使用UICollectionViewController,那么这个重排对于你来讲会更加的简单。
class UICollectionViewController : UIViewController {
var installsStandardGestureForInteractiveMovement: Bool
}复制代码
你只须要把installsStandardGestureForInteractiveMovement这个属性设置为True便可。CollectionViewController会自动为你加入手势,而且自动为你调用上面的方法。
以上就是去年iOS 9为咱们增长的API。
今年的iOS 10新加入的API是在iOS 9的基础上增长了翻页的功能。
UICollectionView继承自UIScrollView,因此只须要你作的是把isPagingEnabled属性设置为True,便可开启分页的功能。
collectionView.isPagingEnabled = true复制代码
开启分页以前:
开启分页以后就长这样子:
每次移动一次就会以页为单位的翻页。
UIRefreshControl如今能够直接在CollectionView里面使用,一样的,也能够直接在UITableView里面使用,而且能够脱离UITableViewController。由于如今RefreshControl成为了ScrollView的一个属性了。
UIRefreshControl的使用方法很简单,就三步:
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(refreshControlDidFire(_:)),
for: .valueChanged)
collectionView.refreshControl = refreshControl复制代码
先建立一个refreshControl,再关联一个action事件,最后把这个新的refreshControl赋给想要的控件的对应的属性便可。
经过以上,咱们谈到了如下的知识:
最后,谈谈我看了iOS 10 UICollectionView的优化的见解吧,原来有些地方用到AsyncDisplayKit优化UICollectionView速度的,如今能够考虑不用第三方库优化了,系统自带的方法能够解决通常性的卡顿的问题了。我感受iOS 10的UICollectionView才像是一个完整版的,以前的系统优化的都不够。我仍是很看好iOS 10的UICollectionView。
请你们多多指教。新浪微博@halfrost