绝对领域:浅谈粗粒度动态化方案Tangram

写在前面

大家好我是三雒,今天咱们不装杯了,来浅聊一下Tangram。或许你之前从没了解过Tangram,但是你在开发过程可能遇到过以下问题:

  • RecyclerView自带的LayoutManager太鸡肋了,实现稍微杂一点的信息流页面需要写很多管理ViewHolder类型和占用span数量的代码
  • 代码太冗余了,在双列瀑布流头部添加一个四列的卡片,需要ViewHolder内部再套一个RecyclerView,再为里面的RecylerView实现Adapter和ViewHolder, 开发效率极低
  • 滑动页面滑动到某个位置Sticky略微有点难度,需要搞懂嵌套滑动机制、CoordinatorLayout这些玩意儿;实现悬浮可拖动按钮,Banner, 多列,一拖N布局,好像都不太难,但是我只想写好我的业务代码,哪有时间去写这些花里胡哨的东西
  • PM天天想在线上调整页面结构,线上修改布局样式,可是你无奈的告诉她 "做不到"

现在我告诉你这些问题Tangram通通都能解决,并且它都在一个RecyclerView内部就能完成,去它丫的StaggedGridLayoutManager,去它丫的CoordinatorLayout和嵌套滑动,去它丫的自定义View,再也不用为拒绝可爱的PM而烦恼了。

Tangram是什么

Tangram,七巧板,几块简单的积木就能拼出大千世界,方案如其名,也是希望能像七巧板一样可以通过一些基本的组件就搭出丰富多彩的界面 。Tangram从手机天猫首页方案抽象而来,是面向组件 的界面方案,是不断权衡灵活性动态性、性能开发效率、稳定性 等多方面表现的结果。Tangram又不仅仅是一个界面开发框架,而是针对电商首页、搜索结果页等综合信息流页面一套界面解决方案,涵盖了Native SDK,GUI操作台,后端逻辑容器,组件库机制的一整套方案。

我更愿意称Tangram是针对电商首页、搜索结果页等综合信息流页面 的一套粗粒度动态化方案,Tangram的核心其实是灵活的布局能力加上一定的动态能力,而动态性其实本身就要求其灵活性,粗粒度则表示其动态能力没有那么强。

Tangram页面模型

在进一步讲解之前,先来讲一下Tangram的页面模型,对齐基本的概念,方便下文的理解。

  • 页面指的就是整体可滑动页面实体,比如下图整个就是一个页面。
  • 卡片指的是页面内可按行划分的一个一个独立区块,比如开头的单列、整个Banner区域、瀑布流区域等
  • 组件指的是卡片内部一个独立的、业务级别的单元,可以粗略认为是RecyclerView中最小粒度的ViewHolder, 比如单列中一行, Banner内的一张图等等。

因此整体整个页面可以这样描述:一个页面嵌套了多个卡片,一个卡片嵌套了多个组件。

Tangram特性

  • 灵活性

灵活的组件布局异力 支持多种类型的卡片布局,包括网格布、瀑布流、轮播、线性滚动、吸顶、固定、浮标等等,开发者不必为实现布局效果而努力,可专注于组件的业务逻辑实现。

  • 动态性

采用粗粒度的组件,具有一定的动态性,主要体现在两个方面,一是页面结构的动态性,二是组件布局样式的动态性。

  • 高性能

高性能主要体现在组件回收复用和页面渲染效率两方面,在Android 上Tangram借助RecyclerView实现,并且所有的组件都是在同一个RecyclerView中的,天然地借助了RecyclerView的回收复用机制;为了提升渲染效率,Tangram将在视图渲染之前把大量的计算工作在VV中完成,并缓存在VV组成的树形结构里,引入VirvalView的概念,减少View的层级。

个人认为并不能有效提升渲染性能,原因在于Android View的渲染流程分为measure,layout和draw三大步,主要耗时在measure和layout, draw在主线程的耗时很少,主要是由RenderThread线程来做。而VV只是减少绘制的时候的层级,并不能减少measure和layout的耗时。

  • 双端一致性

Tangram制订定了两个端开发原则去严格保证双端一致:任意新功能的提出都是不区分平台,在功能设计中必须同时考虑多端功能,具体的实现方案和逻辑必须多端统一Review以保证多端表现一致;任意一端的变更都必须在改动前把方案同步给其他端,而且变更必须多端同步发布。

Tangram的这些特性描述有些理解的可能不是特别清楚,没关系,接下来核心能力部分这些特性都会有进一步的阐释。

Tangram核心能力

为了比较好理解我们能用Tangram做什么,这里通过需求迭代的形式来讲解。这部分纯属笔者自己杜撰,不代表框架原始的诞生流程。

刀耕火种

假设你是某著名电商的早期研发,我们要做一个类似页面模型部分图所示的页面。

需求1.0:

如下的页面中包含单列、双列、多列网格、一拖N、Banner、吸顶等布局卡片等,滑动到最后是双列,且可以一直滚动加载更多。

针对这个需求在不借助任何框架的情况下,比较朴素的实现可能如下:

  1. 整体使用RecylerView+GridLayoutManager来实现,GridLayoutManager的列数为2,满足最后滚动加载的双列
  2. 上面所有占满屏幕的卡片比如Banner、一拖N、多列卡片都需要使用SpanSizeLookup控制这些卡片占用span数为2
  3. 在Adapter中管理各种卡片类型,实现外部单个卡片的ViewHolder以及对应的View和业务逻辑。对于一些类型的卡片我们需要嵌套一层View作为卡片的根布局,比如多列网格、一拖N、Banner等。以多列网格为例,会使用RecyclerView+GridLayoutManager来实现,然后就需要再为卡片内部的每一个组件实现对应的ViewHolder。

看上去整体的思路并不复杂,但你能感受到两个明显的弊病:

  • 开发效率低,更希望的是开发者能够专注于卡片内每个组件的业务逻辑实现,而不必在卡片本身的布局实现、类型管理、占用span数量等投入过多精力。

  • 性能差,主要体现在滑动和复用两个方面。RecyclerView会在滑动过程中进行创建ViewHolder, 绑定数据并进行渲染,当滑动到图中的双列时候整个卡片的高度会根据数据确定并一次性measure和layout所有双列卡片的所有组件,会让这一帧很卡;另外在一些情况下不同的卡片可能会使用相同的组件,这时候是希望这些组件能够进程复用的,但是由于跨RecyclerView,本身是不复用的。

虽然你也知道存在这些问题,项目还在验证的初期,主打一个能用就行。不过好景不长,随着线上用户的增多,PM对于产品的形态不断探索,提出各种变化卡片形态的需求,代码复用性差、开发效率低的问题突显出来。

效率性能提升(vlayout)

需求2.0:

把最上面单列展示的卡片转改成一拖N的卡片,把StaggerCard部分改成一个三列,另外还一直会有新增的业务卡片。

这时候你发现,每种类型的业务和对应的卡片形态是1:N 的关系,最外层为每种业务实现不同类型的卡片样式的ViewHolder已经呈爆炸式地膨胀,上述代码复用差、开发效率低的问题已经忍无可忍。痛定思痛,你决定对开发一套框架解决这个问题,作为一个名追求极致的开发者,你当然也要解决上述的性能问题。通过一段时间对RecyclerView的深入研究,你发现自定义LayoutManager似乎可以完美解决这个问题。

  • 自定义了一个VirtualLayoutManager,它继承自 LinearLayoutManager;引入了 LayoutHelper 的概念,它负责具体的布局逻辑;VirtualLayoutManager管理了一系列LayoutHelper,将具体的布局能力交给LayoutHelper来完成,每一种LayoutHelper提供一种布局方式,框架内置提供了几种常用的布局类型,包括:网格布局、线性布局、瀑布流布局、悬浮布局、吸边布局等。这样实现了混合布局的能力,并且支持扩展外部,注册新的LayoutHelper,实现特殊的布局方式。

  • 提供了自定义的布局样式,可以满足多样化的布局需求,比如每一个组件范围内的布局支持一个背景颜色、背景图片;网格布局里,可以支持1列、2列、3列、4列、5列共5种样式,每一列的宽度默认平均分配屏幕宽度,也可以指定按比例分配列宽。吸边布局支持吸到屏幕底部、屏幕顶部、屏幕左边、屏幕右边。这些都是系统默认的LayoutManager不支持的。

  • 每一种LayoutHelper负责布局一批组件范围内的组件,不同组件范围内的组件之间,如果类型相同,可以在滑动过程中回收复用。因此回收粒度比较细,且可以跨布局类型复用。

使用示例

java 复制代码
public class MyAdapter extends VirtualLayoutAdapter {
    ...
}

MyAdapter myAdapter = new MyAdapter(layoutManager);

//构造 layoutHelper 列表
List<LayoutHelper> helpers = new LinkedList<>();
GridLayoutHelper gridLayoutHelper = new GridLayoutHelper(4);
gridLayoutHelper.setItemCount(10);
helpers.add(gridLayoutHelper);

GridLayoutHelper gridLayoutHelper2 = new GridLayoutHelper(2);
gridLayoutHelper2.setItemCount(10);
helpers.add(gridLayoutHelper2);

//将 layoutHelper 列表传递给 adapter
myAdapter.setLayoutHelpers(helpers);

//将 adapter 设置给 recyclerView
recycler.setAdapter(myAdapter);

在上述的代码通过给Adapter设置LayoutHelper的集合, 每一个LayoutHelper负责对应ItemCount的数据的布局,这样我们只用通过调整LayoutHelper的种类即可以实现一种卡片形态的变化,大大提升了开发效率和代码复用性。MyAdapter主要是就只负责维护页面数据以及每种组件对应的ViewHoldr的逻辑。

页面布局流程

  1. RecyclerView是整个页面的主体,它的运行需要绑定一个Adapter和LayoutManager,在我们的设计里自定义了VirtualLayoutAdapter和VirtualLayoutManager来绑定到RecyclerView。

  2. VirtualLayoutAdapter继承自系统的Adaper,它在原生地Adapter上扩展了两个接口:setLayoutHelpers()------业务方调用此方法设置整个页面所需要的一系列LayoutHelper; getLayoutHelpers()------与setLayoutHelpers()对应;这两个方法的具体实现都委托给VirtualLayoutManager来完成。

  3. VirtualLayoutManager继承自系统的 LinearLayoutManager,在RecyclerView加载组件或者滑动的时候,会调用VirtualLayoutManager的layoutChunck方法,告诉它当前还有哪些空白区域可以用来摆放组件,

  4. VirtualLayoutManager会持有一个LayoutHelperFinder,当layoutChunck被调用的时候,会传入一个位置参数,告诉LayoutManager当前要布局第几个组件,LayoutHelperFinder就通过这个位置找到当前这个位置对应的LayoutHelper,因为每个LayoutHelper都会绑定它负责的布局区域的起始位置和结束位置。

  5. LayoutHelper负责具体的布局逻辑,它有一系列子模块,其中基类LayoutHelper定义了一系列接口,用来和VirtualLayoutManager通信,包括isOutOfRange()------告诉VirtualLayoutManager它所传递过来位置是否在当前LayoutHelper的布局区域内;setRange()------设置当前LayoutHelper负责的布局区域;beforeLayout()------在真正布局之前做一些前置工作;doLayout()------真正的布局逻辑接口;afterLayout()------在布局完成之后做一些后置工作;MarginLayoutHelper稍微扩展LayoutHelper,提供了布局常用的内边距padding、外边距margin的计算功能;BaseLayoutHelper是第一层具体实现,实现了当前LayoutHelper在屏幕范围内的具体区域,用于填充对这一区域填充背景色、背景图等逻辑。而剩下的LinearLayoutHelper、GridLayoutHelper等负责了具体的布局逻辑,它们都重点实现了beforeLayout()、doLayout()、afterLayout()方法,特别是在doLayout()方法里,会获取一个一组件,按照各自的协议对组件进行尺寸计算、界面布局。框架内置了以下几种重要的 LayoutHelper:

    • LinearLayoutHelper,实现简单的线性布局;

    • GridLayoutHelper,实现网格布局,支持1-5列的网格,支持配置列间距、行间距,支持不等宽的网格;

    • StaggeredLayoutHelper,实现瀑布流式的布局;

    • FloatLayoutHelper,负责悬浮效果,处于该布局中的组件会悬浮在整个页面上方,并且可拖拽,不随页面滚动而滚动;

    • FixedLayoutHelper,负责固定位置的布局,它可固定在屏幕某个位置,不可拖拽,不随页面滚动而滚动;

    • StickyLayoutHelper,它是一种吸边的布局,当它包含的组件处于屏幕可见范围内的时候,像正常的组件一样随页面滚动而滚动,当组件将要被滑出屏幕返回的时候,可以吸到屏幕的顶部或者底部,实现一种吸住的效果;

经过如上的改进之后,面对PM各种业务卡片形态调整都显得异常easy, 开发效率大大提升,同时页面的性能也有所优化。随着用户规模变的庞大,线上的运营活动变多,活动的及时性要求能在线上调整卡片的形态以及内部组件的一些简单属性。

页面结构动态化(Tangram)

需求3.0:

要求能在线上对任意业务卡片的形态或者样式等做出调整,卡片的形态主要就是vlayout支持的种类,样式也基本满足。

在传统的开发中server端api只负责返回业务数据信息,页面的样式由客户端自己决定,但目前要在线上调整卡片的形态和样式的话,很明显我们必须也把这些数据由server来控制。已经有了vlayout作为基石,把卡片的信息动态化并非难事。于是你按照页面层级概念抽象出来了Card和Cell两个Model类,Card代表卡片布局信息,对应于vlayout中的LayoutHelper;Cell则代表具体的业务组件,对应于ViewHolder;一个Card可以有多个Cell,在页面上最小的粒度的业务单元即为Cell。

数据设计

如下是一个简易的数据示列,整个是一个Card的Json数组,一个Card包含卡片类型type,卡片样式 style, 卡片数据items等基本信息,这个我们也可以自行扩展; items中包含的具体的组件,每个组件也包含type, style 等通用信息,我们也可根据业务本身自定义,每种组件type对应一个ViewHolder,这个对应关系需要我们提前注册给框架。

json 复制代码
[ {
  "type": "container-twoColumn",
  "style": {
    "hGap": 10, // 卡片水平间距
    "vGap": 10, // 卡片垂直间距
    "cols": [
      35.5
    ]
  },
  "items": [
    {
      "type": 1
    },
    ...
  ]
},
{
  "type": "container-onePlusN",
  "style": {
    "aspectRatio": "1.778",
    "cols": [
      43.467
    ],
    "rows": [
      43.602
    ]
  },
  "items": [
    {
      "type": "10",
      "action": "xxx",
      "imgUrl": "https://gw.alicdn.com/tfs/TB1pdJFQpXXXXbUXpXXXXXXXXXX-750-243.png",
      "style": {
        "margin": "[0,1,0,0]"
      }
    },
    ...
  ]
}
}]

如上的数据表示的一张双列的卡片和一张一拖N的卡片。

渲染页面流程

  1. 不论是传递原始 JSON 数据给 TangramEngine还是通过直接解析原始数据,都是通过 DataParser 来完成的,它会按照树型结构解析出对应的卡片和组件的 Model 对象,解析过程依赖于相应的卡片 Resolver 和组件 Resolver 来识别卡片、组件是否已注册,关键点就是识别 type 字段。若碰到无法识别的 type,则不会解析出对应的 model 对象。

  2. 解析完成之后会得到一个卡片列表,每个列表的卡片 Model 元素里持有它所包含的组件列表。

  3. Model 列表交给 GroupBasicAdapter 进行处理,首先提取卡片列表,将包含空组件列表的卡片过滤掉,因为它没有东西可以渲染展示,然后创建出 vlayout 所需要的 LayoutHelper 列表,设置它们的样式属性,这样就打通了通过 JSON 数据最终控制布局排版的流程。

  4. 将所有的组件 Model 提取出来成为一个独立的列表,真正交给 GroupBasicAdapter 去渲染数据,组件 Model 列表的大小就是 GroupBasicAdapter 的 item 的大小, RecyclerView 也就直接加载组件视图,卡片相对于只负责了布局逻辑的控制,并没有 UI 实体的承载。

  5. 数据都准备完毕之后,RecyclerView 就驱动 vlayout 里的 VirtualLayoutManager 进行渲染和布局。

  6. VirtualLayoutManager 首先回调 RecyclerView 内部获取 ViewHolder,若复用池里存在复用的对象,就回调 GroupBasicAdapter 进行数据绑定,否则先回调 GroupBasicAdapter 进行组件 ViewHolder 的创建,然后进行数据绑定。ViewHolder 的创建也是通过 Resolver 内部创建 UI 的模块进行构造。

你开发了Tangram框架之后完美解决了线上频繁变更卡片形态的需求,但是一旦踏上动态化的道路就会越走越远,很快PM就提出想要在线上修改组件布局样式的需求。

组件布局动态化 (VirtualView)

需求4.0:在线上任意修改组件布局样式,也可以动态新增新的组件。

业务组件是采用常规的 Native 代码开发的,除非内置了足够多的逻辑,否则组件的样式调整或者新组件的开发都要发布版本。你很快想到是不是可以使用RN或者Weex 开发一个通用的容器组件,这样就可以动态变更了,这样确实可以,但你经过尝试后发现在性能上和原生比还是有较大差距的。由于业务对性能的要求,所以你打算自己设计一套性能接近原生的轻量级方案。

你开始思考组件是怎么构成的,对MVVM异常熟悉的你很快将组件拆分为视图+数据+业务逻辑(数据/事件绑定),在电商的场景下组件业务逻辑一般都不会特别复杂,基本就是数据、事件绑定这些逻辑。数据本身就是动态化的,视图的话Android一般用xml描述,这块动态化其实比较容易的,但关键在于是业务逻辑这块如何动态化,但是你经过简单地抽象和简化之后,这块其实就变成了数据和事件的绑定,只要支持一些通用的动态绑定表达式,在端上实现数据绑定的逻辑就可以解决这个问题了。

这样的话虽然业务逻辑动态化能力没那么强,但也基本满足了日常需求,渲染性能也基本和原生一致。组件的回收复用,依然还是可以借助RecylerView,;为了进一步提升性能你也设计了一些虚拟View去尽量减少视图层级让页面结构扁平化;你也畅想将来能借助虚拟View实现一套异步渲染流程,突破Androird的主线程measure和layout限制,进一步提升性能。

使用流程

这套新的组件方案命名为 VirtualView,简称 VV,基本的使用流程如下:

  1. 先编写业务组件的模板。

  2. 通过工具将模板数据编译成二进制数据。

  3. 客户端加载二进制数据可以有两种路径,一是直接打包到客户端里,另一种是发布到模板管理后台,客户端在线更新到模板数据。

  4. 不论哪种方式加载二进制数据,客户端接下来的工作是解析二进制数据里,比如校验版本号,合法性,读取头信息等等。

  5. 等要真正创建组件的时候,根据组件名称找到二进制数据,从中解析并创建出真正的组件模型数据。

  6. 从模板里创建在组件往往不含有业务数据,因为业务数据是动态性的,用户需要获取到业务数据绑定到组件上,组件的属性里可以写表达式来指定使用哪一个数据字段。

如下是一个使用示例,它表示了一个横向线性布局内部含有一个图和一个文本,除了固定的宽高,动态数据通过表达式从 server下发的JSON中读取。

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<VHLayout
        flag="flag_exposure|flag_clickable"
        orientation="H"
        action="${action}"
        layoutWidth="match_parent"
        layoutHeight="wrap_content">
    <NImage
            id="1"
            src="${logoUrl}"
            layoutMarginLeft="8"
            layoutMarginRight="8"
            layoutMarginTop="8"
            layoutMarginBottom="8"
            layoutWidth="32"
            layoutHeight="32"/>
    <NText
            id="2"
            text="${title}"
            layoutGravity="v_center"
            gravity="${style.text-align}"
            textSize="${style.font-size}"
            textColor="${style.color}"
            layoutWidth="match_parent"
            layoutHeight="wrap_content"/>
</VHLayout>

这个模板里的组件和Android原生的xml并不太一致,主要是为了保持双端的一致性重新设计了一套,同时也简化了一些处理并支持了动态绑定表达式。该模板对应的数据如下,大致可以看出数据和表达式的对应关系。

json 复制代码
{
  "style": {
    "text-align": "h_center",
    "font-size": "20",
    "color": "#FF5000"
  },
  "title": "超高性 99.9% 的用户觉得很快",
  "logoUrl": "https://gw.alicdn.com/tfs/TB1yGIdkb_I8KJjy1XaXXbsxpXa-72-72.png",
  "action":""
}

上述的模版XML会编译成一个二进制格式,可以通过TangramEngine的registerVirtualViewTemplate(String type, byte[] data) 方法去注册type对应的模版数据,这样Tangram在解析JSON数据时候就可以知道这个type对应的VirtualView模版。

接下来主要针对VV的核心设计点做一些介绍,包括数据、事件绑定,虚拟组件,二进制文件格式。

数据绑定

开发业务组件的时候,基础属性或者样式往往不能在模板里直接写死,而是需要从数据里获取,所以引入了用户数据绑定的表达式,语法和实现上目前比较简单,参考了很多同类的设计,尽可能符合开发人员的直觉。

  • 访问数据属性EL表达式

语法上以 ${ 开头,以 } 结束。对于Map,通过 . 操作符进行访问,对于 Array 或者 List 通过 [] 操作符进行访问。

json 复制代码
${benefitImgUrl}
${data[0].benefitImgUrl}
  • 条件表达式(三元操作符)

用来给那些需要根据数据中某个字段来设置值的属性,语法上以 @{ 开头,以 } 结束,中间部分为表达式的具体内容,类似于Java的三元运算符。

json 复制代码
@{${logoUrl} ? visible : invisible }

事件管理

事件类型:

VirtualView 里定义了几种常用类型事件:点击、长按、触摸 、曝光。 前三种类型的事件只会在实体 View 上触发,如果没有实体 View,那事件最终绑定到它的宿主容器上触发;曝光事件是在组件绑定数据要准备显示的时候触发。

事件声明:

默认情况下组件是不触发事件的,只有在模板里显示地声明了才具备触发能力。事件声明属性字段是 flag,其值可以通过 | 组合一次性声明多个。对应于四种事件类型,模板里的枚举定义分别是:flag_clickable,flag_longclickable,flag_touchable, flag_exposure;例如:flag="flag_exposure|flag_clickable"

事件处理:

这些事件触发之后,会通过EventManager.emitEvent(int type, EventData data)方法传递出来,我们可以通过EventManager.register(int type, IEventProcessor processor) 方法统一处理,EventManager可以通过TangramEngine.getService(VafContext::class.java).getEventManager()方法获取。举一个页面点击事件的响应示例:

kotlin 复制代码
vafContext.getEventManager.register(EventManager.TYPE_Click) {
   val action = eventData.mVB.action
   RouterManager.open(action);
   true
}

点击事件触发之后跳转的页面路由可以在控件的action字段中指定action="${action}",

基础组件(控件)

每一个基础的原子组件或者容器组件都会有通用的属性定义和接口定义,自定义的基础组件应当继承自基础定义VIewBase并扩展;通用属性主要定义margin,pading, action等信息,接口主要定义了measure, layout, draw三个流程。 基础组件包括两大类,即原生组件和虚拟组件,以文本组件为例,包含NText和VText两种。

原生组件:

即通过Android原生的View组件或者布局容器实现,它需要继承自ViewBase,不过内部都是调用对应原生View的逻辑。

虚拟组件:

虚拟组件,即View的内容直接通过宿主的canvas绘制出来,它本身并不需要一个实体的View存在,在真实的View Tree中,是看不到这个实例,只能看到其宿主的存在。它包括原子虚拟View组件比如文本、图片、线条,布局虚拟view组件比如线性布局、帧布局等。虚拟组件也遵循Android绘制View的逻辑,需要响应measure、layout、draw的过程才能显示。

不论是虚拟化组件还是原生组件,都采用上述相同的模型来定义,这样对于宿主容器来说,包装在内部的组件就不分虚拟化还是原生,一视同仁,只要将宿主容器像普通的 View 一样添加到的视图界面上,就可以在后续的渲染过程中显示出来。如果虚拟组件使用的越多,View 的个数就越少,对于系统来说层级越扁平。比如以下的xml中使用了两个虚拟组件VText, 两个原生组件NImage, 最终我们在ViewTree上是看不到VText这个控件,因为它们是直接使用FrameLayout对应的原生 Container的canvas来绘制的。

举一个如下的代码示例,其中图片用的都是原生组件,文本用的是虚拟组件:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    flag="flag_exposure|flag_clickable"
    action="action"
    layoutWidth="match_parent"
    layoutHeight="200rp"
    background="#FFFFFF">

    <NImage
        layoutWidth="48rp"
        layoutHeight="48rp"
        layoutMarginLeft="34rp"
        layoutGravity="left|v_center"
        src="${imgUrl}"
        scaleType="center_crop"
    />
    <VText
        layoutWidth="wrap_content"
        layoutHeight="wrap_content"
        layoutMarginLeft="94rp"
        text="${title}"
        layoutGravity="left|v_center"
        textSize="28rp"
        textColor="#555555"
    />
    <VText
        layoutWidth="wrap_content"
        layoutHeight="wrap_content"
        layoutMarginRight="50rp"
        layoutGravity="right|v_center"
        textSize="28rp"
        text="${subtitle}"
        textColor="${subtitleColor}"/>
    <NImage
        layoutWidth="14rp"
        layoutHeight="30rp"
        layoutMarginRight="28rp"
        layoutGravity="right|v_center"
        src="${arrowImgUrl}"
        scaleType="center_crop"
    />


</FrameLayout>

通过Layout Inspector查看Android View Tree的结构时候,发现View Tree中只有两个图片的View,没有文本的View。

二进制文件格式

原始的 XML 模板文件保存成文件的时候,就是以纯文本的形式存在,会包含很多冗余信息,比如空格、换行、还有重复出现的字符串等,文件体积比较大,以xml解析器去解析的时候,也会需要大量字符串操作,效率和性能不能达到最优。而将它编译成二进制格式,会避免这些问题,比如文件重复出现的字符串只保留一份,通过字符串索引去引用它,所有的组件类型也都会被转换成一个数字索引,在客户端内通过数字索引反过来找到对应的类实例化。这样文件格式会非常紧凑,体积更小 ,并且解析的效率也会更高。整个设计也借鉴了 Android 系统编译模板文件的思路。它的具体格式说明如下:

Tangram架构

  • 容器层:最底层是页面层级的容器是RecyclerView,这也是整个框架的基石。从功能上看一个支持滚动的容器,并且支持布局方式的行定义,从性能上看自带ViewHolder的回收复用机制。
  • 卡片/组件实现层:这一层主要提供卡片或者组件的实现支持,vlayout中VirtualLayoutManager及LayoutHelper提供了卡片布局支持,VirtualView则提供了动态组件的支持,当然我们也可自定义ViewHolder实现原生的卡片。
  • 卡片/组件Model层: 主要将卡片和组件能力动态化,Card包含了一个布局类型和样式的信息,和LayoutHelper对应;Cell 包含一个组件的信息,PojoGroupAdapter负责页面数据信息管理、ViewHolder创建与数据绑定。
  • 数据解析层: DataParser 负责解析数据,它将原始数据解析成Card、Cell等。; Resolver 负责识别卡片、组件并构建对象,解析器解析数据的时候需要依赖这些 Resolver去识别数据中的卡片或者组件是否合法。
  • Engine: TangramEngine是核心类,它负责给 vlayout 绑定RecyclerView、绑定页面数据、操作页面数据,创建卡片、组件等。Tangram采用服务发现机制提供服务,TangramEnginge本身实现了ServieManager,不论是内部还是外部功能模块,都可以注册到这里。一方面能被 Tangram 内部的其他模块访问使用,另一方面解耦了框架与业务模块。
  • 用户服务:Tangram对外提一些服务,BusSupport用于通信,类似EventBus; ClickSupport 用于给用户统一处理点击时间;ExposureSupport 用于组件的曝光统计;CellSupport用户监控组件的一些生命周期等。它们都被注册到 ServiceManager 里,业务方在组件或者页面内都可以使用他们。

总结

本篇的介绍就到这里,回顾一下本文的内容:

  • Tangram是一个粗粒度动态化框架
  • Tangram是构建在一个可滑动的页面模型上,层级时候页面--卡片--组件,在Android是的架构大致对应到RecyclerView--LayoutHelper--ViewHolder
  • Tangram的灵活性体现在支持多列、瀑布流、一拖N、Sticky等各种布局方式;动态性体现在页面结构和组件布局是支持动态调整的;性能体现在组件级别的回收复用机制和引入虚拟组件提升页面渲染效率。
  • Tangram支持丰富的布局格式,能够有效提升开发效率;Tangram支持页面结构和组件样式的动态调整;Tangram的性能和原生开发性能几乎一致。

Tangram项目官方早已不维护,框架在使用层面存在部分bug或者API不够丰富等问题,不过这些问题对于普通开发者来说难度并不算大,可以自行修复或扩展。

参考文档

Tangram官网

VirtualView基本原理

猫客页面内组件的动态化方案-Tangram

相关推荐
数据猎手小k2 小时前
AndroidLab:一个系统化的Android代理框架,包含操作环境和可复现的基准测试,支持大型语言模型和多模态模型。
android·人工智能·机器学习·语言模型
你的小103 小时前
JavaWeb项目-----博客系统
android
风和先行3 小时前
adb 命令查看设备存储占用情况
android·adb
AaVictory.4 小时前
Android 开发 Java中 list实现 按照时间格式 yyyy-MM-dd HH:mm 顺序
android·java·list
似霰5 小时前
安卓智能指针sp、wp、RefBase浅析
android·c++·binder
大风起兮云飞扬丶5 小时前
Android——网络请求
android
干一行,爱一行5 小时前
android camera data -> surface 显示
android
断墨先生5 小时前
uniapp—android原生插件开发(3Android真机调试)
android·uni-app
无极程序员7 小时前
PHP常量
android·ide·android studio
萌面小侠Plus8 小时前
Android笔记(三十三):封装设备性能级别判断工具——低端机还是高端机
android·性能优化·kotlin·工具类·低端机