隨着閒魚的業務快速增加,運營類的需求也愈來愈多,其中不乏有不少界面修改或運營坑位的需求。閒魚的版本如今是每2週一個版本,如何快速迭代產品,跳過窗口期來知足這些需求?另外,閒魚客戶端的包體也變的很大,企業包的大小,iOS已經到了94.3M,Android也到了53.5M。Android的包體大小,相比2016年,已經增加了近1倍,怎麼能將包體大小降下來?首先想到的是如何動態化的解決此類問題。java
對於原生的能力的動態化,Android平臺各公司都有很完善的動態化方案,甚至Google還提供了Android App Bundles讓開發者們更好地支持動態化。因爲Apple官方擔心動態化的風險,所以並不太支持動態化。所以動態化能力就會考慮跟Web結合,從一開始基於 WebView 的 Hybrid 方案 PhoneGap、Titanium,到如今與原生相結合的 React Native 、Weex。android
但Native和JavaScript Context之間的通信,頻繁的交互就成了程序的性能瓶頸。於此同時隨着閒魚Flutter技術的推廣,已經有10多個頁面用Flutter實現,上面提到的幾種方式都不適合Flutter場景,如何解決這個問題Flutter的動態化的問題?git
咱們最初調研了Google的動態化方案CodePush。程序員
CodePush是谷歌官方推出的動態化方案,目前只有在Android上面實現了。Dart VM在執行的時候,加載isolate_snapshot_data
和isolate_snapshot_instr
2個文件,經過動態更改這些文件,就達到動態更新的目的。官方的Flutter源碼當中,已經有相關的提交來作動態更新的內容,具體內容能夠參考 ResourceExtractor.java。github
根據官方給出的Guide,咱們這邊也作了相關的測試,patch的包體大小會很大(939kb)。爲了下降包體大小,還能夠經過增量的修改snapshot文件的方式來更新。經過bsdiff生成的snapshot的差別文件,2個文件分別能夠縮小到48kb和870kb。shell
目前看來,CodePush還不能作到很好的工程化。並且如何管理patch文件,須要制定baseline和patch文件的規則。數據庫
動態模板,就是經過定義一套DSL,在端側解析動態的建立View來實現動態化,好比LuaViewSDK、Tangram-iOS和Tangram-Android。這些方案都是建立的Native的View,若是想在Flutter裏面實現,須要建立Texture來橋接;Native端渲染完成以後,再將紋理貼在Flutter的容器裏面,實現成本很高,性能也有待商榷,不適合閒魚的場景。express
因此咱們提出了閒魚本身的Flutter動態化方案,前面已經有同事介紹過方案的原理:《作了2個多月的設計和編碼,我梳理了Flutter動態化的方案對比及最佳實現》,下面看下具體的實現細節。數組
自定義一套DSL,維護成本較高,怎麼能不自定義DSL來實現模板下發?閒魚的方案就是直接將Dart文件轉化成模板,這樣模板文件也能夠快速沉澱到端側。緩存
先來看下一個完整的模板文件,以新版個人頁面爲例,這個是一個列表結構,每一個區塊都是一個獨立的Widget,如今咱們指望將「賣在閒魚」這個區塊動態渲染,對這個區塊拆分以後,須要3個子控件:頭部、菜單欄、提示欄;由於這3部分界面有些邏輯處理,因此先把他們的邏輯內置。
內置的子控件分別是MenuTitleWidget
、MenuItemWidget
和HintItemWidget
,編寫的模板以下:
@override Widget build(BuildContext context) { return new Container( child: new Column( children: <Widget>[ new MenuTitleWidget(data), // 頭部 new Column( // 菜單欄 children: <Widget>[ new Row( children: <Widget>[ new MenuItemWidget(data.menus[0]), new MenuItemWidget(data.menus[1]), new MenuItemWidget(data.menus[2]), ], ) ], ), new Container( // 提示欄 child: new HintItemWidget(data.hints[0])), ], ), ); }
中間省略了樣式描述,能夠看到寫模板文件就跟普通的widget寫法同樣,可是有幾點要注意:
new
或const
來修飾data
開頭,數組形式以[]
訪問,字典形式以.
訪問 模板寫好以後,就要考慮怎麼在端上渲染,早期版本是直接在端側解析文件,可是考慮到性能和穩定性,仍是放在前期先編譯好,而後下發到端側。
編譯模板就要用到Dart的Analyzer
庫,經過parseCompilationUnit
函數直接將Dart源碼解析成爲以CompilationUnit
爲Root節點的AST樹中,它包含了Dart源文件的語法和語義信息。接下來的目標就是將CompilationUnit
轉換成爲一個JSON格式。
上面的模板解析出來build函數孩子節點是ReturnStatementImpl
,它又包含了一個子節點InstanceCreationExpressionImpl
,對應模板裏面的new Container(…)
,它的孩子節點中,咱們最關心的就是ConstructorNameImpl
和ArgumentListImpl
節點。ConstructorNameImpl
標識建立節點的名稱,ArgumentListImpl
標識建立參數,參數包含了參數列表和變量參數。
定義以下結構體,來存儲這些信息:
class ConstructorNode { // 建立節點的名稱 String constructorName; // 參數列表 List<dynamic> argumentsList = <dynamic>[]; // 變量參數 Map<String, dynamic> arguments = <String, dynamic>{}; }
遞歸遍歷整棵樹,就能夠獲得一個ConstructorNode
樹,如下代碼是解析單個Node的參數:
ArgumentList argumentList = astNode; for (Expression exp in argumentList.arguments) { if (exp is NamedExpression) { NamedExpression namedExp = exp; final String name = ASTUtils.getNodeString(namedExp.name); if (name == 'children') { continue; } /// 是函數 if (namedExp.expression is FunctionExpression) { currentNode.arguments[name] = FunctionExpressionParser.parse(namedExp.expression); } else { /// 不是函數 currentNode.arguments[name] = ASTUtils.getNodeString(namedExp.expression); } } else if (exp is PropertyAccess) { PropertyAccess propertyAccess = exp; final String name = ASTUtils.getNodeString(propertyAccess); currentNode.argumentsList.add(name); } else if (exp is StringInterpolation) { StringInterpolation stringInterpolation = exp; final String name = ASTUtils.getNodeString(stringInterpolation); currentNode.argumentsList.add(name); } else if (exp is IntegerLiteral) { final IntegerLiteral integerLiteral = exp; currentNode.argumentsList.add(integerLiteral.value); } else { final String name = ASTUtils.getNodeString(exp); currentNode.argumentsList.add(name); } }
端側拿到這個ConstructorNode
節點樹以後,就能夠根據Widget的名稱和參數,來生成一棵Widget樹。
端側拿到編譯好的模板JSON後,就是解析模板並建立Widget。先看下,整個工程的框架和工做流:
工做流程:
對於Native測,主要負責模板的管理,經過橋接輸出到Flutter側。
模板獲取分爲2部分,Native部分和Flutter部分;Native主要負責模板的管理,包括下載、降級、緩存等。
程序啓動的時候,會先獲取模板列表,業務方須要本身實現,Native層獲取到模板列表會先存儲在本地數據庫中。Flutter側業務代碼用到模板的時候,再經過橋接獲取模板信息,就是咱們前面提到的JSON格式的信息,Flutter也會有緩存,已減小Flutter和Native的交互。
Flutter側當拿到JSON格式的,先解析出ConstructorNode
樹,而後遞歸建立Widget。
建立每一個Widget的過程,就是解析節點中的argumentsList
和arguments
並作數據綁定。例如,建立HintItemWidget
須要傳入提示的數據內容,new HintItemWidget(data.hints[0])
,在解析argumentsList
時,會經過key-path的方式從原始數據中解析出特定的值。
解析出來的值都會存儲在WidgetCreateParam
裏面,當遞歸遍歷每一個建立節點,每一個widget均可以從WidgetCreateParam
裏面解析出須要的參數。
/// 構建widget用的參數 class WidgetCreateParam { String constructorName; /// 構建的名稱 dynamic context; /// 構建的上下文 Map<String, dynamic> arguments = <String, dynamic>{}; /// 字典參數 List<dynamic> argumentsList = <dynamic>[]; /// 列表參數 dynamic data; /// 原始數據 }
經過以上的邏輯,就能夠將ConstructorNode
樹轉換爲一棵Widget
樹,再交給Flutter Framework去渲染。
至此,咱們已經能將模板解析出來,並渲染到界面上,交互事件應該怎麼處理?
在寫交互的時候,通常都會經過GestureDector
、InkWell
等來處理點擊事件。交互事件怎麼作動態化?
以InkWell
組件爲例,定義它的onTap
函數爲openURL(data.hints[0].href, data.hints[0].params)
。在建立InkWell
時,會以OpenURL
做爲事件ID,查找對應的處理函數,當用戶點擊的時候,會解析出對應的參數列表並傳遞過去,代碼以下:
... final List<dynamic> tList = <dynamic>[]; // 解析出參數列表 exp.argumentsList.forEach((dynamic arg) { if (arg is String) { final dynamic value = valueFromPath(arg, param.data); if (value != null) { tList.add(value); } else { tList.add(arg); } } else { tList.add(arg); } }); // 找到對應的處理函數 final dynamic handler = TeslaEventManager.sharedInstance().eventHandler(exp.actionName); if (handler != null) { handler(tList); } ...
新版個人頁面添加了動態化渲染能力以後,若是有需求新添加一種組件類型,就能夠直接編譯發佈模板,服務端下發新的數據內容,就能夠渲染出來了;動態化能力有了,你們會關心渲染性能怎麼樣。
在加了動態加載邏輯以後,已經開放了2個動態卡片,下圖是新版本個人頁面近半個月的的幀率數據:
從上圖能夠看到,幀率並無下降,基本保持在55-60幀左右,後續能夠多添加動態的卡片,觀察下效果。
注:由於個人頁面會有本地的一些業務判斷,從其餘頁面回到個人tab,都會刷新界面,因此幀率會有損耗。
從實現上分析,由於每一個卡片,都須要遍歷ConstructorNode
樹來建立,並且每一個構建都須要解析出裏面的參數,這塊能夠作一些優化,好比緩存相同的Widget,只須要映射出數據內容並作數據綁定。
如今監控了渲染的邏輯,若是本地沒有對應的Widget建立函數,會主動拋Error。監控數據顯示,渲染的流程中,尚未異常的狀況,後續還須要對橋接層和native層加錯誤埋點。
基於Flutter動態模板,以前須要走發版的Flutter需求,均可以來動態化更改。並且以上邏輯都是基於Flutter原生的體系,學習和維護成本都很低,動態的代碼也能夠快速的沉澱到端側。
另外,閒魚正在研究UI2Code的黑科技,不瞭解的老鐵,能夠參考閒魚大神的這篇文章《重磅系列文章!UI2CODE智能生成Flutter代碼——總體設計篇》。能夠設想下,若是有個需求,須要動態的顯示一個組件,UED出了視覺稿,經過UI2Code轉換成Dart文件,再經過這個系統轉換成動態模板,下發到端側就能夠直接渲染出來,程序員都不須要寫代碼了,作到自動化運營,看來之後程序員失業也不是沒有可能了。
基於Flutter的Widget,還能夠拓展更多個性化的組件,好比內置動畫組件,就能夠動態化下發動畫了,更多好玩的東西等待你們來一塊兒探索。
原文連接 本文爲雲棲社區原創內容,未經容許不得轉載。