网站视频存储方案,上海的重大新闻,打赏网站怎么建设,金属材料网站建设一、前沿
随着闲鱼的业务快速增长#xff0c;运营类的需求也越来越多#xff0c;其中不乏有很多界面修改或运营坑位的需求。闲鱼的版本现在是每2周一个版本#xff0c;如何快速迭代产品#xff0c;跳过窗口期来满足这些需求#xff1f;另外#xff0c;闲鱼客户端的包…一、前沿
随着闲鱼的业务快速增长运营类的需求也越来越多其中不乏有很多界面修改或运营坑位的需求。闲鱼的版本现在是每2周一个版本如何快速迭代产品跳过窗口期来满足这些需求另外闲鱼客户端的包体也变的很大企业包的大小iOS已经到了94.3MAndroid也到了53.5M。Android的包体大小相比2016年已经增长了近1倍怎么能将包体大小降下来首先想到的是如何动态化的解决此类问题。
对于原生的能力的动态化Android平台各公司都有很完善的动态化方案甚至Google还提供了Android App Bundles让开发者们更好地支持动态化。由于Apple官方担忧动态化的风险因此并不太支持动态化。因此动态化能力就会考虑跟Web结合从一开始基于 WebView 的 Hybrid 方案 PhoneGap、Titanium到现在与原生相结合的 React Native 、Weex。
但Native和JavaScript Context之间的通讯频繁的交互就成了程序的性能瓶颈。于此同时随着闲鱼Flutter技术的推广已经有10多个页面用Flutter实现上面提到的几种方式都不适合Flutter场景如何解决这个问题Flutter的动态化的问题
二、动态方案
我们最初调研了Google的动态化方案CodePush。
2.1 CodePush
CodePush是谷歌官方推出的动态化方案目前只有在Android上面实现了。Dart VM在执行的时候加载isolate_snapshot_data 和isolate_snapshot_instr 2个文件通过动态更改这些文件就达到动态更新的目的。官方的Flutter源码当中已经有相关的提交来做动态更新的内容具体内容可以参考 ResourceExtractor.java。
根据官方给出的Guide我们这边也做了相关的测试patch的包体大小会很大939kb。为了降低包体大小还可以通过增量的修改snapshot文件的方式来更新。通过bsdiff生成的snapshot的差异文件2个文件分别可以缩小到48kb和870kb。
目前看来CodePush还不能做到很好的工程化。而且如何管理patch文件需要制定baseline和patch文件的规则。
2.2 动态模板
动态模板就是通过定义一套DSL在端侧解析动态的创建View来实现动态化比如LuaViewSDK、Tangram-iOS和Tangram-Android。这些方案都是创建的Native的View如果想在Flutter里面实现需要创建Texture来桥接Native端渲染完成之后再将纹理贴在Flutter的容器里面实现成本很高性能也有待商榷不适合闲鱼的场景。
所以我们提出了闲鱼自己的Flutter动态化方案前面已经有同事介绍过方案的原理《做了2个多月的设计和编码我梳理了Flutter动态化的方案对比及最佳实现》下面看下具体的实现细节。
三、模板编译
自定义一套DSL维护成本较高怎么能不自定义DSL来实现模板下发闲鱼的方案就是直接将Dart文件转化成模板这样模板文件也可以快速沉淀到端侧。
3.1 模板规范
先来看下一个完整的模板文件以新版我的页面为例这个是一个列表结构每个区块都是一个独立的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写法一样但是有几点要注意
每个Widget都需要用new或const来修饰数据访问以data开头数组形式以[]访问字典形式以.访问
模板写好之后就要考虑怎么在端上渲染早期版本是直接在端侧解析文件但是考虑到性能和稳定性还是放在前期先编译好然后下发到端侧。
3.2 编译流程
编译模板就要用到Dart的Analyzer库通过parseCompilationUnit函数直接将Dart源码解析成为以CompilationUnit为Root节点的AST树中它包含了Dart源文件的语法和语义信息。接下来的目标就是将CompilationUnit转换成为一个JSON格式。 上面的模板解析出来build函数孩子节点是ReturnStatementImpl它又包含了一个子节点InstanceCreationExpressionImpl对应模板里面的new Container(…)它的孩子节点中我们最关心的就是ConstructorNameImpl和ArgumentListImpl节点。ConstructorNameImpl标识创建节点的名称ArgumentListImpl标识创建参数参数包含了参数列表和变量参数。
定义如下结构体来存储这些信息
class ConstructorNode {// 创建节点的名称String constructorName;// 参数列表Listdynamic argumentsList dynamic[];// 变量参数MapString, 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。先看下整个工程的框架和工作流 工作流程
开发人员编写dart文件编译上传到CDN端侧拿到模板列表并在端侧存库业务方直接下发对应的模板id和模板数据Flutter侧再通过桥接获取到模板并创建Widget树
对于Native测主要负责模板的管理通过桥接输出到Flutter侧。
4.1 模板获取
模板获取分为2部分Native部分和Flutter部分Native主要负责模板的管理包括下载、降级、缓存等。 程序启动的时候会先获取模板列表业务方需要自己实现Native层获取到模板列表会先存储在本地数据库中。Flutter侧业务代码用到模板的时候再通过桥接获取模板信息就是我们前面提到的JSON格式的信息Flutter也会有缓存已减少Flutter和Native的交互。
4.2 Widget创建
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; /// 构建的上下文MapString, dynamic arguments String, dynamic{}; /// 字典参数Listdynamic argumentsList dynamic[]; /// 列表参数dynamic data; /// 原始数据
}
通过以上的逻辑就可以将ConstructorNode树转换为一棵Widget树再交给Flutter Framework去渲染。
至此我们已经能将模板解析出来并渲染到界面上交互事件应该怎么处理
4.3 事件处理
在写交互的时候一般都会通过GestureDector、InkWell等来处理点击事件。交互事件怎么做动态化
以InkWell组件为例定义它的onTap函数为openURL(data.hints[0].href, data.hints[0].params)。在创建InkWell时会以OpenURL作为事件ID查找对应的处理函数当用户点击的时候会解析出对应的参数列表并传递过去代码如下
...
final Listdynamic 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);
}
...
五、 效果
新版我的页面添加了动态化渲染能力之后如果有需求新添加一种组件类型就可以直接编译发布模板服务端下发新的数据内容就可以渲染出来了动态化能力有了大家会关心渲染性能怎么样。
5.1 帧率
在加了动态加载逻辑之后已经开放了2个动态卡片下图是新版本我的页面近半个月的的帧率数据 从上图可以看到帧率并没有降低基本保持在55-60帧左右后续可以多添加动态的卡片观察下效果。 注因为我的页面会有本地的一些业务判断从其他页面回到我的tab都会刷新界面所以帧率会有损耗。 从实现上分析因为每个卡片都需要遍历ConstructorNode树来创建而且每个构建都需要解析出里面的参数这块可以做一些优化比如缓存相同的Widget只需要映射出数据内容并做数据绑定。
5.2 失败率
现在监控了渲染的逻辑如果本地没有对应的Widget创建函数会主动抛Error。监控数据显示渲染的流程中还没有异常的情况后续还需要对桥接层和native层加错误埋点。
六、展望
基于Flutter动态模板之前需要走发版的Flutter需求都可以来动态化更改。而且以上逻辑都是基于Flutter原生的体系学习和维护成本都很低动态的代码也可以快速的沉淀到端侧。
另外闲鱼正在研究UI2Code的黑科技不了解的老铁可以参考闲鱼大神的这篇文章《重磅系列文章UI2CODE智能生成Flutter代码——整体设计篇》。可以设想下如果有个需求需要动态的显示一个组件UED出了视觉稿通过UI2Code转换成Dart文件再通过这个系统转换成动态模板下发到端侧就可以直接渲染出来程序员都不需要写代码了做到自动化运营看来以后程序员失业也不是没有可能了。
基于Flutter的Widget还可以拓展更多个性化的组件比如内置动画组件就可以动态化下发动画了更多好玩的东西等待大家来一起探索。
原文链接 本文为云栖社区原创内容未经允许不得转载。