AsyncDisplayKit介紹(一)原理和思路

UITableView/UICollectionView的優化一直是iOS應用性能優化重要的一塊。即便是iOS10+iPhone7這樣的最新軟硬件配置,在系統的信息app中滾動,仔細觀察的話仍然能感到必定的掉幀現象。對於UI要求苛刻的蘋果居然在如此簡單的tableView上沒法達到60fps的幀率,可見優化滾動性能的背後並不簡單。javascript

爲何?

理想狀態下,iOS的幀率應該保持在60fps。然而不少狀況下用戶操做時會感受到掉幀或者『不跟手』。緣由可能有不少,這裏只簡單列舉幾個,網上能夠找到許多相應分析:html

  1. CPU(主要是主線程)/GPU負擔太重或者不均衡(諸如mask/cornerRadius/drawRect/opaque帶來offscreen
    rendering/blending等等)。因爲全部的UIView都是由CALayer來負責顯示,所以對Core
    Animation的瞭解就變得尤其重要。這裏推薦Nick Lockwood的Core Animation: Advanced
    Techniques
    一書,其中有對Core Animation的性能有着很是詳盡的梳理和剖析。
  2. Autolayout佈局性能瓶頸,約束計算時間會隨着數量呈指數級增加,而且必須在主線程執行。具體分析能夠參考這篇文章:floriankugler.com/2013/04/22/…。這也是爲什麼ASDK拋棄了Autolayout而設計了本身的佈局系統的重要緣由之一(github.com/facebook/As…)。Autolayout在單個View開發時能帶來不少便利,而在一些須要高性能的場景下須要謹慎使用。
  3. 儘管從iPhone4S(A5)開始CPU已經採用多核,然而對於大多數app來講,多線程協做並無被充分利用。換句話說,在app卡頓(主線程所佔用的核心滿負荷)時,每每CPU的其餘核心幾乎無事可作。通常狀況下,因爲主線程承擔了絕大部分的工做,若是能把主線程的任務轉移一部給其餘線程進行異步處理,就能夠立刻享受到併發帶來的性能提高。這應該也是AsyncDisplayKit得名的緣由之一。

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

  1. 針對Autolayout性能優化:提早計算並緩存cell的layout:
    github.com/forkingdog/…
  2. 省去中間滑動過程當中的計算,直接計算目標區域cell:
    github.com/johnil/VVeb…
  3. 棄用Autolayout,採用手動佈局計算。這樣雖然能夠換來最高的性能,可是代價是編寫和維護的不便,對於常常改動或者性能要求不高的場景並不必定值得。
  4. 自行異步渲染Layer,如:github.com/ibireme/YYA…
  5. iOS10列表的prefetch API,只是並無解決Autolayout的性能,同時也受到系統版本限制。

ASDK的基本思路:異步

對於通常的開發者,本身從新實現一整套異步佈局和渲染機制是很是困難的。幸運的是,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最主要集中於這三點進行優化):

  1. 渲染,對於大量圖片,或者大量文字(尤爲是CJK字符)混合在一塊兒時。而文字區域的大小和佈局,偏偏依賴着渲染的結果。ASDK儘量後臺線程進行渲染,完成後再同步回主線程相應的UIView。
  2. 佈局。ASDK徹底棄用了Autolayout,另闢蹊徑實現了本身的佈局和緩存機制。關於佈局的問題會在下一篇講到。
  3. 系統objects的建立與銷燬。因爲UIKit封裝了CALayer以支持觸摸等顯示之外的操做,耗時也相應增長。而這些一樣也須要在主線程上操做。ASDK基於Node的設計,突破了UIKit線程的限制。

既然同步就意味着阻塞,那就異步放到其餘線程去作,在須要主線程時再同步回來。

咱們知道對於通常UIView和CALayer來講,由於不是線程安全的,任何相關操做都須要在主線程進行。正如UIView能夠彌補CALayer沒法處理用戶事件的不足同樣,ASDK引入了Node的概念來解決UIView/CALayer只能在主線程上操做的限制(不禁讓人想起『Abstract layer can solve many problems, except problem of having too many abstract layers.』)。

主要特色以下:

  1. 每一個Node對應相應的UIView或者CALayer,從開發者的角度而言,只須要將初始化UIView的代碼稍做修改,替換爲建立ASDisplayNode便可。在不須要接受用戶操做的Node上能夠開啓isLayerBacked,直接使用CALayer進一步下降開銷。根據Scott的研究UIView的開銷大約是CALayer的5倍。
  2. Node默認是異步佈局/渲染,只有在須要將frame/contents等同步到UIView上纔會回到主線程,使其空出更多的時間處理其餘事件。
  3. ASDK只有在認爲須要的時候纔會異步地爲Node加載相應的View,所以建立Node的開銷變得很是低。同時Node是線程安全的,能夠在任意queue上建立和設置屬性。
  4. ASDK不只有與UIView對應的大部分控件(如ASButtonNode、ASTextNode、ASImageNode、ASTableNode等等),同時也bridge了大多數UIView的方法和屬性,能夠很是方便的操做frame/backgroundColor/addSubnode等,所以通常狀況下只要對Node進行操做,ASDK就會在適當的時候同步到其View。若是須要的話,當相應的View加載以後(或訪問node.view手動觸發加載),也能夠經過node.view的方式直接訪問,回到咱們熟悉的UIKit。
  5. 當實現自定義View的時候,ASDisplayNode提供了一個初始化方法initWithViewBlock/initWithLayerBlock,就能夠將任意UIView/CALayer用Node包裹起來(被包裹的view可使用autolayout),從而與ASDK的其餘組件相結合。雖然這樣建立的Node與通常view在佈局和渲染上的差別不大,可是因爲Node管理着什麼時候何地加載view,咱們仍然能獲得必定的性能提高。

舉例來講,當使用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多個不一樣場景下的示例項目。

一些細節

  1. 在ASDisplayNode.h中有至關多的註釋,其中displaysAsynchronously屬性大體描述了異步渲染的步驟:
  • 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_transaction

  • Once 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的回調中將以前異步完成的工做同步到主線程中去。

  1. ASDisplayNode還有一個屬性shouldRasterizeDescendants。

/**

  • @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 backing

  • store. Defaults to NO.

  • If a node’s descendants are static (never animated or never change attributes
    after creation) then that node is a

  • good 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 rasterized

  • container. 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 subtree

  • which can help improve animation/scrolling/etc performance.

  • Rasterization does not currently support descendants with transform,
    sublayerTransform, or alpha. Those properties

  • will be ignored when rasterizing descendants.

  • Note: this has nothing to do with -[CALayer shouldRasterize], which doesn’t
    work with ASDisplayNode’s asynchronous

  • rendering model.

*/

當咱們不須要分別關注單個CALayer,也不須要對他們進行操做時,就能夠將全部的子node都合併到父node的backing
store一併繪製,從而達到節省內存和提升性能的目的。

注意事項

  1. ASDK不支持Storyboard和Autolayout,可是能夠與使用Autolayout的view兼容共存。一樣React native和Component
    Kit等其餘Facebook出品的iOS庫也不支持Storyboard。
  2. 因爲Node的異步渲染,頗有可能在其View到達屏幕以後,內容仍然在渲染過程當中。此時須要額外考慮每一個Node的placeholder狀態,使用戶不至於看到一片空白。
  3. 在使用ASDisplayNode初始化initWithViewBlock時,因爲Node須要在適當的時候調用該block來建立view,所以並不會當即調用block(block可能capture其餘變量,例如self),而是存在一個ivar當中。若是該view始終沒被建立,而此時擁有該node的父元素被銷燬,容易形成retain
    cycle致使memory leak。

Best Practice

因爲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

相關文章
相關標籤/搜索