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