FlutterBoost1.0到2.0,我一共做了這幾件事...

作者:閒魚技術-餘玠

一:背景

一定規模的App開發如要引入Flutter開發體系,因某些原因如底層二、三方Native庫或頁面調用,不可避免需要混合開發的能力,但Flutter本身是個單容器的應用,純粹引入SDK會遇到頁面在Flutter和Native跳轉無法流暢切換,沒有統一的路由管理等問題。
我們發佈的FlutterBoost1.0能很好的解決這些問題(文檔參考這裏)。
我們也持續關注到,FlutterBoost的底層容器——Flutter在不斷的演進升級,其演進會給上層應用來到更多可能;同時,在我們的業務應用中,FlutterBoost1.0在線上使用的過程中遇到一些如黑屏和白屏的反饋,這些歷史遺留問題我們希望解決掉。
最後,社區的關注及需求是推動我們前進的動力,我們也希望藉此將FlutterBoost的開源做的更完善,比如增加更多測試用例,更多文檔等等。
這篇文章介紹了FlutterBoost2.0(統稱1.0之後的所有適配版本,下同)針對上述問題的架構升級,並且重點介紹iOS端在升級的過程中遇到的問題和解決方式。

1. 底層容器的升級

1.1 渲染和引擎的解耦

大家知道FlutterBoost1.0是單頁面模式,即不管你打開多少Flutter頁面,其實呈現頁面的FlutterViewController或者FlutterView其實僅有一個,這其實是有歷史原因的。讓我們從Flutter底層的架構說起。
Flutter發展到現在,在Plugin, ViewController(FlutterView),Flutter Engine這三個核心對象的管理上一直在演進。1.5版本是個分水嶺,終於對這三個對象做了較好的解耦。
如flutter0.x版本,這三個對象的關係及我們使用API的方式是這樣的(根本看不到Engine對象):


作爲使用方,我們看不到Engine對象,因爲Engine已經內置在FlutterViewController中,沒有暴露出來。
Flutter1.0做了解耦和改進,如下圖:



我們可以看到雖然全局還是僅有一個FlutterViewController實例,但FlutterEngine被單獨剝離出來。不過三者關係還是沒理清。如,Plugin似乎應該註冊到Engine上更合理,怎麼會和負責渲染的FlutterViewController發生關係;Flutter Engine雖然已經暴露出來給用戶直接使用,但和VC之間還是1對1的關係,按理說負責引擎和數據的Engine全局唯一,渲染層FlutterView應該可以多次切換啊。終於在1.5之後,我們看到Flutter團隊努力的結果:

Engine終於完全剝離,Plugin也終於“嫁”對了人。FlutterViewController和Engine不再是同生共死的組合關係,而僅是個通過VC表現層呈現及事件獲取的依賴。我們可以猜到Flutter嘗試往多引擎方向發展的努力!相應的,我們也給FlutterBoost提出了適配Flutter新架構的要求。

2. 歷史遺留問題

2.1 頁面白屏或者黑屏

FlutterBoost1.0受限於Flutter的架構,出於節省內存的考慮,全局只有一個FlutterViewController。同時在混合頁面滑動切換的時候,爲了快速顯示上一個頁面並實現原生頁面切換的效果,採用CPU截圖的方式爲每個頁面保存了打底圖。打底圖通過文件和內存二級緩存的方式避免持有多張圖片的內存問題。
但這也帶來了一些問題:在切換的過程中因需要對老頁面截圖及加載之前截圖圖片等耗時工作,會偶爾出現白屏或者黑屏問題——截圖和加載都在CPU線程上進行,會影響主線程渲染;而且在頁面切換的時候,截圖和加載圖片操作雖然處於降低內存的目的,但會帶來短暫的內存飆漲(見下圖),雖然持續的時間很短,但帶來了OOM的abort的風險。


2.2 生命週期管理

Flutter目前的架構是單實例的,這意味着一個FlutterViewController的生命週期和整個App的生命週期AppLifecycleState是一致的。頁面完全隱藏或者app切後臺都會發送pause消息告知監聽者告知app要暫停。但這在有多個flutter頁面和原生頁面共存的混合棧情形下顯然不合理。針對單個Flutter Container頁面,也需要有自己可見與不可見的事件通知。
FlutterBoost1.0中沒有解決這個問題,在閒魚內部也導致一些小問題,比如二次打開視頻播放頁面後,老頁面通過app在WidgetBinding中監聽了pause事件就將播放器stop,但同時也將新頁面本自動播放的視頻也停止了。如果有單獨的頁面事件來分別精準控制而非依賴於整個App的事件就能解決這個問題。
我們曾就這個VC頁面和應用生命週期的設計和Flutter的同學討論過,他們也覺得是個問題,但受限於當初的設計,暫沒有具體的解決時間點。

3. 開源建設

升級之前,我們在github上的issue較多,同時文檔不足,升級計劃也不明確,單元測試並不全面。這些短板需要我們在升級後解決。

二:解決方案

正是基於上述的問題,我們對FlutterBoost做了升級,主要有以下幾個方面。

1. 頁面管理方案升級

新版不再維護單一的FlutterViewController(或FlutterView),而是和原生一樣每次有新頁面請求時就直接打開新的ViewController或者FlutterView,和管理原生的頁面一毛一樣。如此,自然也不需要實現截圖功能,小夥伴們再也不用擔心通過CPU截圖導致白屏或者黑屏的困擾啦。
我們看如下頁面結構前後的對比。



如上圖,升級前全局只有一個FlutterViewAdapter(其實是FlutterViewController),負責FlutterView渲染子View並將其內嵌在每個FLBFlutterViewContainer(繼承自UIViewController)中,每次新的FLBFlutterViewContainer拉起時就需要複雜的detach和attach操作來轉移唯一的FlutterView,同時進行截圖緩存。
升級後,不再需要內嵌的FlutterViewAdapter和Screenshot緩存列表:



其底層實現也變的更加簡單,不再需要在detach頁面的時候截圖,下圖是前後兩個方案在頁面pop和push過程中的對比。

2. 內存及穩定性治理

在頁面管理方案升級之後,白屏和黑屏問題解決了,世界就應該安靜了。但我們繼續做了深入的內存及穩定性治理,發現新版本在iOS下每個新的VC打開的時候雖然沒有了內存飆漲的peak,但每個新頁面會帶來約10M內存的增量。拿FlutterBoost的官方demo做了測試,1.54這個版本就是升級後的原始版本:


這個內存增量是因爲什麼導致的?我們升級前後內存變化做了分析,如下圖(左邊是升級前,右邊是升級後):

發現內存的增量主要來自於Anonymous VM和IOSSurface。
Android版本這類問題並不明顯,暫略不表。

2.1 IOSurface的增量

什麼是IOSurface?從apple的文檔裏瞭解到:

The IOSurface framework provides a framebuffer object suitable for sharing across process boundaries.

記得哪一年蘋果的開發者大會上重點提過這個,主要是和iOS上OpenGL的GPU內存和CPU內存管理創新有關,如CVOpenGLESTextureCacheCreateTextureFromImage就是基於IOSurface的能力直接從圖像映射爲紋理,而不像OpenGL官方的glTexImage2D需要將圖像從CPU傳輸到GPU再映射,從而提高了性能。
在Flutter裏,Rasterrizer的setup和teardown會使底層系統創建和銷燬IOSurface。FlutterViewController的surfaceUpdated會用於刪除或創建Rasterrizer。我們review了升級後對於VC的surface的控制,的確有許多不合理之處——多次重複調用surfaceUpdated。
導致多次調用的根本原因脫離不了FlutterViewController單引擎單頁面的設計。
FlutterVC設計並不考慮混合棧的情形,它設想的場景是全局一個engine,只需管理其唯一持有的FlutterVC的生命週期。如此,其surfaceUpdated函數固化在view的appear和disappear事件中,並沒有暴露出來。這導致混合棧在處理多FlutterVC頁面切換的時候,無法重寫頁面事件處理函數而靈活處理何時應該創建和銷燬surface,最終不可避免的重複創建surface或者銷燬surface。
同時,我們發現頁面present和push在iOS13下將被覆蓋的頁面的生命週期和新彈出的頁面生命週期順序還不一樣。比如present在新頁面view appear之後纔會disppear老頁面,並不像push的時候先disppear老頁面然後再appear新頁面。這也給我們處理混合棧頁面的surfaceUpdate帶來了困難。
爲了兼容這個頁面邏輯,並且儘量避免多次創建或銷燬surface,我們重新梳理了頁面的生命週期,只在viewDidAppear的時候重新創建surface,而在viewDisappear的時候僅對非前臺VC進行刪除surface。
但儘管如此,受限於FlutterVC固化了surfaceUpdate的調用,這裏還是難以避免會多重複一次創建和銷燬surface。不過,內存略有改觀,線上穩定性得到不少提升。見下圖內存比較。


2.2 VM的內存增長

從內存分析的Anonymous VM上看不出VM的內存增長的原因,但從升級後有多個VC這個角度看,有許多可解釋這個增長的地方。
每個新的VC打開後,其會通過CAGLLayer渲染內容,CAGLLayer會持有後臺的content。正是基於這個content,iOS系統內部對VC進行截圖,在頁面切換的時候纔有半開半閉的動態效果。相對於FlutterBoost1.0的CPU截圖,系統截圖自然有許多性能的優化,但帶來內存的增長難以避免。
爲了驗證這個問題,我寫了個類似Flutter的用OpenGL渲染UIView的demo。果然,每次打開新頁面,內存肯定增長10M左右。
這個增量似乎難以優化,只能設法避免。目前我們推薦兩種方式:

  1. 通過頁面棧裏頁面個數限制來避免過多頁面導致OOM。如閒魚這邊限制了頁面多次push後,僅保留最近3個頁面。
  2. 避免從Flutter頁面打開Flutter頁面就新建FlutterVC,可重用上次的FlutterVC。FlutterBoost提供了這種能力,BoostContainer繼承了Navigator,原生支持Navigator的能力。但這裏需要用戶自己判斷何時應該使用Navigator的push,何時用FlutterBoost的open來打開頁面。後期我們會增加一些這樣的demo。

3. 穩定性治理

每次底層庫的升級都會帶來穩定性問題。爲了保障穩定性,我們通過收集線上crash日誌的方式,定位到幾個Engine層面的bug。這些bug或提交了issue給google,或在我們engine內部版本做了規避。
如無障礙模式下FlutterSemanticObject泄漏導致crash,參考這裏
在Flutter1.12下,多FlutterVC情形會觸發surface空指針調用而crash,參考這裏
其他還有一些bug,我們在內部版本做了規避,並和google做了溝通,但因復現難度等原因,還未取得一致的結論。
整體上,FlutterBoost2.0在閒魚內部場景升級後,經多輪灰度及線上驗證,穩定性不錯,效果較好。

4. 頁面生命週期管理及其他提升

FlutterBoost完善了之前的ContainerLifeCycle,在Dart層能較好的支持頁面的appear和disappear事件,同時能監聽app切到Background或者Foreground事件。如果涉及到頁面的生命週期管理,建議您使用FlutterBoost.singleton.addBoostContainerLifeCycleObserver()來註冊相關事件監聽程序。
社區同學也給了不少建議,比如幫忙優化了hero動畫的能力等。其他功能提升及使用上的建議,請參考FAQ
爲了整個框架更穩定和易於迴歸驗證,我們也補了一些單元測試。目前主要是Dart層面的用例(覆蓋率達70%左右),後續會增加混合頁面跳轉方面的用例。同時定義了升級計劃和release清單(見首頁)。
在這輪升級後,我們總結了目前FlutterBoost的能力對比表,供參考:

* FlutterBoost2 Flutter官方方案 其他框架
是否支持混合頁面之間隨意跳轉 Y N Y
一致的頁面生命週期管理(多Flutter頁面) Y N ?
是否支持頁面間數據傳遞(回傳等) Y N N
是否支持測滑手勢 Y Y Y
是否支持跨頁的hero動畫 Y Y N
內存等資源佔用是否可控 Y Y Y
是否提供一致的頁面route方案 Y Y N
iOS和Android能力及接口是否一致 Y N N
框架是否穩定,是否線上廣泛驗證 Y N ?
是否已經支持到View級別混合 N N N

三:結尾

綜上所述,經過此次升級,flutterboost解決掉了頁面切換時白屏或者閃爍等問題,同時代碼也更簡潔易懂。同時我們對內存及穩定性做了分析。對於內存上的問題,給出了規避的方式,穩定性上我們主要通過頁面的detach和attach時序優化來解決。
後續我們會繼續優化代碼,增加更多的測試用例,尤其是支持混合測試的用例。同時在Flutter頁面跳Flutter頁面上也在考慮不影響上層業務的基礎上支持一致的API。
路漫漫其修遠兮,FlutterBoost會一直與時俱進,進化爲更加完美的框架。也歡迎大家多參與到這個框架的開發中,一起討論並改進他。
最後,我們也開發了一個使用flutterboost的腳手架:flutter-boot。歡迎使用並送小星星,地址在:https://github.com/alibaba-flutter/flutter-boot

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章