UITableView/UICollectionView的优化一直是iOS应用性能优化重要的一块。即便是iOS10+iPhone7这样的最新软硬件配置,在系统的信息app中滚动,仔细观察的话仍然能感到必定的掉帧现象。对于UI要求苛刻的苹果居然在如此简单的tableView上没法达到60fps的帧率,可见优化滚动性能的背后并不简单。javascript
理想状态下,iOS的帧率应该保持在60fps。然而不少状况下用户操做时会感受到掉帧或者『不跟手』。缘由可能有不少,这里只简单列举几个,网上能够找到许多相应分析:html
UIKit的单线程设计也有必定的历史缘由。早在十年前iOS SDK刚问世的时候,mobile
SDK仍是一个很是新的概念,更没有移动多核CPU的存在,所以当时的重点是简单可靠,大多数API都没有支持相对复杂的异步操做。时至今日,若是要彻底重构UIKit使之支持异步绘制和布局,对于兼容已有海量的app,难度可想而知。在iOS10中虽然对UICollectionView/UITableView作了必定的预加载优化(WWDC2016
Session219),然而并无从根本上解决主线程布局和渲染的问题。java
咱们知道,当用户开始滚动或点击一个View,全部的事件都会被送到主线程等待处理。此时主线程可否抽出足够充裕的时间来处理变得极为重要,尤为是在连续操做(如UIGestureRecognizer)时,每次touchMoved事件处理都会占用主线程必定的时间(如新的UIImageView进入视图,主线程开始处理布局或者图片解码,而这些须要连续占用大量CPU时间)。若是一个操做耗时超过16ms(1000ms/60fps),那就意味着下一帧没法及时获得处理,引发丢帧。node
图片截取自wwdc2016 session219ios
如何能将主线程的压力尽量减轻成为优化的首要目标。git
对列表滚动卡顿的经常使用解决方案有(推荐Yaoyuan的博客,有比较深刻的介绍:
blog.ibireme.com/2015/11/12/…):github
对于通常的开发者,本身从新实现一整套异步布局和渲染机制是很是困难的。幸运的是,ASDK作到了。缓存
AsyncDisplayKit(ASDK)是2012年由Facebook开始着手开发,并于2014年出品的高性能显示类库,主要做者是Scott
Goodson。Scott(github:
appleguy)曾经参与了多个iOS版本系统的开发,包括UIKit以及一些系统原生app,后来加入Facebook并参与了ASDK的开发并应用到Paper,所以该库有机会从相对底层的角度来进行一系列的优化。安全
如今最新的版本是2.0,除了拥有1.0系列版本核心的异步布局渲染功能,还增长了相似ComponentKit的基于flexbox的布局功能。源文件一共近300个,3万多行代码,是一个很是庞大而精密的显示和布局系统。使用上若是不考虑工程成本,彻底能够在必定程度上代替UIKit的大部分功能。同时因为和Instagram同处于FB家族,所以也迅速在最近的更新中加入了IGListKit的支持。性能优化
在Scott介绍ASDK的视频中,总结了一下三点占用大量CPU时间的『元凶』(虽然仍然可能有以上提到的其余缘由,但ASDK最主要集中于这三点进行优化):
既然同步就意味着阻塞,那就异步放到其余线程去作,在须要主线程时再同步回来。
咱们知道对于通常UIView和CALayer来讲,由于不是线程安全的,任何相关操做都须要在主线程进行。正如UIView能够弥补CALayer没法处理用户事件的不足同样,ASDK引入了Node的概念来解决UIView/CALayer只能在主线程上操做的限制(不禁让人想起『Abstract layer can solve many problems, except problem of having too many abstract layers.』)。
主要特色以下:
举例来讲,当使用UIKit建立一个UIImageView:
_imageView = [[UIImageView alloc] init];
_imageView.image = [UIImage imageNamed:@"hello"];
_imageView.frame = CGRectMake(10.0f, 10.0f, 40.0f, 40.0f);
[self.view addSubview:_imageView];复制代码
使用ASDK后只要稍加改动:
_imageNode = [[ASImageNode alloc] init];
_imageNode.image = [UIImage imageNamed:@"hello"];
_imageNode.frame = CGRectMake(10.0f, 10.0f, 40.0f, 40.0f);
[self.view addSubview:_imageNode.view];复制代码
虽然只是简单的把View替换成了Node,然而和UIImageView不一样的是,此时ASDK已经在悄悄使用另外一个线程进行图片解码,从而大大下降新的用户操做到来时主线程被阻塞的几率,使每个回调都能获得及时的处理。实践中将会有更加复杂的状况,有兴趣的话能够参考项目中的Example目录,有20多个不一样场景下的示例项目。
Asynchronous rendering proceeds as follows:
When the view is initially added to the hierarchy, it has -needsDisplay true.
After layout, Core Animation will call -display on the _ASDisplayLayer
-display enqueues a rendering operation on the displayQueue
When the render block executes, it calls the delegate display method
(-drawRect:… or -display)The delegate provides contents via this method and an operation is added to
the asyncdisplaykit_async_transactionOnce all rendering is complete for the current
asyncdisplaykit_async_transaction,the completion for the block sets the contents on all of the layers in the
same frame
从中咱们能够看到,全部异步渲染操做是先被同一加入asyncdisplaykit_async_transaction,而后一块儿提交的。在_ASAsyncTransactionGroup.m源文件中,能够看到ASDK是在主线程的runloop(关于runloop能够参考Yaoyuan的文章和sunny的视频)中注册了observer,在kCFRunLoopBeforeWaiting和kCFRunLoopExit两个activity的回调中将以前异步完成的工做同步到主线程中去。
/**
@abstract Whether to draw all descendant nodes’ layers/views into this node’s
layer/view’s backing store.@discussion
When set to YES, causes all descendant nodes’ layers/views to be drawn
directly into this node’s layer/view’s backingstore. Defaults to NO.
If a node’s descendants are static (never animated or never change attributes
after creation) then that node is agood candidate for rasterization. Rasterizing descendants has two main
benefits:1) Backing stores for descendant layers are not created. Instead the layers
are drawn directly into the rasterizedcontainer. This can save a great deal of memory.
2) Since the entire subtree is drawn into one backing store, compositing and
blending are eliminated in that subtreewhich can help improve animation/scrolling/etc performance.
Rasterization does not currently support descendants with transform,
sublayerTransform, or alpha. Those propertieswill be ignored when rasterizing descendants.
Note: this has nothing to do with -[CALayer shouldRasterize], which doesn’t
work with ASDisplayNode’s asynchronousrendering model.
*/
当咱们不须要分别关注单个CALayer,也不须要对他们进行操做时,就能够将全部的子node都合并到父node的backing
store一并绘制,从而达到节省内存和提升性能的目的。
因为ASDK的基本理念是在须要建立UIView时替换成对应的Node来获取性能提高,所以对于现有代码改动较大,侵入性较高,同时因为大量本来熟悉的操做变成了异步的,对于一个团队来讲学习曲线也较为陡峭。
从咱们在实际项目中的经验,结合Scott的建议来看,不须要也不可能将全部UIView都替换成其Node版本。将注意力集中在可能形成主线程阻塞的地方,如tableView/collectionView、复杂布局的View、使用连续手势的操做等等。找到合适的切入点将一部分性能需求较高的代码替换成ASDK,会是一个较好的选择。
对于优化之后的效果,能够尝试咱们的应用:即刻,其中消息盒子部分应用了ASDK进行了大量调优工做,从而在拥有大量图片和gif/不定长度文字共存的状况下,仍然能达到60fps的流畅体验。在咱们将近两年的实践中,尽管也碰到过一些坑,可是带来的提高也是很是明显的,使在构建更复杂的界面同时保持高性能成为可能。
AsyncDisplayKit Getting Started
AsyncDisplayKit Tutorial: Node
Hierarchies
NSLondon — Scott Goodson — Behind
AsyncDisplayKit
MCE 2015 — Scott Goodson — Effortless Responsiveness with
AsyncDisplayKit
AsyncDisplayKit 2.0: Intelligent User Interfaces — NSSpain
2015
PS: 即刻正在招聘,若是你是一个追求极致又富有探索精神的iOS工程师,同时也热爱咱们的产品,那么欢迎加入咱们,一块儿参加WWDC(公司cover所有费用),持续打造更棒的即刻!联系咱们:hr@ruguoapp.com