學習最忌盲目,無計劃,零碎的知識點沒法串成系統。學到哪,忘到哪,面試想不起來。這裏我整理了Flutter面試中最常問以及Flutter framework中最核心的幾塊知識,大概化二十篇左右文章分析,歡迎關注,共同進步。![Flutter framework]面試
歡迎搜索公衆號:進擊的Flutter或者runflutter 裏面整理收集了最詳細的Flutter進階與優化指南。關注我,獲取個人最新文章~緩存
最近由於在作Flutter中相關的性能優化,搜刮了網上全部的文章以後,看到了閒魚的Flutter 高性能、多功能的全場景滾動容器。但奈何該組件沒有開源,所以準備從文章給出的思路嘗試研究和開發一個高性能的ScrollView。這個系列預計會分爲4-5篇文章,前三篇主要對現有問題研究和分析,後兩篇實際的進行開發。性能優化
原理篇:markdown
一、Widget、Element、Render樹到底是如何造成的?app
二、ListView的構建過程與性能問題分析less
要想分析ListView的性能問題,首先咱們得掌握ListView的構建過程。在閱讀本文以前,最好已經熟悉Flutter三棵樹以及基本的佈局原理與Flutter滑動原理,否則構建過程的理解可能任然停留在表面。推薦Widget、Element、Render樹到底是如何造成的?和總結了30個例子以後,我悟到了Flutter的佈局原理,深刻進階-一張圖理清Flutter的滑動原理ide
上一期文章中咱們學習了Flutter中三棵樹的構建過程,引伸出Flutter UI體系中的一個重要設計思想,即:svn
Widget -> 對於每個頁面元素的抽象,便於開發者使用。
Element -> 管理整個UI的構建,橋接Widget與RenderObject,提供高效的刷新機制。 RenderObject -> 屏蔽每個元素具體的佈局和渲染細節。佈局
對於Flutter中任何的控件,咱們均可以從這三個類掌握它的構建渲染過程(animation階段,build階段,layout階段,paint階段)!!! 一樣的,咱們也以這三個類爲線索剖析ListView,先來張總覽圖壓壓驚!post
看到這張圖是否是頭皮發麻,別急,咱們一步步分析。
首先,咱們將聚光燈打在咱們今天的主角ListView上
上圖中咱們能夠看出,首先ListView繼承於BoxScrollView繼承於ScrollView繼承於StatelessWidget。咱們知道StatelessWidget是組合類的Widget,它只是在build()方法中組合多個Widget造成嵌套結構。在這個繼承關係中build()方法由ScollView實現。
這個方法中,首先調用了 List<Widget> buildSlivers(BuildContext context)
這個抽象方法獲得一個Widget的集合 slivers。這個方法最終由ListView實現,返回的是一個SliverList(slivers集合中只有這一個元素)。這個集合做爲參數被傳入buildViewport
,而這個方法的結果被嵌套在Scrollable中
這個方法返回一個Viewport類的組件,Viewport是一個能夠顯示多個Widget(採用Sliver佈局協議)的組件,根據滑動偏移顯示不一樣區域,這個滑動偏移由Scrollable收集滑動手勢提供。整個Widger的主要嵌套結構就是 ScollView(ListView) -> Scrollable -> Viewport -> SliverList 。看完這兩段源碼以後回過去看上面的小圖,是否是清晰了許多。
咱們知道,Flutter中,widget只是一個配置文件,構建過程當中主要的開銷在於Element樹創建與更新,由BuildOwner管理。根據上面梳理的Widget嵌套結構,咱們能夠查出對應的三棵樹的結構(忽略其中次要嵌套結構)
在Widget、Element、Render樹到底是如何造成的一期中提到,Element樹造成的過程就是根據Widget的嵌套的每個節點遞歸的調用Element.mount()這個方法將本身插入樹中。對於組合類的Widget-ScrollView和Scrollable咱們很清楚,他的mount()過程核心在於updateChild(_child, built, slot)
方法,在第一次構建的時候這個方法會調用子節點的inflateWidget(newWidget, newSlot)
生成對應的Element對象並插入到樹中。
而對於渲染類的Widget-ViewportElement在上一篇文章中也提到了,child節點的element集合會掛載到他的children屬性上,RenderObject對象經過雙向鏈表進行管理。這裏因爲Viewport下面只有一個child即SliverList,因此這裏他只有一個子節點SliverMutiBoxAdaptorElement。而最後的SliverMultiBoxAdaptorElement節點中,咱們發現他並無重寫mount()方法
因此這裏執行的是父類RenderObjectElement的mount()。
RenderObjectElement.mount()這個方法咱們在上一期分析過,首先調用super.mount()將本身掛載在Element樹上。以後的核心邏輯就是圖中標記的方法。這個方法會向上找到最近的RenderObject,而後將本身掛載上去,造成RenderObject樹。
看到這裏實際上Element和Render樹都只到了ListView這一層級,與每個item沒有關聯。那麼咱們在使用ListView的時候,每個item節點到底是如何插入到這個樹中的呢?
要解決上面的疑惑,先思考兩個本質問題。
一、在當前Flutter的UI體系中,有沒有Widget能夠繞過Element樹直接顯示到屏幕上(不考慮Scene等底層Api)?
二、若是ListView的item在mount階段就所有掛載到element樹上了,會有什麼問題?
第一個問題,若是這樣的Widget,那麼ListView的每個item可能不須要掛載就能夠顯示。但就目前個人瞭解,是不存在的(若是有誤,歡迎評論交流)。渲染到屏幕上的Widget最終都會經過RenderObject實現繪製的細節。查看RenderObject的markNeedsPaint()方法,在其調用裏面有一個關鍵點,就是他會依賴樹形結構。而RenderObject樹的造成依賴RenderObjectElement。因此ListView的每個item必定會在某個階段併入到Element和RenderObject樹中。
第二個問題,通常咱們在使用ListView的時候每每是item數量較多,若是在mount階段一次性掛載了全部的節點,那麼在構建的節點很容易發生卡頓,借鑑原生的思路也有一個重要的設計方法懶加載。
懶加載能夠理解爲按需加載,如何理解"按需"?"按需"就是須要顯示到屏幕上的頁面元素,那麼咱們如何判斷這個元素須要顯示到頁面上呢?最簡單的思路就是,在佈局過程過程當中,不停的佈局子節點,直到當前窗口範圍被佈滿或者沒有子節點。在Flutter中,還額外增長了一個緩存區(double cacheExtent),因此這個範圍變成了窗口大小加上緩存區大小(默認是250)
提到佈局,那麼天然咱們從RenderObject樹開始捋,先看看RenderViewport的佈局過程。
///佈局僅由父節點決定,與child節點無關,寬高從performResize中獲取
@override
bool get sizedByParent => true;
@override
void performResize() {
assert(() {
if (!constraints.hasBoundedHeight || !constraints.hasBoundedWidth) {
///拋出沒有寬高限制的異常
}
return true;
}());
///尺寸爲寬高的最大值
size = constraints.biggest;
switch (axis) {
case Axis.vertical:
offset.applyViewportDimension(size.height);
break;
case Axis.horizontal:
offset.applyViewportDimension(size.width);
break;
}
}
複製代碼
由於RenderViewport中sizeByParent爲true,說明他的大小僅由父元素給約束決定,與子節點無關。 再看他的performLayout()
@override
void performLayout() {
double mainAxisExtent;
double crossAxisExtent;
switch (axis) {
case Axis.vertical:
mainAxisExtent = size.height;
crossAxisExtent = size.width;
break;
case Axis.horizontal:
mainAxisExtent = size.width;
crossAxisExtent = size.height;
break;
}
final double centerOffsetAdjustment = center.centerOffsetAdjustment;
double correction;
int count = 0;
do {
///嘗試佈局
correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment);
if (correction != 0.0) {
///有偏差,修正
offset.correctBy(correction);
} else {
///沒有偏差,跳出循環
if (offset.applyContentDimensions(
math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
))
break;
}
count += 1;
} while (count < _maxLayoutCycles);
}
複製代碼
這個方法會調用_attemptLayout,最終調用layoutChildSequence對於viewport中的每個child進行layout(使用Sliver約束佈局,區別於以前提到的Box約束)因爲咱們的child只有一個即RenderSliverList,因此查看他的佈局過程是怎樣。
源碼太長,以Element爲線索的話,我畫了時序圖標明主要的流程。RenderSliverList中有個循環,當endScrollOffset小於targetEndScrollOffset的時候,會調用insertAndLayoutChild()
,這個方法最終會調用到SliverMultiBoxAdaptorElement中,由代理類SliverChildBuilderDelegate生成child(ListView的itemBuilder傳遞到這兒),以後對每個child進行layout,累加endScrollOffset。
有了這樣的認識回去看前面的結構圖,是否是要更清晰了一點。
固然這裏面有兩個細節咱們能夠關注一下
一、在RenderSliverList的佈局過程當中,child節點的element建立是運行在BuildOwner的buildScope方法中
二、ListView會對每個child節點經過delegate嵌套KeyedSubtree、AutomaticeKeepAlive、RepaintBoundary組件
最後借用upYang大佬在Flutter ListView 是如何管理 item 的?中畫的兩張神圖表示這個過程。
一、ListView的建立
二、ListView的滾動
在粗略的瞭解了ListView的構建過程以後,咱們開始對ListView使用過程當中的問題進行分析。
閒魚的Flutter 高性能、多功能的全場景滾動容器一文中提到,咱們在使用ListView的使用每每會組合刷新控件,添加加載更多的功能。當加載更多的時候,咱們通常會經過刷新列表來顯示更多元素。最終會調用到SliverMultiBoxAdaptorElement.performRebuild()
清空全部 child widget 緩存,從新 build child widget,update child Element;若是遇到數據的變化,例如 insert、delete,頗有可能致使 element 沒法複用,這樣 rebuild 的成本會更高。經過斷點發現,調用setState()以後,item的build會重走一遍。這時若是對於ListView有粒度更細的操做,例如原生上Adapter的增長刪除等操做,那麼在這種場景下就能帶來必定的優化。
其次就是Element的複用,SliverMultiBoxAdaptorElement 經過 _childElements 來緩存 elements,當滾動超出 viewport 的顯示以及預加載範圍或者數據源發生變化,會經過調用 collectGarbage 方法回收不須要的 elements;
這個方法最終會調用到SliverMultiBoxAdaptorElement.removeChild(RenderBox child)
其中核心的updateChild方法的第一個參數傳遞的是index對應的element對象,而第二個參數變成了null,在原來我一直在錯誤的使用 setState()? 中提到過,在第二個參數爲null的時候,那麼以前的element對象會被卸載unmount()。這樣在二次建立的時候,該index對應的element對象又會被再次建立。因此這裏能夠經過創建一個element緩存池,在建立的時候優先從緩衝池獲取;
最後一點就是每個item的分幀上屏,我的感受這點比較有意義。由於即便上面咱們將加載更多的場景進行了優化,可是在ListView建立的時候,任會對屏幕上能顯示的Widget進行構建,若是item較爲複雜,在進入頁面的時候,可能發生構建卡頓。經過佔位削峯的,將複雜widget分爲多幀渲染是一個不錯的思路,不過暫時還沒想明白如何實現。
這期源碼梳理花了很長的時間,遠遠沒有行文時的流暢,由於早期陷入了Sliver約束的佈局過程當中,研究了好久。但其實咱們的優化和佈局關係不是很大,要根據線索去梳理主幹源碼,這樣纔不會陷入其中沒法自拔。
最後感謝一下參考學習到各位大佬的文章:
法佬:大佬,sliver的一輩子之敵 Flutter Sliver一輩子之敵 (ExtendedList)
upYang:製圖大佬,對於滑動研究很是深入Flutter ListView 是如何滾動的?
TravelingLight_:大佬,能夠看看各類Sliver的解析Flutter - 按部就班 Sliver
對於ListView的分析就到這兒了,下面就打算對於這幾個問題開幹了!理解完原理以後,對於解決問題也有了必定的思路,下一期聊聊對於這個ListView的功能設計與規劃,歡迎持續關注!!
最後求個贊QAQ,你的贊是我更新路上強大的動力。