在以前的文章《猫客 Tangram 页面内组件的动态化方案》 里介绍了 Tangram 页面的组件动态化方案,可是有不少细节没有展开讲,鉴于内容比较多,打算建一个系列,分多篇文章介绍。本文介绍编译 XML 模板的过程。html
Android
iOS
名词解释
Virtualview 方案:简单来说,就是经过自定义 XML 模板搭建 UI 视图,并经过自研的渲染引擎渲染界面的一种方案,其中支持定义 Canvas 绘制的控件,所以成为 virtualview。 编译模板:将原始 XML 格式的模板序列化成一种二进制格式的过程。git
为什么选用二进制格式
经过 XML 编写的业务组件,若是直接加载解析,会有几个问题:一是原始文件相对较大,由于 XML 里会有冗余信息,如空格、换行、还有重复出现的字符串等,文件体积比较大;二是解析 XML 会有必定开销,相对于二进制数据直接解析,XML 解析会比较重,例如节点遍历、属性访问等都显得有些臃肿。经过提早将 XML 模板处理成二进制格式,能够将繁重的解析工做从客户端运行时中剥离出来,而经过将一些重复的资源作合并处理并创建索引,能够减小冗余信息,减小模板文件大小,一般状况下,处理成二进制格式的模板比原始模板可减小 50% - 60% 的大小。github
二进制模板的格式
尽管以前的文章已经提过二进制模板文件的格式,不过这里仍是要再次说起一下:框架
开始5个字节固定为 ALIVV;至关于咱们的文件格式的一个标记。
版本号分三个,分别为主版本号,次版本号和修订版本号,均为 2 个字节;在无重大重构更新时,前两位通常不变,第三位用于组件的业务级别变动升级;
组件区的起始位置和长度,均为 4 个字节;表示这份文件里组件区数据从第几个字节开始,它总共有多少个字节,这样解析这份数据的时候能直接将文件指针定位到特定位置来读取数据。
字符串区的起始位置和长度,均为 4 个字节;表示这份文件里字符串数据从第几个字节开始,它总共有多少个字节。
表达式区的起始位置和长度,均为 4 个字节;表示这份文件里字符串数据从第几个字节开始,它总共有多少个字节。
数据区的起始位置和长度,均为 4 个字节;表示这份文件里附加数据从第几个字节开始,它总共有多少个字节。目前这一区块是做为一种保留区,实际还未使用到。
当前文件所属页编码,2 个字节,惟一标识一个页(保留使用)
当前文件依赖页的个数为 2 个字节,后面为依赖页的 Id,依赖页个数大于 0 表示该页用到了其余页的资源或者代码,在该页加载以前须要确保依赖页必须已经加载;(保留使用)
组件区开始,前 4 个字节表示文件里业务组件个数,目前一个 XML 模板编译成一个二进制文件,故其值固定为 1。每一个业务组件前 2 个字节表示业务组件名称字符串的长度,后面为指定长度的字符串字节数据;紧接着是 2 个字节的编译后组件二进制流长度,后面为二进制代码;二进制代码的内容其实就是按照 XML 里定义的嵌套结构存储了一棵 UI 树,只不过节点开始、节点结束、每一个节点tag名、属性、属性值等都被映射成一个整型索引;在解析的时候会经过索引值到对应的资源池里找到具体的资源;
字符串区开始,前4个字节表示字符串个数,在咱们的框架里,会内置一些系统级别的字符串资源,这些字符串不用序列化到二进制文件里,而模板文件里出现的非系统字符串才会做为资源序列化到二进制文件。每一个字符串资源前 4 个字节字符串索引 Id 即它的 hashCode,后面 2 个本身为字符串的长度,再后面为对应的字符串;
逻辑表达式代码表。前 4 个字节表示逻辑表达式资源个数,每一个表达式资源前 4 个本身表示表达式的索引,它是表达式原始字符串的 hashCode,后面 2 个字节表示表达式的长度,后面为对应的表达式内容;
扩展数据段是保留为第三方扩展使用;(保留使用)
在一开始的时候,咱们将全部模板文件编译到一个二进制文件里,相似于 Android 编译资源时作的处理,这样能更大程度地节省存储空间。可是考虑到后续要对模板进行动态下发,咱们改为一个 XML 文件一份二进制文件的策略,这样当有个别模板更新的时候,只须要发布对应的模板,而不须要总体从新编译。尽管编译成一份文件也能够经过增量编译等方式来解决个别模板更新的问题,可是从管理、维护、使用等各方面考虑,仍是一对一的策略更方便一些。工具
资源的映射处理,有如下逻辑:编码
颜色:转换成4字节整型颜色值,格式 AARRGGBB;
枚举:按照预约义的整数转换,好比 gravity 的类型,orientation 的类型;
字符串:以 hashCode 值做为它的序列化后整数,并在字符串资源区创建以 hashCode 为索引的列表,在解析的时候从中获取原始的字符串值;
逻辑表达式:与字符串的处理相似;
数字:直接转换成 4 字节的整型或者浮点型,并支持带单位的类型;
其中字符串等资源,采用了一个 hashCode 来做为索引值,主要是考虑当模板在线发布时,字符串有变更的状况下,可以不影响原来的字符串资源索引;不然若是按照带有顺序约定的协议来分配资源索引,很容易在模板变动的时候同一索引值在变动先后指向的资源内容是不同的,这对稳定性和动态性会产生影响。.net
另外上面还提到保留使用的一些区段,这是前期设计时考虑加入的,虽然目前没有在用,可能未来会有使用的地方,好比页面编码能够用来归类模板的分组,页面依赖能够指定模板之间资源依赖的关系,能够用来作进一步的资源整合处理。又好比扩展数据区,能够用来存储额外的数据;设计
编译的具体流程
建立一个文件对象,编译工具开始编译模板的时候,先在建立一个输出文件的对象,指向特定路径,后续编译过程当中的数据都写到这个文件里。
写入 ALIVV、版本号数据,按照文件格式,开头 5 字节固定未 ALIVV,可先写入,紧接着 6 个字节是 3 位版本号,主版本号固定为 1,次版本号固定未 0,修订版本号每次编译的时候开发人员经过参数传入,从 1 开始。
写入各区域的占位空间,根据文件格式,接下来 32 个字节分别为组件区、字符串区、表达式区、数据区的起始位置值和长度,因此先占位,初始化为 0。还有当前文件页面编码、以及它的依赖,这也是编译时用户传入,默认页面编码为 1,若是没有依赖的页面,这一部分不占空间。
读取一个原始模板文件,一个业务组件对应着一个模板,先读取一个原始模板数据。
建立 XML 解析器,由于原始模板是 XML 格式,使用XML解析器来解析其中的内容,XML 解析器会按照 XML 的格式获取到每一个节点以及它的属性,因此接下来只要遍历这些节点和属性来序列化原始数据。
开始遍历,先获取一个节点名,先记录节点开始标记。
根据节点名字符串,先建立对应的基础组件编译器对象,在编译工具里,每个基础组件都注册了对应的编译器类型。用户开发自定义基础组件,也要提供自定义编译器注册到编译工具里。基础组件和对应的编译器类经过组件类型关联起来。
获取该基础组件下全部属性,开始遍历属性并处理。
每获取到一个基础组件属性,就调用编译器处理属性,编译器知道每一个属性应该如何处理,由于这是定义属性、开发编译器类的时候肯定的,每一种属性都会被序列化成如下4种类型:int 整型、float 浮点型、string 字符串型、表达式类型,前二者直接做为序列化后的值写到返回结果里,后二者先经过 hashCode 为一个 4 字节索引做为序列化后的值写到返回结果里,真实的内容存储到临时列表里,后面会存储到单独的资源区。
遍历完当前节点全部属性。
按照整型、浮点型、字符串、表达式四种类别归类属性,按照 4 字节 key 索引、4 字节 value 索引存到内存里。
当前节点处理完毕,写入一节点结束标记。检查是否遍历晚全部节点,若是还有其余节点,回到第 6 步开始处理新的节点,若是没有,开始下一步准备写入文件
将第 11 步序列化后的组件数据写入到文件,将第 9 步里存储的字符串和表达式资源分别依次写入到文件。
这样组件区、字符串区、表达式区的起始位置都知道了,就可已更新第3步里预留的空白区域。
若是有扩展数据,能够在表达式区后面写入扩展数据,目前作保留。
所有写完以后全部数据输出到文件,文件后缀为 out。
目前的局限性
在上述编译过程当中,每一个基础组件的编译都须要对应的编译模块器来执行二进制转换工做,也就是说每一个类型的基础组件都有一个对应的编译器,这对于扩展新的自定义基础组件带来了一些不便,由于还要开发对应的编译器类,目前咱们正在将它重构成基于属性的编译器模式,并经过配置文件的方式来解耦对自定义基础组件节点、自定义属性编译处理的逻辑,这样才能真正释放它的动态性,有助于提高开发效率与使用便捷度。指针