珊瑚海 - 一站式跨端动态化布局框架原理解析

目录

1. 珊瑚海介绍

CoralSea官网: http://doc.58corp.com/CoralSea

珊瑚海是安居客发起,58无线团队参与共建的一站式动态布局框架,支持 Android、iOS、小程序、H5. 包含引擎框架、DSL 管理后台、可拖拽低代码前端、JS 开发框架等全套基础能力。适用于 UI 交互、动画复杂性较低、布局动态要求高的页面。

一站式工具链:

  • 轻量级,快速接入

接入改造成本低,仅需少量的代码进行接入,包大小只增加 100k

  • 跨平台

底层使用 Yoga,支持 Android、iOS、小程序、H5,高度的 UI 一致性

  • 性能优

高性能,Yoga 底层 C++ 实现,可用于首页、混合页面,列表滑动丢帧率低

  • 替换客户端列表、卡片开发模式

新的列表、卡片样式,可以内置在 App 中,提前编译成原生代码(低代码+跨端+性能),无 DSL 加载解析过程,扁平化性能更优

  • 丰富的基础组件、事件,支持自定义

基础组件、事件丰富,支持注册自定义组件、事件

  • 支持卡片级别的动态化,高可复用性

支持基于卡片级别的动态化,DSL 文件更小。高复用性,一套卡片可用于不同页面。DSL 前端提供卡片仓库,开发、上线更快捷

  • 支持页面的动态化

少量代码即可让整页面作为布局动态化载体

  • DSL 管理后台一站式上线发布

提供 DSL、模版创建、修改、发布的一站式服务

  • 低代码前端支持拖拽、sketch 导入

低代码前端操作便捷,适用于各种角色

  • 低代码前端支持 RN、Flutter 对接

打造大前端 DSL 生态

1.1 58同城预接入效果

DSL 管理后台 + 低代码前端:

App:

描边卡片为珊瑚海卡片:

1.2 安居客落地业务

珊瑚海房价卡片页面:

  • 一套DSL跨Android/iOS,跨58App/安居客,开发成本节省 50%
  • 体验与原生一致,具备 RN 的热更新能力,性能优于 RN 30%

房产909项目经纪人门店单页

  • 使用珊瑚海 + BFF,9月上线以来热修复问题1次,覆盖率 100%

整改需求- 删除当前APP的服务痕迹

  • 跨 Android & iOS,表单展示类页面,0.5人/日快速开发完成

1.3 后期落地计划

# 业务 解决的痛点
1 58同城厂商包 1. 快速响应厂商问题 2. 优化包大小
2 58同城、58本地版市场包 1. 瀑布流卡片动态化,为多元化场景赋能 2. 高效率编译期 DSL,提供一种全新的 UI 开发模式

2. 与其他框架的对比

2.1 布局动态化你要理解的一些概念

目前市面上的跨端框架层出不穷,RN、Hybrid、Flutter、小程序,除了需要具备跨端的能力,在动态性、多端一致性、性能、包大小等方面要求也越来越高。每种框架都有自己独特的优势和不足,不同公司、不同业务、不同时期在这些框架的选择上都存在不同。

在评价跨端框架上,我们一般会从几个方面来看:

  • 研发效率
  • 动态化
  • 多端一致性
  • 性能体验
  • 生态

那么为什么会衍生出布局动态化这种框架呢?以 58同城 APP 为例,产品需要快速验证 UI 效果,运营需要经常上线各种活动。

主流的跨端框架大多过重,无论是 H5 还是 RN、Flutter 类的方案,它们动态化能力的关键在于随时可发布代码,是面向开发的动态化方案,发布代码意味着开发、测试、灰度等一系列流程都要完整跑通,显然难以满足产品和运营的诉求。

同时在现有工程中使用,对于包大小、接入成本、性能等有着严格地限制,RN 当前的版本依托于 Bridge,无法用于首页 (最新版使用 JSI、Hermes,但内存占用是问题),Flutter 与混合工程对接、动态化、包大小、运行内存一直被诟病,H5 性能更差,冗长的栅格化绘制流程慢的让人抓狂。

当然每种框架都有特定适用的场景,一般大型 APP 都是多套框架并存。

当然中间我们也做过一些尝试,比如基于 Android App Bundle + Qigsaw 的任意门、Mocha,实现了基于版本级别的线上 AB 测能力:

业务线接入情况:无线、招聘、部落、二手车
使用情况:任意门进行 bug 修复 3次,AB 测 6次
动态包覆盖速度:动态包安装量 7天 能覆盖活跃用户的 90% 以上

任意门、Mocha 性能与原生无异,具备彻底地动态化能力 (dex、so、资源),但其存在一些限制,特别是在纯布局动态化场景下:

  • 只支持 Android
  • 基于版本级别,PM 如果想跨版本连续验证效果,则需要打包、上线多次

2.2 布局动态化框架需要具备哪些能力?

  • DSL 布局描述文件: 一般为 json(简单易用)、xml(直观)、ProtoBuf(性能、大小) 格式。无论是 H5 还是 RN,动态化都需要有描述页面结构的语言。H5 和 RN 为了追求开发模式的完备,各自定义了一套通用编程语言 (GPL),比如 RN 的 JSX.

  • 可视化编辑前端/工具: 用于编辑产出 DSL

  • 管理后台: 用于产品管理、DSL 管理,可快速编辑、发布上线

  • 端侧引擎: 基础的组件、事件,下载、解析、运行 DSL

  • 可扩展能力: 组件、事件等业务可定制扩展

2.3 布局动态化框架间的对比

Tangram、VirtualView

Tangram、VirtualView 是业内比较知名的布局动态化框架,同时其具备其他框架没有的组件动态更新能力,所以这边和它对比一下。

优点:

  • 跨端支持:支持
  • 性能: 性能优,底层 C 实现,同时具备扁平化能力
  • 组件与动态能力:内置布局、组件丰富,开放的 API,同时 VirtualView 具备动态更新组件的能力

缺点:

  • 更新维护:最新更新在 2020/12
  • DSL 前端、管理后台未开源

珊瑚海

除了不支持组件的动态更新之外,在跨端支持、性能、内置的布局组件等方面和 Tangram 相差不大。同时属于内部团队持续维护,具备全套的开发组件 (可拖拽低代码前端、管理后台)

分别查看 Native 原生、Yoga、Tangram 在同一列表样式中的 fps 数据:

丢帧数和丢帧率结果为:

Tangram(3.87%) > Native(2.30%) > Yoga(1.43%)

再对比一下 Native 原生、Yoga、Tangram 在同一列表样式中过度绘制数据:

可以看到 Yoga、Tangram 因为都做了扁平化处理,所以在过度绘制方面表现不错。

3.客户端引擎

介绍客户端引擎设计时,我们先来看下珊瑚海整体架构:

3.1 客户端引擎架构图

3.2 DSL 设计

  • 基础组件:Text,Image, View,List,ViewPager,Span
  • ⾃定义组件:轮播头图,titleBar
  • 基础 Action:setPropsById,condition, JSEval …
  • ⾃定义 Action:invokeApi, jump, sendLog,getLocalData …

示例:

{
    
    
  "node": {
    
    
    "name": "View",
    "id": "700-003",
    "layout": {
    
    
      "height": "100%",
      "width": "100%",
      "flexDirection": "column",
      "alignItems": "center",
      "justifyContent": "center"
    },
    "style": {
    
    
      "backgroundColor": "#ffffff"
    },
    "child": [
      {
    
    
        "name": "View",
        "id": "700-0017",
        "layout": {
    
    
          "width": "100%",
          "height": "50",
          "flexDirection": "column",
          "alignItems": "center",
          "justifyContent": "center"
        },
        "style": {
    
    },
        "child": [
          {
    
    
            "name": "View",
            "id": "700-0031",
            "layout": {
    
    
              "width": "100%",
              "flexDirection": "row",
              "alignItems": "center",
              "justifyContent": "center"
            },
            "child": [
              {
    
    
                "name": "Image",
                "id": "700-0039",
                "layout": {
    
    
                  "width": "28",
                  "height": "28",
                  "marginLeft": "8"
                },
                "props": {
    
    
                  "url": "https://pic3.58cdn.com.cn/nowater/fangfe/n_v2a5955cca5b41421a87cbc06261fb7290.webp"
                },
                "event": [
                  {
    
    
                    "name": "onClick",
                    "action": [
                      {
    
    
                        "type": "Action",
                        "value": "goBack",
                        "params": [
                          {
    
    
                            "type": "String",
                            "value": {
    
    }
                          }
                        ]
                      }
                    ]
                  }
                ]
              }
    ]
  },
  "api": [
    {
    
    
      "name": "communityInfo",
      "method": "GET",
      "url": "https://api.anjuke.com/community/info"
    }
  ],
  "lifeCycle": {
    
    
    "onCreate": []
  }
}

3.3 DSL 引擎

布局

基于 Flexbox 语法(弹性布局,跨端),Yoga(RN 使用的渲染引擎)渲染,高性能,底层 C++;扁平化

数据绑定:

⾃定义 Action

注册到 ActionManager,继承 BaseAction,注册/静态类⽅法

逻辑动态

集成轻量级 QuickJS 引擎,Action: JSEval(script, params…)

3.4 全新的移动端 UI 开发模式

传统的移动端开发 UI 一般分为以下几步:

从流程中可以看出,传统的开发模式存在以下几个问题:

  • Android & iOS 需要对一份 UI 设计在对应平台分别进行开发
  • 单平台开发时,也存在重复输入,每一个 UI 都需要对应编写视图描述文件、数据实体、数据解析、适配器绑定 UI 数据等逻辑

使用布局动态化开发 UI 的方案优点:

  • 跨端支持,支持 Android & iOS,同一份 UI 只需要编写一份 DSL 文件即可在两端运行
  • DSL 文件支持数据绑定能力,无需重复编写视图查找、数据实体、数据解析、绑定的代码
  • 如果不考虑动态更新问题,也可以将 DSL 文件内置在移动端本地来解决本文章提到的问题

但在动态布局方案中,强调的更多是布局的动态化,其加载、解析 DSL 存在部分耗时:

以 500行的 DSL 文件为例,在中高端手机运行时加载 + 解析文件耗时达到 70ms(其实这个过程和原生相差不大,原生的优势在于系统 AOT 的加持)

如果不考虑动态化,只是作为内置型应用场景,如何优化此部分性能呢?

编译期提前将 DSL 文件转换成原生代码,省去加载、解析的时间,同时又可以更多地享受系统 AOT 的加持,缺点就是 dex 变大, 加长 classloader 加载时间

这边转换的是只是 DSL 对应的 JSON 对象的代码,而没有转换 JSON 对应的每个视图的代码,主要是因为更灵活,无需重复编写 DSL 运行时解析视图的代码,且 DSL 解析成 JSON 对象后,对应的构建 View 等操作和原生差异不大。

实验数据,同样的 DSL 文件,JIT 模式优化到 8ms, AOT 模式优化到 1ms.生成的代码示例:

class RevertToJsonTest {

    /**
     * output:
     * {"node":{"layout":{"alignItems":"center","flexDirection":"column","width":"100%","justifyContent":"center","height":"100%"},"name":"View","style":{"backgroundColor":"#ffffff"},"id":"700-003"},"api":[{"method":"GET","name":"communityInfo","url":"https://api.anjuke.com/community/info"}],"lifeCycle":{"onCreate":[]}}
     *
     * Test on mobile: load and parse json 80ms, revert map to json: JIT 6ms, AOT 1ms, save 92% - 98%
     */
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

        Map<String, Object> resultMap = new HashMap<>();
        Map<String, Object> node17645087271206Map = new HashMap<>();
        resultMap.put("node", node17645087271206Map);
        Map<String, Object> layout17645087328278Map = new HashMap<>();
        node17645087271206Map.put("layout", layout17645087328278Map);
        layout17645087328278Map.put("alignItems", "center");
        ....
        _17645091636381Map.put("style", style17645091785331Map);
        style17645091785331Map.put("color", "#0B0F12");
        ....
        _17645091636381Map.put("id", "700-00244");
        _17645093309193Map.put("layout", layout17645093317330Map);
        layout17645093317330Map.put("marginRight", "16");
        ....
        _17645093309193Map.put("name", "Text");
        Map<String, Object> style17645093400202Map = new HashMap<>();
        _17645093309193Map.put("style", style17645093400202Map);
        style17645093400202Map.put("color", "#0B0F12");
        style17645093400202Map.put("fontSize", "16");
        _17645093309193Map.put("id", "700-00389");
        Map<String, Object> props17645093441338Map = new HashMap<>();
        _17645093309193Map.put("props", props17645093441338Map);
        props17645093441338Map.put("text", "区别于组件,模版指的是可复用的DSL代码段,是现有DSL组件能力的组合和复用");
        Map<String, Object> _17645093462008Map = new HashMap<>();
        child17645090142087Array[14] = _17645093462008Map;
        Map<String, Object> layout17645093470302Map = new HashMap<>();
        _17645093462008Map.put("layout", layout17645093470302Map);
        layout17645093470302Map.put("marginRight", "16");
        ....
        _17645093462008Map.put("name", "Text");
        Map<String, Object> style17645093573580Map = new HashMap<>();
        _17645093462008Map.put("style", style17645093573580Map);
        style17645093573580Map.put("color", "#0B0F12");
        ....
        _17645093462008Map.put("id", "700-00408");
        Map<String, Object> props17645093625570Map = new HashMap<>();
        _17645093462008Map.put("props", props17645093625570Map);
        props17645093625570Map.put("text", "珊瑚海Bundle DSL描述文件");
        Map<String, Object> _17645093647561Map = new HashMap<>();
        child17645090142087Array[15] = _17645093647561Map;
        Map<String, Object> layout17645093656085Map = new HashMap<>();
        _17645093647561Map.put("layout", layout17645093656085Map);
        ....
        _17645093647561Map.put("name", "Text");
        Map<String, Object> style17645093736060Map = new HashMap<>();
        _17645093647561Map.put("style", style17645093736060Map);
        style17645093736060Map.put("color", "#0B0F12");
        style17645093736060Map.put("fontSize", "16");
        _17645093647561Map.put("id", "700-00430");
        Map<String, Object> props17645093779067Map = new HashMap<>();
        _17645093647561Map.put("props", props17645093779067Map);
        props17645093779067Map.put("text", "指的是包含珊瑚海Bundle DSL描述、版本、依赖视图组件版本等信息的JSON文件,需要随珊瑚海Bundle DSL一起上传平台");
        Map<String, Object> _17645093807173Map = new HashMap<>();
        child17645090142087Array[16] = _17645093807173Map;
        Map<String, Object> layout17645093816175Map = new HashMap<>();
        _17645093807173Map.put("layout", layout17645093816175Map);
        layout17645093816175Map.put("marginRight", "16");
        ....
        _17645093807173Map.put("name", "Text");
        Map<String, Object> style17645093952708Map = new HashMap<>();
        _17645093807173Map.put("style", style17645093952708Map);
        style17645093952708Map.put("color", "#0B0F12");
        style17645093952708Map.put("fontSize", "16");
        style17645093952708Map.put("fontWeight", "bold");
        _17645093807173Map.put("id", "700-00449");
        Map<String, Object> props17645094058650Map = new HashMap<>();
        _17645093807173Map.put("props", props17645094058650Map);
        props17645094058650Map.put("text", "珊瑚海Bundle DSL版本映射表");
        
        resultMap.put("lifeCycle", lifeCycle17645094899247Map);
        Object[] onCreate17645094910216Array = new Object[0];
        lifeCycle17645094899247Map.put("onCreate", onCreate17645094910216Array);

        // new JSONObject by map
        JSONObject jsonObject = new JSONObject(resultMap);

        System.out.println("RevertToJsonTest revert cost time: " + (System.currentTimeMillis() - startTime));
        System.out.println("map revert to json: \r\n: " + jsonObject.toJSONString());
    }
}

4. 管理后台

4.1 一站式全链路闭环

平台管理 -> 低代码前端 -> 各端引擎,一站式开发部署上线

多平台、多端

打通低代码前端

快速上线发布

4.2 业务服务可配

各业务可接入管理平台 API,部署自己的 DSL 服务,保证高流量、高并发请求。

4.3 一份 DSL,多端复用

基于 DSL 文件级别的复用,项目管理分开,做到复用的核心为多端引擎的对齐,引擎的向下兼容性。

优点:

  • 移动端、小程序、h5 版本节奏不一致,分开管理灵活度高。出现问题方便回滚,不会牵一发动全身。
  • 平台版本、引擎版本、DSL 版本逻辑复杂,揉合在一起系统会非常复杂难以管理。这样设计同时可减少后端复杂度,出现问题易排查。
  • 后台预留了直接上传的入口,开发工作量可节省。

缺点:增加 DSL 管理、上线的工作量。

4.4 插拔式组件库

插拔式组件库,严苛地版本管理

业务可以定制自己的组件库,并进行版本迭代,在编辑 DSL 文件时,会根据版本规则匹配可用的组件库:

低代码前端动态加载对应组件库:

4.5 共建共享型模版

类比应用市场主题,大家可以分享自己的 DSL 模版,一起快速构建美观的视图

5. 低代码前端

5.1 架构设计

其中低代码平台负责:

  • 加载组件库
  • 拖拽、鼠标事件
  • canvas 绘制(布局、位置、margin、padding)
  • 代码编辑
  • 操作历史记录
  • 模版管理
  • 生成 dsl

组件库负责:

  • 拥有哪些组件,每个组件的属性、绘制,和端侧引擎同步迭代

5.2 功能拆解

6.打造更易用的组件库

接入方可以按照规范开发自己的组件库,目前我们厂商业务正在开发一款更通用、更易用的组件库,完成后将共享给更多业务使用:

组件说明:

# 组件名称 描述
1 View 容器
2 Image 图片
3 Link 链接
4 Text 文本
5 TextEditor 文本输入框
6 Span 图文混排
7 RadioButton 单选按钮
8 RadioGroup 单选按钮组
9 CheckBox 复选按钮
10 Switch 状态切换按钮
11 List 列表,需要配合 ListGridItem 使用,支持多 item 样式
12 LoopList 循环列表,需要配合 ListGridItem 使用,适合固定 item 样式
13 GridView 网格视图,需要配合 ListGridItem 使用,支持多 item 样式
14 ListGridItem List/Grid item 视图

7.生态规划

  1. 完成 H5、小程序端引擎
  2. 推广全新的列表/卡片 UI 开发模式
  3. 低代码平台支持开源项目 Fair 的 DSL 编辑,打造大前端生态

猜你喜欢

转载自blog.csdn.net/u014294681/article/details/122342921