乾貨 | Flutter控件CustomScrollView原理解析及應用實踐

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"說起移動端跨平臺解決方案,Flutter無疑是最近被談到最多的話題。相對於React Native這樣的前端技術棧,Flutter更貼近於客戶端的技術棧特性,所以迅速獲得大批原移動端開發的熱烈擁護,再加上其優秀的渲染性能和友好的開發模式,目前已經在業內被廣泛使用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"攜程酒店研發從去年底開始對Flutter進行可行性調研,在今年年初陸續完成了酒店詳情頁和酒店列表頁的轉Flutter工作,通過這項工作,實現了客戶端技術棧的統一,大大提高了研發效率和雙端一致性。在Flutter開發的過程中,對CustomScrollView的使用是比較多的,這也是我們開發過程中比較重要和複雜的控件。    "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/af\/af5911b5d5e0b8c136ef09a9ecb9ee98.jpeg","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖1  CustomScrollView可承載的子佈局類型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CustomScrollView是Flutter的SDK提供的實現長列表的控件。它像一個強大的粘合劑,如圖1所示在此控件中我們可以將各種不同的佈局,比如列表,網格,瀑布流,吸頂組件等,在其裏面組合,實現較爲複雜的頁面。以往在Native的開發中,官方組件沒有提供如此強大的組合能力,我們在Native中實現列表中組合不同佈局,或者是通過index映射佈局類型這種異構的方式,或者需要自己去自定義一個能夠組合不同佈局的控件,都沒有CustomScrollView方便。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/78\/78957c692b648dadaf83c3017ce0d5b3.jpeg","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖2  酒店詳情頁使用的主要sliver類型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"圖2是攜程酒店詳情頁主要模塊所使用到的佈局類型。如上文提到,系統提供的佈局方式還是很強大的,基本能夠滿足我們這個相對複雜頁面大多數的佈局要求,當然有些特殊的模塊,需要去做一些定製,比如通過定製“paintOrigin”實現的日曆模塊的特殊吸頂交互等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於這個較複雜且使用廣泛的組件的內部實現原理有較深入的瞭解,對於我們的應用以及後續的性能優化都有較大意義。因此本文將對其實現原理做一定的剖析,並就其在實際工作中的應用實踐給出具體例子。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一、概述"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.1 Flutter渲染流程簡述"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/38\/38a46c0624a39ed6d773f4d66dedfab8.jpeg","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖3  Flutter渲染流程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"FlutterUI繪製的驅動主要可以簡述如圖3所示。可以看到,Flutter的Framewrok在啓動初始化後主要構建了四顆樹Widget、Element、RenderObject和Layer。然後在系統Vsync的驅動下,通過它們的改變生成出繪製每一幀畫面的數據,然後顯示到屏幕上。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中的Widget樹是平常接觸最多的一顆樹,它類似一顆配置數據樹,配置頁面的樣子。而RenderObject樹則是一顆真正的實現生成繪製內容樹,完成各個控件的大小計算,佈局,以及繪製數據,它的數據來源就是前面的Widget樹。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"中間的Element樹更像是一個媒介,因爲Flutter借鑑了當今比較流行的React的思想,它並不希望我們還是像以前在Native的時候直接去操作RenderObject,而是希望我們在它的框架下面只配置我們想要什麼,以及狀態怎麼改變,而最終的複雜的位置計算和如何繪製交給它解決。因此中間的Element樹因此就應運而生,它會負責根據Widget樹去生成和改變RenderObject樹,當然這個過程中會做一定的Diff策略,從而儘量減少RenderObject樹的變化,因爲RenderObject樹的變化相對來說是比較大的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最終RenderObject樹會生成Layer樹,Layer樹是Flutter engine所需要的數據格式,Flutter engine會利用這顆樹進行相應渲染,並最終繪製在我們宿主平臺提供給Engine的畫布上。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/76\/766420b6b01625857a33ad63af44cb6c.jpeg","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖4  CustomScrollView的三層結構"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CustomScrollView作爲Flutter提供的控件,其內部結構肯定也是上述這樣,圖4給出了其三層(Widget,Element,RenderObject)對應的結構圖。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先看一下其在Widget這層的主要構成。總的來說由兩部分構成:第一部分是Srollable,這層主要是接受用戶手勢同時根據配置參數,決定相應的滑動位置;第二部分是真正要顯示的內容ViewPort,這層會根據監聽Srollable給設置的offset,去將自己的顯示內容也就是一個個的sliver展示出來。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"中間的一層是Viewport Element,然後就是最後的RenderObject層。RenderObject主要是由展示窗口RenderViewPort和其具體的展示內容條目List(Render sliver)組成。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣的分層設計方式還是很清晰,解耦的,相對於以往Native將上述的大部分內容聚合在一個View類裏面,Flutter在這方面還是做了相應的設計的。儘量將不同職責的內容做了拆分,完成高內聚低耦合,從而能在多變的場景的應用中組合,實現相應的功能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"總的來說,不管是Widget還是RenderObject層,各自都可以對應的分成兩部分,一部分負責監聽用戶手勢然後計算自己對應滑動偏移值Offset,還有一部分則是具體展示內容,以及相應地怎麼佈局。下面我們以一個垂直向下滾動的CustomScrollView爲例對它的實現做一些具體的剖析。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"二、Srollable"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.1  Srollable總述"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/9f\/9f07cc626ea6df859627a2210def1ebe.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖5  Srollable的Build方法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我們來看一下Srollable的builder方法如圖5所示。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Srollable最外面一層是Srollablescrope,這層可以理解爲一個輔助層。我們可以利用它在Srollable的子Widget裏很方便地鎖定到對應的Srollable。在Srollable中有一個“of”方法,這個方法就是依靠Srollablescrope的“Type”很方便地定位到Srollable。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而在Srollablescrope的child層則是此方法的核心,主要是通過“RawGestureDetector”去監聽了用戶的滑動手勢,從而讓Srollable根據用戶的滑動手勢去做相應的位置變化。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.2 觸摸事件的監聽"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面主要介紹一下主要的4個觸摸事件處理:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)DragDown"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/e0\/e0cc3dd0312110d4652fec1dac3813e6.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖6  dragDown觸摸事件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如圖6所示,這個事件主要是對應用戶手指按下跟屏幕接觸的時刻。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)DragStart"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/95\/95a4ccdc86059e5d174fbcd7e672ef88.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖7  dragStart觸摸事件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如圖7所示,這個是手勢Recongnize認爲用戶這次的操作已經達到了drag的標準,此時用戶本次手勢的操作才真正被認爲是一個合法的drag動作的開始。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3)DrageUpdate"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/9a\/9adbb5786b3bf9b6627505bab93836da.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖8  dragUpdate觸摸事件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如圖8所示,這個手勢代表用戶在dragStart後在屏幕上move的更新值。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4)DragEnd"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/59\/59eaff78dc770e49646e930dce124c36.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖9  dragEnd觸摸事件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如圖9,dragEnd這個手勢代表用戶的手離開了屏幕,也就意味着這次手勢操作的結束。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"   "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過這幾個方法,我們可以看到,手勢的開始是通過“scrollPosition”生成了一個drag對象,然後接下來的update,end都是讓這個對象進行處理,因此這個對象纔是真正決定了當前的scrollView如何應對用戶的操作,而進行相應的改變的處理類。接下來我們就重點來看這個類都做了什麼。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.3 ScrollPosition"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/20\/20f9eb53ad6b1ca439f922d266bab41d.jpeg","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖10  scrollPosition類圖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"圖10給出了” scrollPosition”主要關係的一個類圖,下面我們具體看一下它們各自的作用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)首先可以看到” scrollPosition”是繼承於ViewportOffset和ScrollMetrics這兩個類。其中ScrollMetrics主要描述了scroll基本的一些狀態信息。比如當前Srollable可視區域的大小,最小、最大的滑動offset限制,以及當前的offset。而ViewportOffset則提供了很多改變offset的方式,比如不帶任何過渡交互效果就直接滑動到某個offset的“jumpto”方法,還有可以以帶動畫的方式滑動到某個offset的“animateto”。同時可以看到ViewportOffset的父類是一個ChangeNotifier,也就是說” scrollPosition”改變是可以被觀察的。因此可想而知Srollable的子child也就是真正我們要顯示的內容ViewPort會以觀察者的模式監聽它的改變,從而做出相應的變化。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)再來看下ScrollPhysics這個重要的類,它主要決定了滑動位置處於一些邊界場景情況下,對於用戶的滑動應該怎麼去反饋。比如說對於overScroll的反饋即用戶滑動的位置超過scrollview的最大或最小活動限制的邊緣時,在Android和iOS這兩個平臺上的表現是不一樣的。在Android平臺上默認是不讓用戶overscroll的,就是不能滑動超過邊緣,而在iOS平臺上則允許。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"又比如我們經常使用的PageView(它的原理與scrollView類似)。它要求每次滑動都是整頁滑動。即使用戶在滑動手擡起時,頁面當前的offset位置還處於兩個頁面的過渡期間,不是一個整頁。這時候PageView對應的ScrollPhysics就會再給一個自動的矯正滑動,讓我們的頁面滑動到對應的整頁。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ScrollPhysics在SDK中已經提供了好幾種實現。比如提供給Android平臺的“ClampingScrollPhysics”,提供給iOS平臺的默認的是“BouncingScrollPhysics”。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這些不同類型的ScrollPhysics是可以組合使用的,ScrollPhysics本身的設計也考慮到了這點。在構造一個ScrollPhysics時,我們可以傳入一個默認的ScrollPhysics,也就是說新的ScrollPhysics默認就會組合傳入的ScrollPhysics特性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來具體看一下這個類可以用來控制特性的一些重要的方法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"“applyPhysicsToUserOffset”方法"},{"type":"text","text":":當用戶手勢滑動超出scrollable最大或最小的滑動界限時,也就是我們常說的overscroll狀態時,對用戶手勢做出一定的矯正。比如通過算法轉換壓縮用戶的滑動距離,從而體現出一定的阻尼效果,讓用戶感知到已經滑到邊緣了,沒有可以滑動的內容了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"“shouldAcceptUserOffset”方法"},{"type":"text","text":":它配置用戶是否能夠滑動scrollable。比如說NeverScrollableScrollPhysics的這個方法永遠返回的都是false,那也就意味着scrollable不允許用戶通過手勢去滑動它。當然一般情況我們實際使用時都是返回true,允許滑動。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"“applyBoundaryConditions”方法"},{"type":"text","text":":它主要也是爲“overscroll”場景服務。它決定了用戶的滑動位置能否overscroll。這個方法的返回值是一個矯正值,比如BouncingScrollPhysics 永遠返回的都是0,也就是說它允許用戶進行overscroll。而“ClampingScrollPhysics”在overscroll狀態的返回的是一個非0的矯正值,會將新的offset矯正到scrollable的boundary裏面來,避免出現overscroll。因此如果我們想要實現一個一端可以overscroll,另一端不允許的scrollable,就可以通過重寫這個方法加以實現。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"“createBallisticSimulation”方法"},{"type":"text","text":":它主要是返回一個變化的方程式。其大多數的應用場景主要是用來在用戶的操作或者說滑動結束時有個反彈的效果。比如在PageView中當用戶滑動結束手擡起時,頁面的滑動位置不是一個整頁的位置,這個方法就會返回一個方程式,然後我們就看到了一個按照這個方程式變化反彈動畫,滑動到一個整頁的位置。類似的iOS平臺上默認的BouncingScrollPhysics在overscroll時,手鬆開時也會有一個反彈的動畫,也是由這個方程決定。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"“recommendDeferredLoading”方法"},{"type":"text","text":":它主要是提供給scrollable自己的顯示內容子控件使用。其目的是爲了提高性能,比如當我們做了“Fling”這樣的快速動作後,scrollable接下來可能會滑動一個非常大的距離,而在這個距離中間的很多很耗資源的數據在這個過程不需要加載,因爲用戶基本也不會看到。特別典型的比如圖片,因此在這個過程中這些耗資源的組件就可以通過這個方法判斷是否需要延遲加載,以提高性能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"總的來說ScrollPhysics還是非常重要的,它承擔用戶在scrollable上滑動各種特殊場景的效果邏輯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3)ScrollContext:它主要是充當一個媒介角色,其真正的實現就是ScrollableState,目的主要是讓scrollPosition可以去改變ScrollableState的一些能力。比如說在做某個滑動的過程中,scrollable中的內容是否能接受點擊,以及控制用戶能否對scrollable進行滑動。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4)ScrollActivity:這個類主要負責封裝當scrollable接受到用戶的各種手勢事件後做各種不同的流程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比如當用戶的手勢被確認識別成drag動作後就會發起一個“DragScrollActivity”,負責此後用戶手勢在此基礎上的新的滑動變化的處理,一直到用戶手勢擡起結束後怎麼反應。還有比如像用戶在滑動過程中突然有系統框彈出該怎麼處理等這些針對具體場景的處理,都封裝成了特定的流程,定義在這個類的某個具體實現子類裏面,由其負責具體處理。像上文講的用戶手鬆開後的一個反彈效果,對應就是“BallisticScrollActivity”。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5)Controller:這個類是我們在使用CustomScrollView時經常會設置的一個參數,它顧名思義就是一個控制器可以讓我們去控制ScrollView,設置參數讓它去滾動。之所以能夠控制,是因爲在內部綁定了前面講的scrollPosition,因此能讓我們利用它去控制CustomScrollView滑動,以及監聽CustomScrollView最新的狀態。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"   "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"小結一下,scrollPosition主要負責用來實現對ScrollView的offset計算怎麼改變,而physics是scrollPosition用來做怎麼改變的重要的規則和限制,而最終scrollPosition又通過Controller與外界的CustomScrollView的使用者串聯,讓外界可以操控和獲得CustomScrollView的滑動狀態。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至此CustomScrollView第一個重要的部分滑動位置改變的控制,我們基本就分析完了,接下來看一下有了這個具體的滑動的Offset,顯示的內容怎麼展示。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"三、ViewPort"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.1  整體佈局流程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ea\/ea1f1e88044c596e986e9030dba6891f.jpeg","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖11  RenderViewport佈局流程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來我們來看真正展示內容的ViewPort它的RenderObject(RenderViewport)是怎麼佈局的。如圖11所示,是其佈局的整個流程概況。可以看到其主體的流程還是比較簡單的,從第一個child不斷的遍歷到最後一個child,從而完成整個ViewPort的佈局。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"裏面有個特殊場景會拋出Error的異常,我們在佈局每個child的過程中,會把當前scrollview的offset作爲輸入給當前正在佈局的child,而某些chid在做內部佈局的時候,可能會認爲scrollview給的offset會有問題需要矯正。比如說用來展示長列表的SliverList在做內部佈局的時候,如果SliverList發現自己的child已經全部佈局完了,但是scrollview給的offset還沒有填滿,這時候就會認爲scrollview給的offset太長了,會給一個矯正值,讓它縮短回去。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.2  吸頂效果(Pinned)的實現原理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實際開發中用的比較多的一個效果是吸頂。在Native的開發中,一般這個效果是我們自己去實現的。但是CustomScrollview很強大,直接提供了這個功能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對應的控件是SliverPersistentHeader,並將其pinned屬性設置爲true,就可以實現吸頂效果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/70\/70cb9d2722ea9f5249a6fbb614d72f43.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖12 RenderSliverPinnedPersistentHeader的佈局代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其對應的renderObject是RenderSliverPinnedPersistentHeader,它的佈局代碼如圖12所示。重點關注一下其返回給renderViewPort的SliverGeometry中的paintOrigin,這個參數直接給的就是“constraints.overlap”。那麼這個參數在renderViewport中具體代表什麼意思哪。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/e1\/e11af909e6fd89480313c04136d812a0.jpeg","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖13 RenderViewport佈局流程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"再回頭來看renderViewport的layoutChildSequence方法。前面說了這個方法會遍歷自己所有的子sliver然後逐個佈局,在這個過程中我們着重關注一下maxPaintOffset和layoutOffset這兩個變量。在普通場景下這兩個值都是從0開始,隨着對child list的遍歷而做相應的遞增,也就是說默認的情況下這兩個offset都是相等的。但是參考圖13所示,黃色部分的某個pinned sliver child模塊如果前面已經出現了紅色區域的吸頂部分,那麼此時對於黃色的這個child這兩個值的位置就不是一致的了。如圖中所示,可以看到此時對於它的PaintOffset是比layoutOffset大的,而它們之間的差值就是作爲輸入傳給黃色sliver的overlap。可以看到RenderSliverPinnedPersistentHeader在自己的佈局方法中,在返回給renderViewPort的“SliverGeometry”返回值中的paintOrigin就是直接賦的這個值。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後再回到renderviewport裏,可以看到renderviewport在拿到child的這個參數會做如圖14所示的一個修正流程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/e1\/e12ba102983af20e5cde53ea42c9cf2f.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖14 renderViewport修正LayoutOffset"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"也就是說render viewport會用子sliver回傳的paintOrigin矯正一下最後真正繪製的offset,經過這個矯正後的offset正好是圖13中所示的已經吸頂(紅色)部分的底部。當用戶再繼續往上滑動時,本應該滑出可視區域的黃色sliver,因爲上面講的處理,將一直繪製在屏幕上方,因此實現了吸頂效果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/9d\/9dd0271024b7e64373a0cedfdecf1fef.gif","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖15 日曆部分階段性吸頂效果"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了這個參數我們可以很多特殊的處理,比如酒店詳情頁的日曆,交互要求其是階段性吸頂。就是說雖然要吸頂,但不是一直都是吸頂的,當房型區域滑出屏幕時要隨着最後一個房型的底部同步滑出,如圖15所示。我們知道customscrollview默認沒提供這樣的實現,後來就是通過監聽最後一個房型的滑動位置,然後去改變日曆吸頂組件中“paintOrigin”參數的值,從而完成了此效果。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.3  Tab按鈕和錨定"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/91\/91542dcf75c8385bc80d3e259bdaf2d6.gif","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖16 Tab按鈕和錨定效果"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如圖16所示的tab變化和錨定是我們經常會遇到的場景,這個時候需要準確地知道要錨定的模塊所對應的offset值,而Tab的變換就是一個反向的過程,即當前scrollview的offset對應到了哪個具體的模塊。說白了就是需要一個轉化公式,給定一個指定的模塊我們需要知道其對應的offset值。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"很慶幸scrollview直接提供了對應的接口,如圖17所示。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a1\/a1642e22e4038e909bacbb094bb88151.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖17 獲取指定child展示在可視區域內offset的函數"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面我們分析過renderViewport會在每次佈局時對其所有的子sliver進行佈局,同時每個child會返回它們自己的佈局結果。那麼在返回結果裏面跟這個方法緊密相關的兩個變量是scrollExtent和maxScrollObstructionExtent,其中“scrollExtent”代表了這個child自己擁有的滑動距離,而maxScrollObstructionExtent則主要是爲吸頂的sliver所服務的, 它表示這個吸頂的sliver處於吸頂狀態時所佔的吸頂區域的高度。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當我們要獲得某個具體的sliver滑動到屏幕可視區域最上方所需要的offset時,其實就是把該sliver前方所有的sliver的scrollExtent相加,同時減去該sliver前面所有吸頂的sliver的maxScrollObstructionExtent,就可以獲得相應的offset值。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.4  長列表的懶加載機制和其子renderObject的複用機制"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來我們再看一下非常重要同時大家都很關注的長列表的懶加載機制和內存複用的機制。我們還是用展示向下佈局的長列表“SliverList”作爲代表來介紹一下。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"3.4.1 懶加載機制"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/95\/95f64f1aece49c263c8eab960c3abb7f.jpeg","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖19 SliverList的佈局流程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如圖19所示是SliverList佈局的主要流程,大體可以分爲三個階段。第一個階段和第二階段主要是定位,定位在當前scrollView對應的scrollOffset下在可視窗口內用戶所能看到的第一個child是誰。那麼第一個階段是從上一次佈局結果的firstChild按其index的逆序往前找,找到第一個自己的scrollOffset比scrollView的scrollOffset小的child。在這個過程中找到的child是有可能在用戶的可視範圍內的,再往前的child用戶肯定是看不見了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二個階段是一個相反的過程,它會從第一個階段找到的那個child往後找,找到第一個child的尾部是超過scrollView的當前的scrollOffset。那麼這個child就是接下來用戶在當前所能看到的第一個child了,本次的佈局也只需從這個child開始,index在這個child之前的children相應肯定是看不到的,因此本次佈局和渲染會忽略它們。在這之後會定義一個遊標trailingChild指向child。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來就進入了第三階段,真正創建和佈局本次渲染所要的所有child。算法也很清晰,一直往下逐個遍歷和佈局接下來child,直到某個child的末尾超過了本次佈局一開始提前限定的範圍。這個範圍一般是scrollView可視範圍的窗口高度再加上一個cache距離。至此整個佈局就全部結束了。可以看到對於一個有很多數據的列表來說,在本次佈局中,只有用戶可視範圍內的child會參與其中,不在的都會被忽略,從而實現了懶加載,大大提高了繪製性能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了SliverList,sdk中的Grid,開源的瀑布流組件StaggeredGrid等長列表實現懶加載的機制也是類似,只是排列自己子child的佈局方式不一樣。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"3.4.2 內存的複用管理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在以往Native的開發中,內存的複用是大家非常關心的問題,因爲長列表可能會對內存造成非常大的壓力,從而出現OOM。我們在接觸flutter的時候也很好奇,下面來看一下SliverList在這塊的處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/1c\/1cd7498d127b81042fcda6c8d0012bed.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖20 SliverList單個child的創建或重用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/2b\/2bbcbc8c5a7c371a1ebb072077cc9f6a.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖21 SliverList單個child的銷燬或回收"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"sliverList創建和回收每個scrollview的child的方法分別如圖20和圖21所示。從創建的代碼可以看到,其首先會去一個keepAliveBucker的Map裏面根據該child的index去尋找有沒有對應的child緩存。如果有,會重用這個緩存裏面的child,如果沒有,則會使用childManager去真正地創建一個child對象。在destory方法中主要是一個逆向的過程,會首先判斷輸入的child是不是要做緩存的,如果是則放入緩存池,如果不是則會真正將其對象銷燬。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/1a\/1ab11268bc1c07b678c407bf7d36698d.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖22 keepAlive後keepAliveBucker中節點的數量"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到這裏面是否會做緩存主要是由一個keepAlive的標誌決定的。對於sliverList默認情況下所有的child是不開啓keepAlive的,也就是說每次佈局只要是被認爲不需要的child都會被銷燬。而如果我們需要讓某個child變爲keepAlive狀態,只需要在這個child的widget外面用“AutomaticKeepAliveClientMixin”包裝一下,就可以實現對它做緩存。圖22所示是把每個child都設置成keepAlive的狀態後的緩存截圖,可以看到keepAliveBucker這個Map裏面緩存了每個index對應的child,數量達到了200多個child。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"總的來說,Flutter在長列表的內存複用這塊基本沒采取特別的優化措施。如果我們打開child的keepAlive,也只是一個對應到index的簡單的重用,並沒有像Native那樣去設計比較複雜的複用機制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從我們之前的應用來看,不用keepAlive對於像List,Grid這樣的普通佈局在使用時性能還好,但是如果是瀑布流的佈局,在Android某些機型上如果不開啓keepAlive對性能有一定影響,當然開啓後對內存的消耗也相應會增大。對於這塊需要思考如何做進一步的優化。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"四、結語"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至此,對於CustomScrollView這個Flutter中比較複雜的且應用廣泛的組件的大體運行機制我們就分析完了。應該說在應用的方便性上,相對以往Native中的組件在功能上還是更強大的,它像一個粘合劑,讓我們可以在它裏面組合各種不同的佈局子組件,以往在Native的開發中這些大都需要我們自己去定製。當然在數據量很大的情況下,對內存使用這塊的設計相對以前Native還是比較簡單的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"後續我們也會在應用繼續深入的基礎上,在功能上做進一步的豐富以及在性能上考慮如何做進一步的優化。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"作者簡介:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"popeye,攜程軟件技術專家,關注移動端跨端技術,致力於快速,高性能地支撐業務開發。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文轉載自:攜程技術中心(ID:ctriptech)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/dF5Id3w_To4aXeXDbUnUjQ","title":"xxx","type":null},"content":[{"type":"text","text":"乾貨 | Flutter控件CustomScrollView原理解析及應用實踐"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章