iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 27
1
Mobile Development

Why Flutter why? 從表層到底層,從如何到為何。系列 第 27

days[26] = "為什麼Flutter的渲染樹這麼複雜?(上)"

如果你有在Follow這系列的話,應該會注意到我們每次談到關於三顆渲染樹中的細節時,事情總是會變得非常有趣。具體上來說,我指的是這幾篇的內容:

雖然已經寫了八篇又臭又長的文章,但想必你也知道這還不是全部,我想這系列剩下幾篇可能也沒辦法把它講完了。不過至少到現在,我們應該已經對這三顆樹有足夠的瞭解,可以來回答這個問題:「Flutter渲染樹這麼多複雜的機制,到底是為了什麼?」

先講結論,一切都是為了Flutter的激進式複合(Aggressive Composability),也就是被我戲稱為走火入魔的Composition over Inheritance的設計哲學。(註:這裡的走火入魔沒有貶意,是一種誇飾的稱讚)

不過,為了解釋為什麼Flutter要"走火入魔",首先我們必須瞭解一點歷史。

Oh-My-God-View in Android

如果你有寫過Android,一定知道我在說什麼。在Android世界裡,用來代表一個UI元件的base class叫做View,其它所有TextView, ImageView, ListView, LinearLayout, ConstraintLayout...無數的sub-class都是繼承自它。猜猜看這個View class的原始碼有幾行?27753行

你現在一定很想知道Flutter的Widget base class有幾行吧?120行。當然這樣比較可能不太公平,我們再加上Element和RenderObject吧?3321行。而且,為求方便,這些都是加上註解的行數,別忘了Flutter原始碼裡的註解常常都是又「香」又長,簡稱「香長」,實際程式碼的比例又更低了。

View的這將近三萬行都在做什麼呢?雖然沒有人想真的去看這些原始碼,但我們可以換個角度,來看看Android View提供了哪些屬性,就可以看出它負擔了多少責任。稍微瀏覽一下這將近100個屬性,我們也可以清楚的看到Android View和Flutter南轅北轍的設計哲學:

  • 所有View都有可能變透明,所以我們加一個alpha屬性吧
  • 所有View都有可能有背景,當然要有個background屬性
  • 所有View都有可能須要padding,也把它加進去吧
  • 所有View都有可能被隱藏,再加個visibility
  • 所有View都有可能須要各種互動吧?加入onClick, onLongClick, onTouch, onDrag...
  • 所有View都有可能進行各種變形吧?將入rotation, scale, translation...

這還只是一些常見、容易理解的屬性,其它還有很多放在base class根本沒道理的,像是textAlignment, textDirection, scrollbars, scrollIndicators, importantForAutofill, autofillHints...。

或是放在base class可能有點道理,但是邊緣到不能再邊緣的情境:keyboardNavigationCluster, requiresFadingEdge, soundEffectsEnabled, screenReaderFocusable...。

前面不厭其煩的重複「所有View都有可能...」因為真的就只是有可能而已。實際上在99%的情境中,我們只會使用其中不到10%的屬性。但為了那1%的使用情境,其它90%的屬性都被加入了View這個base class裡,而繼承它的所有subclass,也就同時背負了這將近100個屬性和27753行的複雜邏輯。

對了這還只是最最上層的base class而已,其實View底下還有幾個被很多subclass繼承的大家長,像是ViewGroup, ImageView, TextView。這些第二層的base class也都發生了和View一樣的問題,為了支援subclass中各式各樣的情境,負擔了許多很少會用到的屬性和邏輯。來看看TextView本身的12552行程式碼被哪些View繼承了:
https://ithelp.ithome.com.tw/upload/images/20200927/20129053ugxcnyCieE.png
這也是Composition over Inheritance的一個絕佳案例。在Android中的Button是一個(is-a)TextView,到了Flutter就變成Button可以有一個(has-as)Text Widget,當然也可以組合其它任何Widget,不但提供了更大的彈性,責任也被更良好的分配。

雖然我對iOS的瞭解沒有像Android這麼多,但據我所知iOS的View繼承樹也有著和Android一模一樣的問題。

Flutter的激進式複合

前面講了那麼多,都是為了讓我們更加清楚的理解,為什麼Flutter不僅僅只是採納了由React發揚光大的陳述式和複合式UI,還更進一步地創造了激進式複合,走向和原生View徹底相反的道路。

須要padding嗎?包一個Padding Widget。須要互動嗎?加一個GestureDetector。要變透明嗎?就用Opacity。在我的1000個Widget裡,有一個不想讓它被focus?我們連ExcludeFocus都準備好了。什麼?你說Android所有View都有個focusable屬性,為什麼Flutter沒有?拜託,都2020了,人家都已經上太空了,你還在殺豬公。

把無數個從熱門到邊緣的屬性全部拆解開來,變成一個個輕如鴻毛的Widget,再透過一層層複合的方式去設定每一個屬性,就是Flutter的激進式複合了。

這一切的重點不在於要什麼屬性都可以包一個Widget上去,而是在於當我們不需要這些屬性時,它們一個都不會存在

當然,激進式複合也不是全部只有優點,它的問題主要分成兩部份:
首先對一般人來說最顯而易見的,自然是它嚴重影響了程式碼的可讀性。Flutter最常被詬病的絕對就是它深不見底的Widget Tree。雖然Flutter團隊也提供了像Container這樣,利用Widget解壓縮的技巧,讓你能透過設定Widget參數的方式來滿足幾個最常見的情境,像是padding, alignment, color等等,但就算盡量使用了Container,Widget Tree複雜起來還是非常複雜。

講個題外話,有人可能會覺得使用Container這樣的Widget,是不是有點在走回頭路呢?常常我們只是為了padding或color而使用Container,卻同時引入了不必要的alignment, margin, transform等等。如果我們把更多常用不常用的Widget都壓縮進Container,變成它的屬性,好像最後又變回Android View了不是嗎?
https://ithelp.ithome.com.tw/upload/images/20200927/20129053elgFTyrabN.png
的確,如果我們到處使用Container的話,是會產生一些不必要的浪費。但是你猜怎麼著?我們可以不用Container。如果我們只需要Padding,就用Padding就好了。如果我們只需要顏色和置中,就用ColoredBox加Center,一個不多一個不少。重點在於我們有得選擇

Flutter Framework最有彈性的地方,就在於它提供了從最高階的MaterialApp、到中高階的ListTile、低階的Text等Widget。而且不只是Widget,還可以繼續深入Element, RenderObject, Layer...一路直達dart:ui。求快求方便就使用高階組件,須要越多客製越多效能,就用越低階的API,一切都開放給我們依照自身需求取用。

好了繼續回到激進式複合的問題。第二點就是這無數Widget可能產生的效能問題。在Android的View Tree中,通常有個五層十層就很多了,但在Flutter中可能隨便就是二三十層。也就是說當我們在遍歷整個Widget Tree執行一些演算法時,它的n往往是很大的,而我們也就必須對於演算法的效能吹毛求疵,盡可能的設計能夠達到O(n)甚至更小的演算法、針對最常發生的情境做最佳化、導入各種機制來避免不必要的計算和渲染。

於是,這個第二點的效能問題,就是為什麼Flutter會有三顆渲染樹,為什麼裡面的各種機制這麼複雜的原因。一切都是為了能夠支援Flutter的激進式複合的設計理念,讓我們在開發時有最大的彈性和方便,同時在執行時有最佳的效能。

而我們下一篇將會繼續深入,看看這裡面的每個機制,是如何增進了演算法的效能的,敬請期待!


上一篇
days[25] = "為什麼動畫需要Ticker?"
下一篇
days[27] = "為什麼Flutter的渲染樹這麼複雜?(中)"
系列文
Why Flutter why? 從表層到底層,從如何到為何。30

尚未有邦友留言

立即登入留言