原文地址在這裏。算法
本文主要說了Flutter內部使用了怎樣的算法和優化讓Flutter如此強大。某些內容對比了Flutter和其餘開發工具一致性算法的優劣,不過我的感受仍是太過簡短,後面我會花更多的時間來研究這方面的內容,後續補上。最後還講述了Flutter在API設計上是如何達到開發者的預期的。因爲譯者水平有限,疏漏之處還請見諒。
本文描述了Flutter的內部工做原理。Flutter的widget是用激進組合的方式工做的,因此用戶在構建UI的時候會用到不少的widget。爲了支持這個工做量,Flutter使用了亞線性算法來處理佈局、構建組件以及樹數據結構。還包括了其餘的一些常量及的優化。綜合考慮其餘的一些細節,這樣的設計也會讓開發者更加的容易的使用回調來建立無限滾動列表中對用戶可見的部分。數組
Flutter的特色之一就是激進組合(aggressive composability)。通常組件(widget)都是由其餘的組件組成的,這樣的組件都是由更加基本的組件組成的。好比Padding
就是一個組件,而不是一個組件的某個屬性。總之,用戶的UI是有不少,不少的組件組成的。安全
組件的最末尾的節點都是RenderObjectWidget
,這些組件都會被用來建立繪製到屏幕上的節點。一顆繪製樹就是一個保存了用戶界面幾何信息(大小,位置等)的數據結構。這些幾何信息是在layout階段計算出來的,並在繪製(painting)和碰撞檢測(hit test)被用到。基本上Flutter的開發人員不會直接去建立繪製對象(render object),而是經過組件(widget)來操做繪製樹。數據結構
爲了在組件層支持激進組合,Flutter對組件層和繪製樹層使用了不少的算法和優化。這些都會在隨後的章節中介紹。app
如何應對大量的組件和繪製對象(render object),如何獲取好的性能?關鍵就是有效的算法!這其中最關鍵的就是layout算法,這個算法決定了繪製對象的幾何信息,好比大小和位置。某些工具的佈局算法的時間複雜度到了O(N²)甚至更差(好比,在默寫約束下作固定點的迭代)。Flutter的目標是首次佈局計算達到線性性能,在隨後的更新已存在佈局的時候達到亞線性性能,除了某些特殊狀況以外。尤爲是,相對於大量增長的繪製對象,佈局消耗的時間只會緩慢增長。框架
Flutter每一幀只執行一次佈局,每次佈局的計算都是以單通道(single pass)的方式工做。約束會經過父對象調用每一個子對象的layout方法傳遞下去。子對象會遞歸地執行本身的layout方法並在返回的時候把幾何信息向上返回。一旦一個繪製對象從它的layout方法返回了,那麼這個繪製對象不會再被訪問,一直到下一幀(frame)的佈局計算。這樣的方式把原本會分紅兩步:測量(measure)和佈局的計算集合成了一個單通道的方式。這樣,每一個render object再聚聚的時候最多被訪問兩次:一次是沿着樹向下的訪問,一次是沿着樹向上的訪問。ide
Flutter包含了一個協議(protocol)的多個具象(specialization)。最多見的具象就是RenderBox
,做用於一個二維笛卡爾座標系。在盒佈局中,它的約束是一個最大、最小寬度和一個最大、最小高度。在佈局的時候,child
就是經過在邊界中選擇一個值做爲它的幾何信息。當child從佈局中返回,父對象決定了child在父對象座標系的位置。注意:子對象的佈局和它的位置無關,由於直到子對象從layout返回才能直到它的位置。所以,父對象能夠能夠任意給子對象定位,而不須要再次計算佈局。函數
總的來講,在佈局期間,惟一從父對象流向子對象的就是約束(constraints)。惟一從子對象流向父對象的數據就是幾何信息。這些不變量能夠大幅度減小布局計算所需的工做量。工具
這麼多優化的結果是:當繪製對象樹(render object tree)裏包含了髒(dirt)節點的時候,只有這些髒節點和他們周圍有限的子樹須要在layout的被訪問到。佈局
和layout算法相似,Flutter的組件構建算法也是亞線性的。在構建以後,全部組件都被element樹持有,這個樹也保持了UI的邏輯結構。Element樹很是必要,由於組件自己是不可修改的(immutable)。也就是說若是有多個wiget的話,他們是不會記得他們之中的父子節點關係的。Element樹也持有StatefuleWidget
的state對象。
在用戶數據或者其餘操做以後,一個element能夠變爲髒(dirty),好比一個開發者對state對象調用了setState()
。那麼Flutter會保留一個「髒」element的列表,並在構建階段直接跳過去而忽略掉其餘乾淨(clean)的element。在構建階段,數據單向的由上向下從element樹流動,也就是說在構建階段,element只會被訪問一次。一旦element變成乾淨的(clean)就不會變成髒的,由於它的祖先element都是乾淨的了。
因爲組件是不可修改(immutable)的,若是父對象使用同一個組件從新構建,並且組件對應的element沒有把本身標記爲髒,那麼這個element能夠在構建階段當即返回。並且,element只須要比較兩個widget引用的ID來肯定兩個widget是否爲同一個。這個優化叫作二次投影模式,具體來講就是一個組件包含了一個構建前的子組件,在構建的時候把它保存爲了一個成員變量。
在構建的時候,Flutter也會避免使用InheritedWidget
來訪問父鏈。若是全部組件都訪問父鏈,好比獲取當前的主題顏色,那麼根據樹的深度,構建將變成O(N²)。這樣的耗時就回很是之多。爲了不這樣的狀況發生,Flutter在每一個element上都有一個InheritedWidget
的哈希表。不少的element只會引用同一個哈希表,只有element引用了新的InheritedWidget
纔會發生改變。
與廣泛認爲的不一樣,Flutter不會進樹級別的找不一樣。並且使用了一個O(N)算法:獨立檢測每一個element的子element列表來決定是否要重用這個element。子列表一致性算法的優化分爲如下幾種狀況:
一般的方法就是從頭至尾的對比兩個列表裏的每一個組件的運行時類型(runtime type)和key,極有可能會在每一個列表裏發現一段包含了全部不匹配的組件,以及他們的範圍(range) 。Flutter會把舊的列表裏的組件根據他們的key,放進一個哈希表裏。接下來,Flutter遍歷新的列表的範圍(range),並從哈希表中查找匹配的key。不匹配的會被拋棄,匹配的則使用新的組件從新構建。
重用element對於性能來講很是之重要,由於element持有兩種很重要的數據:狀態組件的狀態和底層的繪製對象。當Flutter能夠重用一個element的時候,UI某個邏輯部分的狀態得以保留,而且以前計算出來的layout數據也能夠重用,基本能夠避免整個子樹的遍歷。事實上,Flutter支持保留了狀態和佈局的非本地(non-local)樹修改。
開發者能夠經過使一個widget和一個GlobalKey
關聯的方式來執行非本地樹修改。每個全局鍵(global key)在整個app裏都是惟一的,而且註冊在了一個線程相關的哈希表裏。在構建階段,開發者能夠把一個有全局鍵的組件移動到element樹的任意位置。而不是在那個位置上再建一個全新的組件。Flutter會檢查哈希表,而後把組件掛在新的父組件下,並保留整個子樹。
在子樹裏的繪製對象能夠保留他們的佈局信息,由於佈局的約束是惟一從樹的父對象流向子對象的數據。新的父對象會被標記爲髒(dirty)由於它的子對象列表已經發生了改變。可是若是新的父對象傳過來的layout數據和舊的parent傳過來的是同樣的,那麼這個子對象會馬上返回,中止遍歷。
全局鍵和非本地樹修改普遍用於英雄轉化(hero transition)和導航(navigation)。
在這些算法優化以外,要達到及機組和還須要幾個常量因素優化。這些優化對於上面提到的算法也相當重要。
RenderBox
類有一個抽象的visitChildren()
方法而不是實際的firstChild和nextSibling接口。許多子類都支持一個單一的child,直接作爲一個類成員變量,而不是一列子節點。好比,RenderPadding
支持一個惟一的child。這樣只會有一個耗時更短的,更簡單的layout方法會被執行。RenderParagraph
。這是一個繪製樹的葉子節點。文本的處理不須要繼承的方式,而是用組合的方式。如此一來就能夠避免RenderParagraph
從新計算它所持有的文本在父節點傳遞了一樣的約束的條件下再次計算佈局數據。着很常見,即便是在樹分解中也同樣。鑑於一般都是碩大的組件數來講,這樣的優化帶來的性能提高很是顯著。
繪製對象樹(繪製樹)和Element樹是同構的(嚴格的說,繪製樹是element樹的一個子集)。一個明顯的簡化是把這兩個數組合成一個。然而,在實踐中把這兩個數分開有不少的益處。
無限滾動列表的實現對於各類工具來講都是很是之難。Flutter在構建的時候提供了一個很是簡單的接口來實現這個功能。一個列表,當用戶滾動的時候,使用了一個回調來實現可視的時候顯示一個組件。支持這個功能須要用到viewport-aware佈局和按需構建組件。
和Flutter多數的東西同樣,可滾動組件也是用組合的方式組成的。在可滾動組件的外面是一個Viewport
,的子組件它能夠擴展到可視窗口外面的部分,還能夠滾動到視圖內。然而,一個viewport有一個RenderSilver
類型的子組件,而不是RenderBox
類型的子組件。RenderSilver
類型有一個視圖感知接口。
這個silver佈局協議和盒式佈局的協議結構上是一致的,也會給子節點傳遞約束並返回幾何信息。然而,約束和幾何信息在二者之間卻不一樣。在silver協議裏,子節點收到的是viewport數據,包括剩餘的可視空間。他們返回的幾何數據讓不少種和滾動相關的效果成爲可能,包括可摺疊的header和視差效果。
不一樣的silver填充viewport剩餘空間的方式是不一樣的。好比,一個silver能夠生成一列子組件,一個挨着一個排列,直到這個silver顯示了所有的子組件或者用光了全部的空間。相似地,一個silver生成一個二維的grid,子組件只填滿這個grid可視的部分。由於他們能夠感知到剩餘的空間還有多少。silver還能夠生成有限的子組件,雖然他們也能夠生成無限的子組件。
silver能夠經過組合生成不一樣的可滾動佈局和效果。好比,一個單獨的viewport能夠有一個可摺疊的heaer,下面跟着一個線性列表和一個grid。全部的三個silver都會根據silver協議來互動,從而生成在viewport裏可視的子組件,無論他們是屬於header,list仍是grid的。
若是Flutter有一個嚴格的先構建再佈局再繪製的管道(pipeline),前述內容在構建無限滾動列表的時候就很是的低效了,由於viewport裏剩餘多少空間可使用的數據只有在layout的階段才能夠知道。不採用其餘手法的前提下,佈局階段對於構建填充剩餘空間的組件來講太遲了。Flutter把管道中的構建和佈局兩個階段互相交叉,解決了這個問題。在佈局階段的任什麼時候刻,Flutter均可以按需構建新的組件,只要這些組件是當前執行佈局的繪製對象的子組件。
交叉構建和佈局能夠實現,徹底是由於構建和佈局算法中嚴格的數據傳遞控制。尤爲是,在構建階段,數據只能夠向下傳遞。當一個繪製對象在計算佈局的時候,佈局遍歷尚未訪問到這個繪製對戲的子樹,也就是說子樹生成的寫入還不能改寫當前佈局的計算結果。相似的,一旦佈局從一個繪製對象返回了,在此次佈局計算中這個繪製對象講不會再被訪問到,也就是說任何後一步佈局計算生成的寫入都不能影響當前繪製對象用於構建子樹的數據。
另外,線性一致性和樹分解對於滾動中高效的更新element都很是的有必要。當element滾動進或出可視區域的時候,對修改繪製樹來講也一樣的重要。
只有在框架能夠被正確使用的時候,快纔有意義。爲了達到API友好的效果,Flutter和開發者進行了普遍的體驗研究。這些有時確定了以前的某些決定,有時會幫助肯定某些功能的優先級,有時又改變了API設計的方向。好比,Flutter的API都有豐富的文檔。UX研究確定了這些文檔的價值,可是也明確了示例代碼和圖標的做用。
這一節討論Flutter的API的設計以備急用。
Flutter的Widget
, Element
和RenderObject
樹節點的基類沒有子模型(child model)。這也讓某個節點能夠成爲它要適用的某個節點的子模型。
多數的組件對象都只有一個子組件,所以只暴露了一個child
參數。某些組件支持不少的子組件,因此暴露了一個叫作children
的列表參數。某些組件沒有任何的子組件,因此也不會暴露任何的參數。相似的,RenderObjects
暴露特定的子模型API。RenderImage
是一個葉子節點,沒有子對象的概念。RenderPadding
接受一個單一的child,因此它只有一個單獨的引用指向一個child。RenderFlex
接受未知數量的child並使用一個鏈表管理他們。
在某些特殊的狀況下,須要更復雜的子模型。RenderTable
繪製對象接收的是一個二維數組,這個類對應的getter和setter來控制行和列的數量,還有必定數量的方法能夠替換某個x,y下標的child。
Chip
組件和InputDecoration
對象的屬性也和相關控件相符合。一刀切的子模型會強制語義至於子模型分層的頂端,好比,定義第一個child爲前綴值,第二個child爲後綴值,那麼這個特定的子模型(child model)能夠被用於特定的命名屬性。
這種靈活性讓這些樹的每一個節點按照它的角色來操做。不多會把一個cell插入到一個table裏,由於全部其餘的cell都會變形,移位。相似地,也不多會使用下標而不是引用從一個flex行刪除某個元素。
RenderParagraph
對象是組極端的例子:它有一個徹底不一樣的child,TextSpan
。在RenderParagraph
範圍內,RenderObject
樹會變成一個TextSpan
樹。
整體上,讓API的設計符合開發者預期,不僅是子模型上的努力。
某些簡單的組件的存在就是爲了讓開發者在解決的某個問題的時候能夠找他他們。給一個行或列添加空間,一旦你知道方法就回變得很是簡單:使用Expanded
組件和一個零大小的SiezedBox
子組件,不過其實這還不是最好的方法。若是你搜索space的話你會找到Spacer
組件,這個組件內部就使用了Expanded
和SizedBox
。
相似的還有隱藏一個組件的子樹也很容易,只要不要在構建的時候包含這個組件的子樹。開發者但願有一個組件能夠達到這個效果,那麼就有了Visibility
組件。
UI框架都會有不少的參數,通常來講開發者不多會記得構造函數裏的每一個參數的語義。Flutter使用響應模式,因此在構建的時候會用到不少的構造函數。有了Dart語言的命名參數,Flutter的API就能夠保證每一個build方法都清晰,容易理解。
這個模式能夠擴展到每一個用了不少參數的方法上。尤爲是每一個bool類型的參數,因此方法裏的每一個true
和false
都是自帶文檔屬性的。
一個在Flutter裏廣泛使用的技術是定義一種錯誤條件不存在的API。這樣避免了對於錯誤的過多關注。
好比,一個插值方法容許一端或者插值的兩端都爲null,Flutter沒有把它定義爲錯誤。而是:插值的兩端都爲null則返回null,若是有一端爲null,那麼就至關因而給某個特定類型的0值插入值。也就是說,開發者若是意外給插值方法傳入了null值,那麼不會發生錯誤,而是會輸出一個合理的值。
在Flex
佈局算法裏有一個更加細微的例子。這個佈局的概念是flex的繪製對象的空間會有它的一個或者多個子組件分割,因此flex佈局的大小應該是佔滿可用空間。在最初的設計中,提供一個無線的空間會出錯:這樣隱式的代表flex佈局是無限大小,一個沒有用的佈局。然而,API作了修改,這樣當一個無限大小的空間賦值給flex繪製對象的時候,它會變成這些子組件須要的大小,減小了可能的錯誤狀況。
不是全部的錯誤均可以經過設計避免的。對於那些在debug的時候依舊存在的問題,Flutter一般都會今早的捕獲異常,而且及時報告。斷言(assert)的使用很是廣泛。構造函數的參數也都檢查到了細節。生命週期也都有監控,一旦發生錯誤就會拋出異常。
在某些狀況下,這些發揮到了極致:好比,當運行單元測試的時候,無論測試的是什麼,每一個RenderBox
子類都會檢查固有size方法是否知足固有size的契約。這能夠幫助發現那些API中不容易暴露的問題。
拋出的異常也包含了竟可能豐富的信息。
基於可變樹的API都要經歷一種混亂:建立樹的最初狀態的操做集合和後續的更新的操做結合存在很大的不一樣。Flutter的繪製層使用了這個模型,這是一種維護一個持久樹的有效方法,也是使佈局和繪製高效的關鍵所在。然而,直接操做繪製層會顯得很是奇怪,更糟糕的是還可能引入bug。
Flutter的組件層使用了響應式的模式來組合組件,並以此來操做底層的繪製樹。這一API把樹的建立和修改多個步驟合成爲一個樹的描述(build)步驟,每當APP的狀態(state)發生了改變,那麼UI就會生出新的配置,這個配置是由開發者控制的。以後Flutter對樹的修改作必要的計算來反映出新的修改。
Flutter鼓勵開發者根據當前APP狀態的,做出相應的配置。也就是說,App狀態變了,那麼對應的組件也發生了變化。這個時候須要一種機制能夠保證這些變化是有動畫效果來過渡的。
好比,在狀態1的時候有S1,界面上包含了一個圓圈。可是在下一個狀態S2,它變成了一個方框。沒有任何動畫機制的話,這個顯得很突兀。一個隱式的動畫會讓圓圈通過幾幀以後再變成方框,體驗會更好。
Flutter的口號是:「everything is a widget」,也就是使用基本的組件來構建複雜的組件。積極組合的結果會致使開發者使用大量的組件,這就須要仔細地設計算法和數據結構,這樣才能高效的處理組件。再加上另外的設計,這些數據結構也讓開發者能夠很容易的建立無限滾動列表組件。