寫在前面
跨平臺技術由于其一碼多端的生產力提升而表現出巨大的生命力,從早期的Hybrid App到ReactNative/Weex、小程序/快應用,再到現在的Flutter,跨平臺技術一直在解決效率問題的基礎上最大化的解決性能和體驗問題。這也引出了任何跨平臺技術都會面臨的核心問題:
-
效率:解決在多應用、多平臺、多容器上開發效率的問題,一碼多端,業務快跑。
-
性能:解決的是業務的性能和體驗問題。
效率作為跨平臺技術的基本功能,大家都能做到。問題是誰能把性能和體驗做得更好,在渲染技術這塊一共有三種方案:
-
WebView渲染:依賴WebView進行渲染,在功能和性能上有妥協,例如PhoneGap、Cordova、小程序(有的小程序底層也采用了ReactNative等渲染方案)等。
-
原生渲染:上層擁抱W3C,通過中間層把前端框架翻譯為原生控件,例如ReactNative+React、Weex+Vue的組合,這種方案多了一層轉譯層,性能上有損耗。隨著原生系統的升級,在兼容性上也會有問題。
-
自建渲染:自建渲染框架,底層使用Skia等圖形庫進行渲染,例如Flutter、Unity。
Flutter由于其自建渲染引擎,貼近原生的實現方式,獲得了優秀的渲染性能。
Flutter擁有自己的開發工具,開發語言、虛擬機,編譯機制,線程模型和渲染管線,和Android相比,它也可以看做一個小型的OS了。
第一次接觸Flutter,可以看看Flutter的創始人Eric之前的訪談《What is Flutter?》,Eric之前致力于Chromium渲染管線的設計與開發,因此Flutter的渲染與Chromium有一定的相似之處,后面我們會做下類比。
后面我們會從架構和源碼的角度分析Flutter渲染機制的設計與實現,在此之前也可以先看看Flutter官方對于渲染機制的分享《How Flutter renders Widgets》。
視頻+圖文的方式會更加直觀,可以有一個大體的理解。
架構分析
架構設計
從結構上看,Flutter渲染由UI Thread與GPU Thread相互配合完成。 1)UI Thread
對應圖中1-5,執行Dart VM中的Dart代碼(包含應用程序和Flutter框架代碼),主要負責Widget Tree、Element Tree、RenderObject Tree的構建,布局、以及繪制生成繪制指令,生成Layer Tree(保存繪制指令)等工作。
2)GPU Thread
對應圖中6-7,執行Flutter引擎中圖形相關代碼(Skia),這個線程通過與GPU通信,獲取Layer Tree并執行柵格化以及合成上屏等操作,將Layer Tree顯示在屏幕上。
注:圖層樹(Layer Tree)是Flutter組織繪制指令的方式,類似于Android Rendering里的View DisplayList,都是組織繪制指令的一種方式。
UI Thread與GPU Thread屬于生產者和消費者的角色。
流程設計
我們知道Android上的渲染都是在VSync信號驅動下進行的,Flutter在Android上的渲染也不例外,它會向Android系統注冊并等待VSync信號,等到VSync信號到來以后,調用沿著C++ Engine->Java Engine,到達Dart Framework,開始執行Dart代碼,經歷Layout、Paint等過程,生成一棵Layer Tree,將繪制指令保存在Layer中,接著進行柵格化和合成上屏。
具體說來:
1)向Android系統注冊并等待VSync信號
Flutter引擎啟動時,會向Android系統的Choreographer(管理VSync信號的類)注冊并接收VSync信號的回調。
2)接收到VSync信號,通過C++ Engine向Dart Framework發起渲染調用
當VSync信號產生以后,Flutter注冊的回調被調用,VsyncWaiter::fireCallback() 方法被調用,接著會執行 Animator::BeiginFrame(),最終調用到 Window::BeginFrame() 方法,WIndow實例是連接底層Engine和Dart Framework的重要橋梁,基本上與平臺相關的操作都會通過Window實例來連接,例如input事件、渲染、無障礙等。
3)Dart Framework開始在UI線程執行渲染邏輯,生成Layer Tree,并將柵格化任務post到GPU線程執行
Window::BeiginFrame() 接著調用,執行到 RenderBinding::drawFrame() 方法,這個方法會去驅動UI界面上的dirty節點(需要重繪的節點)進行重新布局和繪制,如果渲染過程中遇到圖片,會先放到Worker Thead去加載和解碼,然后再放到IO Thread生成圖片紋理,由于IO Thread和GPI Thread共享EGL Context,因此IO Thread生成的圖片紋理可以被GPU Thread直接訪問。
4)GPU線程接收到Layer Tree,進行柵格化以及合成上屏的工作
Dart Framework繪制完成以后會生成繪制指令保存在Layer Tree中,通過 Animator::RenderFrame() 把Layer Tree提交給GPU Thread,GPU Thread接著執行柵格化和上屏顯示。之后通過 Animator::RequestFrame() 請求接收系統的下一次VSync信號,如此循環往復,驅動UI界面不斷更新。
逐個調用流程比較長,但是核心點沒多少,不用糾結調用鏈,抓住關鍵實現即可,我們把里面涉及到的一些主要類用顏色分了個類,對著這個類圖,基本可以摸清Flutter的脈絡。
綠色:Widget 黃色:Element 紅色:RenderObject
以上便是Flutter渲染的整體流程,會有多個線程配合,多個模塊參與,拋開冗長的調用鏈,我們針對每一步來具體分析。我們在分析結構時把Flutter的渲染流程分為了7大步,Flutter的timeline也可以清晰地看到這些流程,如下所示:
注:ui代表UI Thread,raster代表GPU Thread。
UI Thread
1)Animate
由 handleBeiginFrame() 方法的transientCallbacks觸發,如果沒有動畫,則該callback為空;如果有動畫,則會回調 Ticker.tick() 觸發動畫Widget更新下一幀的值。
2)Build
由 BuildOwner.buildScope() 觸發,主要用來構建或者更新三棵樹,Widget Tree、Element Tree和RenderObject Tree。
3)Layout
由 PipelineOwner.flushLayout() 觸發,它會調用 RenderView.performLayout(),遍歷整棵Render Tree,調用每個節點的 layout(),根據build過程記錄的信息,更新dirty區域RenderObject的排版數據,使得每個RenderObject最終都能有正確的大?。╯ize)和位置(position,保存在parentData中)。
4)Compositing Bits
由 PipelineOwner.flushCompositingBits() 觸發,更新具有dirty合成位置的渲染對象,此階段每個渲染對象都會了解其子項是否需要合成,在繪制階段使用此信息選擇如何實現裁剪等視覺效果。
5)Paint
由 PipeOwner.flushPaint() 觸發,它會調用 RenderView.paint()。最終觸發各個節點的 paint(),最終生成一棵Layer Tree,并把繪制指令保存在Layer中。
6)Submit(Compositing)
由 renderView.compositeFrame() 方法觸發,這個地方官方的說法叫Compositing,不過我覺得叫Compositing有歧義,因為它并不是在合成,而是把Layer Tree提交給GPU Thread,因而我覺得叫Submit更合適。
GPU Thread
7)Compositing
由 Render.compositeFrame() 觸發,它通過Layer Tree構建一個Scene,傳給Window進行最終的光柵化。
GPU Thread通過Skia向GPU繪制一幀數據,GPU將幀信息保存在FrameBuffer里,然后根據VSync信號周期性的從FrameBuffer取出幀數據交給顯示器,從而顯示出最終的界面。
Rendering Pipeline
Flutter引擎啟動時,向Android系統的Choreographer注冊并接收VSync信號,GPU硬件產生VSync信號以后,系統便會觸發回調,并驅動UI線程進行渲染工作。
1 Animate
觸發方法:由 handleBeiginFrame() 方法的transientCallbacks觸發
Animate在 handleBeiginFrame() 方法里由transientCallbacks觸發,如果沒有動畫,則該callback為空;如果有動畫,則會回調 Ticker._tick() 觸發動畫Widget更新下一幀的值。
void handleBeginFrame(Duration rawTimeStamp) {
...
try {
// TRANSIENT FRAME CALLBACKS
Timeline.startSync('Animate', arguments: timelineWhitelistArguments);
_schedulerPhase = SchedulerPhase.transientCallbacks;
final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
_transientCallbacks = <int, _FrameCallbackEntry>{};
callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
if (!_removedIds.contains(id))
_invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack);
});
...
} finally {
...
}
}
handleBeiginFrame() 處理完成以后,接著調用 handleDrawFrame(),handleDrawFrame() 會觸發以下回調:
- postFrameCallbacks用來通知監聽者繪制已經完成。
- pesistentCallbacks用來觸發渲染。
這兩個回調都是SchedulerBinding內部的回調隊列,如下所示:
-
_transientCallbacks:用于存放一些臨時回調,目前是在 Ticker.scheduleTick() 中注冊,用來驅動動畫。
-
_persistentCallbacks:用來存放一些持久回調,不能在此回調中再請求新的繪制幀,持久回調一經注冊就不嫩嫩移除, RenderBinding.initInstaces().addPersitentFrameCallback() 添加了一個持久回調,用來觸發 drawFrame()。
-
_postFrameCallbacks:在Frame結束時會被調用一次,調用后會被移除,它主要是用來通知監聽者這個Frame已經完成。
接著會調用 WidgetBinder.drawFrame() 方法,它會先調用會先調用 BuildOwner.buildScope() 觸發樹的更新,然后才進行繪制。
@override
void drawFrame() {
...
try {
if (renderViewElement != null)
buildOwner.buildScope(renderViewElement);
super.drawFrame();
buildOwner.finalizeTree();
} finally {
assert(() {
debugBuildingDirtyElements = false;
return true;
}());
}
...
}
接著調用 RenderingBinding.drawFrame() 觸發layout、paingt等流程。
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
if (sendFramesToEngine) {
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
_firstFrameSent = true;
}
}
以上便是核心流程代碼,我們接著來Build的實現。
2 Build
觸發方法:由 BuildOwner.buildScope() 觸發。
我們上面說到,handleDrawFrame() 會觸發樹的更新,事實上 BuildOwner.buildScope() 會有兩種調用時機:
-
樹構建(應用啟動時):我們上面提到的 runApp() 方法調用的 scheduleAttachRootWidget() 方法,它會構建Widgets Tree、Element Tree與RenderObject Tree三棵樹。
-
樹更新(幀繪制與更新時):這里不會重新構建三棵樹,而是只會更新dirty區域的Element。
也即是說樹的構建和更新都是由 BuildOwner.buildScope() 方法來完成的。它們的差別在于樹構建的時候傳入了一個 element.mount(null, null) 回調。在 buildScope() 過程中會觸發這個回調。
這個回調會構建三棵樹,為什么會有三棵樹呢,因為Widget只是對UI元素的一個抽象描述,我們需要先將其inflate成Element,然后生成對應的RenderObject來驅動渲染,如下所示:
-
Widget Tree:為Element描述需要的配置,調用createElement方法創建Element,決定Element是否需要更新。Flutter通過查分算法比對Widget樹前后的變化,來決定Element的State是否改變。
-
Element Tree:表示Widget Tree特定位置的一個實例,調用createRenderObject創建RenderObject,同時持有Widget和RenderObject,負責管理Widget的配置和RenderObjec的渲染。Element的狀態由Flutter維護,開發人員只需要維護Widget即可。
-
RenderObject Tree:RenderObject繪制,測量和繪制節點,布局子節點,處理輸入事件。
3 Layout
觸發方法:由 PipelineOwner.flushLayout() 觸發。
- 相關文檔:Understanding constraints
- 相關源碼:PipelineOwner.flushLayout()
Layout是基于單向數據流來實現的,父節點向子節點傳遞約束(Constraints),子節點向父節點傳遞大?。⊿ize,保存在父節點的parentData變量中)。先深度遍歷RenderObject Tree,然后再遞歸遍歷約束。單向數據流讓布局流程變得更簡單,性能也更好。
對于RenderObject而言,它只是提供了一套基礎的布局協議,沒有定義子節點模型、坐標系統和具體的布局協議。它的子類RenderBox則提供了一套笛卡爾坐標體系(和Android&iOS一樣),大部分RenderObject類都是直接繼承RenderBox來實現的。RenderBox有幾個不同的子類實現,它們各自對應了不同的布局算法。
-
RenderFlex:彈性布局,這是一種很常見的布局方式,它對應的是Widget組件Flex、Row和Column。關于這一塊的布局算法代碼注釋里有描述,也可以直接看這篇文章的解釋。
-
RenderStack:棧布局。
我們再來聊聊Layout流程中涉及的兩個概念邊界約束(Constraints)和重新布局邊界(RelayoutBoundary)。
邊界約束(Constraints):邊界約束是父節點用來限制子節點的大小的一種方式,例如BoxConstraints、SliverConstraints等。
RenderBox提供一套BoxConstraints,如圖所示,它會提供以下限制:
- minWidth
- maxWidth
- minHeight
- maxHeight
利用這種簡單的盒模型約束,我們可以非常靈活的實現很多常見的布局,例如完全和父節點一樣的大小,垂直布局(寬度和父節點一樣大)、水平布局(高度和父容器一樣大)。
通過Constraints和子節點自己配置的大小信息,就可以最終算出子節點的大小,接下來就需要計算子節點的位置。子節點的位置是由父節點來決定的。
重新布局邊界(RelayoutBoundary):為一個子節點設置重新布局邊界,這樣當它的大小發生變化時,不會導致父節點重新布局,這是個標志位,在標記dirty的markNeedsLayout()方法中會檢查這個標記位來決定是否重新進行布局。
重新布局邊界這種機制提升了布局排版的性能。
通過Layout,我們了解了所有節點的位置和大小,接下來就會去繪制它們。
4 Compositing Bits
觸發方法:由 PipelineOwner.flushCompositingBits() 觸發。
在Layout之后,在Paint之前會先執行Compositing Bits,它會檢查RenderObject是否需要重繪,然后更新RenderObject Tree各個節點的needCompositing標志。如果為true,則需要重繪。
5 Paint
觸發方法:由 PipeOwner.flushPaint() 觸發。
相關源碼:
- Dart層調用入口:painting.dart
- C++層實現:canvas.cc
我們知道現代的UI系統都會進行界面的圖層劃分,這樣可以進行圖層復用,減少繪制量,提升繪制性能,因此Paint(繪制)的核心問題還是解決繪制命令應該放到哪個圖層的問題。
Paint的過程也是單向數據流,先向下深度遍歷RenderObject Tree,再遞歸遍歷子節點,遍歷的過程中會決定每個子節點的繪制命令應該放在那一層,最終生成Layer Tree。
和Layout一樣,為了提到繪制性能,繪制階段也引入了重新繪制邊界。
重新繪制邊界(RepaintBoundary):為一個子節點設置重新繪制邊界,這樣當它需要重新繪制時,不會導致父節點重新繪制,這是個標志位,在標記dirty的markNeedsPaint()方法中會檢查這個標記位來決定是否重新進行重繪。
事實上這種重繪邊界的機制相對于把圖層分層這個功能開放給了開發者,開發者可以自己決定自己的頁面那一塊在重繪時不參與重繪(例如滾動容器),以提升整體頁面的性能。重新繪制邊界會改變最終的圖層樹(Layer Tree)結構。
當然這些重繪邊界并不都需要我們手動放置,大部分Widget組件會自動放置重繪邊界(自動分層)。
設置了RepaintBoundary的就會額外生成一個圖層,其所有的子節點都會被繪制在這個新的圖層上,Flutter中使用圖層來描述一個層次上(一個繪制指令緩沖區)的所有RenderObject,根節點的RenderView會創建Root Layer,并且包含若干個子Layer,每個Layer又包含多個RenderObject,這些Layer便形成了一個Layer Tree。每個RenderObject在繪制時,會產生相關的繪制指令和繪制參數,并保存在對應的Layer上。
相關Layer都繼承Layer類,如下所示:
-
ClipRectLayer:矩形裁剪層,可以指定裁剪和矩形行為參數。共有4種裁剪行為,none、hardEdge、antiAlias、antiAliashWithSaveLayer。
-
ClipRRectLayer:圓角矩形裁剪層,行為同上。
-
ClipPathLayer:路徑裁剪層,可以指定路徑和行為裁剪參數,行為同上。
-
OpacityLayer:透明層,可以指定透明度和偏移(畫布坐標系原點到調用者坐標系原點的偏移)參數。
-
ShaderMaskLayer:著色層,可以指定著色器矩陣和混合模式參數。
-
ColorFilterLayer:顏色過濾層,可以指定顏色和混合模式參數。
-
TransformLayer:變換圖層,可以指定變換矩陣參數。
-
BackdropFilterLayer:背景過濾層,可以指定背景圖參數。
-
PhysicalShapeLayer:物理性狀層,可以指定顏色等八個參數。
具體可以參考文章上方的Flutter類圖。
聊完了繪制的基本概念,我們再來看看繪制的具體流程,上面提到渲染第一幀的時候,會從根節點RenderView開始,逐個遍歷所有子節點進行操作。如下所示:
1)創建Canvas對象
Canvas對象通過PaintCotext獲取,它內部會創建一個PictureLayer,并通過ui.PictureRecorder調用到C++層創建一個Skia的SkPictureRecorder的實例,并通過SkPictureRecorder創建SkCanvas,而后將SkCanvas返回給Dart Framework使用。SkPictureRecorder可以用來記錄生成的繪制命令。
2)通過Canvas執行繪制
繪制命令會被SkPictureRecorder記錄下來。
3)通過Canvas結束繪制,準備進行柵格化
繪制結束后,會調用 Canvas.stopRecordingIfNeeded() 方法,它會接著去調用C++層的SkPictureRecorder::endRecording()方法生成一個Picture對象并保存在PictureLayer中,Picture對象包含了所有的繪制指令。所有的Layer繪制完成,形成Layer Tree。
繪制完成以后,接著就可以向GPU Thread提交Layer Tree了。
6 Submit(Compositing)
觸發方法:由 renderView.compositeFrame() 方法觸發。
- Dart層調用入口:compositing.dart widow.dart
- C++層實現:scene.cc scene_builder.cc
注:這個地方官方的說法叫Compositing,不過我覺得叫Compositing有歧義,因為它并不是在合成,而是把Layer Tree提交給GPU Thread,因而我覺得叫Submit更合適。
void compositeFrame() {
Timeline.startSync('Compositing', arguments: timelineArgumentsIndicatingLandmarkEvent);
try {
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer.buildScene(builder);
if (automaticSystemUiAdjustment)
_updateSystemChrome();
_window.render(scene);
scene.dispose();
assert(() {
if (debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled)
debugCurrentRepaintColor = debugCurrentRepaintColor.withHue((debugCurrentRepaintColor.hue + 2.0) % 360.0);
return true;
}());
} finally {
Timeline.finishSync();
}
}
-
創建SceneBuilder對象,并通過 SceneBuilder.addPicture() 將上文中生成的Picture添加到SceneBuilder對象對象中。
-
通過 SceneBuilder.build() 方法生成Scene對象,接著會通過window.render(scene)將包含繪制指令的Layer Tree提交給CPU線程進行光柵化和合成。
在這個過程中Dart Framework層的Layer會被轉換為C++層使用的flow::layer,Flow模塊是一個基于Skia的簡單合成器,運行在GPU線程,并向Skia上傳指令信息。Flutter Engine使用flow緩存Paint階段生成的繪制指令和像素信息。我們在Paint階段的Layer,它們都與Flow模塊里的Layer一一對應。
Graphics Pipeline
7 Raster&Compositing
有了包含渲染指令的Layer Tree以后就可以進行光柵化和合成了。
光柵化是把繪制指令轉換成對應的像素數據,合成是把各圖層柵格化后的數據進行相關的疊加和特性處理。這個流程稱為Graphics Pipeline。
相關代碼:rasterizer.cc
Flutter采用的是同步光柵化。什么是同步光柵化?
同步光柵化:
光柵化和合成在一個線程,或者通過線程同步等方式來保證光柵化和合成的的順序。
直接光柵化:直接執行可見圖層的DisplayList中可見區域的繪制指令進行光柵化,在目標Surface的像素緩沖區上生成像素的顏色值。 間接光柵化:為指定圖層分配額外的像素緩沖區(例如Android提供View.setLayerType允許應用為指定View提供像素緩沖區,Flutter提供了Relayout Boundary機制來為特定圖層分配額外緩沖區),該圖層光柵化的過程中會先寫入自身的像素緩沖區,渲染引擎再將這些圖層的像素緩沖區通過合成輸出到目標Surface的像素緩沖區。
異步分塊光柵化:
圖層會按照一定的規則粉塵同樣大小的圖塊,光柵化以圖塊為單位進行,每個光柵化任務執行圖塊區域內的指令,將執行結果寫入分塊的像素緩沖區,光柵化和合成不在一個線程內執行,并且不是同步的。如果合成過程中,某個分塊沒有完成光柵化,那么它會保留空白或者繪制一個棋盤格圖形。
Android和Flutter采用同步光柵化策略,以直接光柵化為主,光柵化和合成同步執行,在合成的過程中完成光柵化。而Chromium采用異步分塊光柵化測量,圖層會進行分塊,光柵化和合成異步執行。
從文章上方的序列圖可以看到,光柵化的入口是 Rasterizer::DoDraw() 方法。它會接著調用 ScopedFrame::Raster() 方法,這個方法就是光柵化的核心實現,它主要完成以下工作:
- LayerTree::Preroll():處理繪制前的一些準備工作。
- LayerTree::Paint():嵌套調用不通Layer的繪制方法。
- SkCanvas::Flush():將數據flush給GPU。
- AndroidContextGL::SwapBuffers():交換幀緩存給顯示器顯示。
到這里我們Flutter整體的渲染實現我們就分析完了。
Android、Chromium與Flutter
Android、Chromium、Flutter都作為Google家的明星級項目,它們在渲染機制的設計上既有相似又有不同,借著這個機會我們對它們做個比較。
現代渲染流水線的基本設計:
我們再分別來看看Android、Chromium和Flutter是怎么實現的。
Android渲染流水線:
Chromium渲染流水線:
Flutter渲染流水線:
相互比較:
寫在最后
最后的最后,談一談我對跨平臺生態的理解。
跨平臺容器生態至少可以分為三個方面:
前端框架生態
前端框架生態直接面向的是業務,它應該具備兩個特點:
- 擁抱W3C生態
- 相對穩定性
它應該是擁抱W3C生態的。W3C生態是一個繁榮且充滿活力的生態,它會發展的更久更遠。試圖拋棄W3C生態,自建子集的做法很難走的長遠。這從微信小程序、Flutter都推出for web系列就能看出端倪。
它應該是相對穩定的。不能說我們每換一套容器,前端的業務就需要重新寫一遍,例如我們之前做H5容器,后來做小程序容器,因為DSL不通,前端要花大力氣將業務重寫。雖然小程序是一碼多端,但是我認為這并沒有解決效率問題,主要存在兩個問題:
-
前端的學習成本增加,小程序的DSL還算簡單,Flutter的Widget體系學習起來就需要花上一點時間,這些對于團隊來說都是成本。
-
業務代碼重寫,大量邏輯需要梳理,而且老業務并不一定都適合遷移到新容器上,比如小程序本來就是個很輕量的解決方案,但是我們在上面堆積了很多功能,造成了嚴重的體驗問題。
在這種情況下,業務很難實現快速奔跑。所以說不管底層容器怎么變,前端的框架一定是相對穩定的。而這種穩定性就有賴于容器統一層。
容器統一層
容器統一層是在前端框架和容器層之間的一個層級。它定義了容器提供的基本能力,這些能力就像協議一樣,是相對穩定的。
協議是非常重要的,就像OpenGL協議一樣,有了OpenGL協議,不管底層的渲染方案如何實現,上層的調用是不用變的。對于我們的業務也是一樣,圍繞著容器統一層,我們需要沉淀通用的解決方案。
- 統一API解決方案
- 統一性能解決方案
- 統一組件解決方案
- 統一配套設施解決方案
- 等等
這些東西不能說每搞一套容器,我們都要大刀闊斧重來一遍,這種做法是有問題的。已經做過的東西,遇到新的技術就推倒重來,只能說明以前定義的方案考慮不周全,沒有考慮沉淀統一和擴展的情況。
如果我們自顧自的一遍遍做著功能重復的技術方案,業務能等著我們嗎。
容器層
容器層的迭代核心是為了在解決效率問題的基礎上最大化的解決性能和體驗問題。
早期的ReactNative模式解決了效率了問題,但是多了一個通信層(ReactNative是依靠將虛擬DOM的信息傳遞給原生,然后原生根據這些布局信息構建對應的原生控件樹來實現的原生渲染)存在性能問題,而且這種轉譯的方式需要適配系統版本,帶來更多的兼容性問題。
微信后續又推出了小程序方案,在我看來,小程序方案不像是一個技術方案,它更像是一個商業解決方案,解決了平臺大流量規范管理和分發的問題,給業務方提供通用的技術解決方案,當然小程序底層的渲染方案也是多種多樣的。
后起之秀Flutter解決的痛點是性能能力,它自建了一套GUI系統,底層直接調用Skia圖形庫進行渲染(與Android的機制一樣),進而實現了原生渲染。但是它基于開發效率、性能以及自身生態等因素的考慮最終選擇了Dart,這種做法無疑是直接拋棄了繁榮的前端生態,就跨平臺容器的發展歷史來看,在解決效率與性能的基礎上,最大化的擁抱W3C生態,可能是未來最好的方向。Flutter目前也推出了Flutter for Web,從它的思路來看,是先打通Android與iOS,再逐步向Web滲透,我們期待它的表現。
容器技術是動態向前發展的,我們今年搞Flutter,明年可能還會搞其他技術方案。在方案變遷的過程中,我們需要保證業務快速平滑的過度,而不是每次大刀闊斧的再來一遍。
隨著手機性能的提升,WebView的性能也越來越好,Flutter又為解決性能問題提供了新的思路,一個基礎設施完善,體驗至上,一碼多端的跨平臺容器生態值得期待。