10 倍高清不花!大麦端选座 SVG 渲染

一、背景介绍

用户在大麦上购票,需要自行选座。在大型场馆下,如何让 10 万 + 座位绘制达到闪开?这需要技术在绘制上保证性能流程,在选座渲染上通过技术手段赋予更多可能性。因此,大麦引用 SVG 绘制技术,并根据业务场景下作了很多优化,本文是大麦在用户端的技术方案设计与应用实践。

二、10 万 + 座位绘制面临以下挑战

  1. 如何丰富标签样式及属性;
  2. SVG 渲染性能优化;
  3. SVG 如何与业务场景结合;
  4. 如何将 CSS 能力应用到 SVGKit,保持(iOS\Android\H5)一致性。

三、大麦 C 端场景下 SVG 应用

  1. SVG 介绍

可伸缩矢量图形 (Scalable Vector Graphics),用来定义用于网络的基于矢量的图形,使用 XML 格式定义图形,图像在放大或改变尺寸的情况下其图形质量不会有所损失,是万维网联盟的标准, DOM 和 XSL 之类的 W3C 标准是一个整体,不失真,兼容现有图片能力前提还支持矢量 (浏览器兼容情况),通过浏览器很早版本支持情况在主流浏览器都支持,SVG 提供的功能集涵盖了嵌套转换、裁剪路径、Alpha 通道、滤镜效果等能力,它还具备了传统图片没有的矢量功能,在任何高清设备都很高清。

图 1 SVG 与其他格式图片比较

  1. SVGKit 使用

浏览器默认就支持 SVG 渲染,属于 XML-Dom 家族系列,但是在移动端上并没有做原生支持,还是按照 XML 进行的读取,支持的开源库也不多,在 IOS 上,目前 OC 版本 SvgKit 还不错,官方 Github 也在继续维护,虽然更新较慢,通过几次 patch 提交 PR 还是很快 merge 的,一些通用属性和控件支持的不够完善,需要进行定制开发,swift 版本的 macaw 也不错,在动画效果上更加酷炫,目前也正在做 swift 效果迁移到 OC 中,渲染流程如下

图 2SVGKit 渲染加载流程图

1)SVGKit 有哪些标签?

复制代码

circle= SVGCircleElement; 【圆形】
clipPath= SVGClipPathElement;【层叠路径】
description= SVGDescriptionElement; 【描述】
ellipse= SVGEllipseElement; 【椭圆】
g= SVGGElement; 【容器标签】
image= SVGImageElement;【图片】
line= SVGLineElement;【直线】
path= SVGPathElement;【路径】
polygon= SVGPolygonElement;【多角形】
polyline= SVGPolylineElement;【多边形】
rect= SVGRectElement;【矩形】
svg= SVGSVGElement;【SVG 容器标签】
switch= SVGSwitchElement;【选择】
text= SVGTextElement;【文本】
textArea= TinySVGTextAreaElement;【区域文本】
title= SVGTitleElement;【标题】

2)扩展基于三端统一 SVG 标签和属性

图 3SVG 标签属性扩展大图

3)SVG 标签在 SVGKit 中渲染流程

a)SVGKit 核心渲染原理分析

视图 SVGKFastImageView.m 加载到窗口显示

核心中心处理类,主要加载 SVG 文件资源文件

复制代码

类:SVGKimage :NSObject
SVGKParseResult* parsedSVG = [parser parseSynchronously];// 解析
SVGKImage* finalImage = [[SVGKImage alloc]initWithParsedSVG:parsedSVG
fromSource:source];

b)分析:

复制代码

解析 SVG- 到合成 ViewLayer
初始化 SVGKSource source svg 资源实例
初始化 SVGSVGElement->DomTree
初始化 SVGDocument->DomDocument
CALayerTree 最终合成的 Layer 树
SVGKParser* parser = [SVGKParser newParserWithDefaultSVGKParserExtensions:source]; 开始解析

c)解析 XML 类 SVGKParser: NSObject 解析 SVG(XML)文件

复制代码

+(SVGKParser ) newParserWithDefaultSVGKParserExtensions:(SVGKSource )source
(SVGKParseResult*) parseSynchronously. 解析异常处理XML解析处理
XML解析过程 SAX
// 每解析一个Node添加到 DOMTree中. (SVGKParserStyles)
SVGKParserDefsAndUse 【解析 useAndDefs 样式】
SVGKParserDOM 【解析 DOM】
SVGKParserGradient【解析渐变标签】
SVGKParserPatternsAndGradients【解析图案】
SVGKParserStyles【解析样式】
SVGKParserSVG【解析 SVG 标签】

d)解析 XML 中 CSS 样式类 SVGKParserStyles :

复制代码

标签解析到生成 Layer 层
1.类:SVGKParserDOM.m:SVGElement.
2.核心思想:
SVG 标签渲染流程一、SVGKImage.m 渲染 核心思想:遍历 DOM 映射到 iOS layer 绘制
3.生成 UILayer:
-(CALayer *)newCALayerTree
CALayer*newLayerTree= [selfnewLayerWithElement:self.DOMTree];
CALayer sublayer = [selfnewLayerWithElement:(SVGElement )child];
[newLayerTreeaddlayer. Sublayer]
[element layoutLayer:layer];
[layer setNeedsDisplay];

4)SVGKit 分析总结

SVGKit 版本升级 2.X 升级 3.X-Release,升级后主要是一些属性的支持度更完善,包括 Text 富文本渲染,字体多样式支持,还有一些渲染上的优化,可通过 patch 提交查看,比较一下 W3C 下 SVG 图在 2.X 分支及 3.X 分支的解析及渲染时间,性能能也有提升,同时增加 image 图片加载 base64 图片,加载在线 URL 及本地资源图片,我们也在 SVGImageElement 中提供扩展 API 增加 Webp 支持,因为 SVG 本身是为矢量图方案加入 PNG 等图存在一定模糊情况,不过运营可能会在底图上做一些 Logo 展示,为了减少 SVG 编辑复杂度,做了一些 png\jpg 图的嵌入,一般图片都不大,以下 API:展示 base64 运营位图片而设计的

复制代码

[NSData dataContentWithBase64Str:str]
  1. 基于 CSS 着色能力

1)为什么用 CSS 着?

SVG 虽然是绘制图形,原理如同 HTML,是给每一个标签设置一个单独 style 好还是通过 CSS Id /class 映射好呢,这个思路和 HTML 处理 STYLE 样式是一样的,便于更改和维护,在性能上也更好,同时增加了 important 属性,可以更好的配置样式,可做到运营侧根据样式 style 下发方式达到更改 SVG 图效果,可以做到更多活动效果及个性化需求。

2)SVG-CSS 着色渲染过程

SVG 标签基于 CSS 样式快速应用,通过遍历 DomTree,找到对应的 Node 节点,在给 node 节点设置 id 或者 class,然后局部刷新 Tree 父节点,实现换色,细节流程如下

图 4CSS 着色原理与时序图

3)大麦端选座渲染效果

图 5CSS 着色渲染效果

4)CSS 着色原理总结及性能比较

如何确定属性使用的是 CSS 颜色还是自带 style 属性?

当 SVG 在解析生成 DomTree 后,我们可以根据 CSSStyle 样式存储的 CSS 样式,给 Node 标签设置 id 及 class,当更改 nodeList 后,相当于树结构进行了修改,在绘制时候查找属性会根据优先策略 id > class > 进行查找进行属性赋值,我们根据 CSS 属性 !important 来设置最高优先级,这样就避免了此问题。

端侧渲染流程如下,左侧:是基于 node 遍历后修改,右侧是修改 id/class 方式【推荐】。

性能比对:为了兼容 W3C 标准,端上增加了 CSS 特殊属性 important。

图 6SVG-Codec 总体性能提升对比

  1. SVG 约束 DTD

1)背景介绍

当 SVG 生产端在制作 SVG 图,可能会用到 Adobe 等软件,有很多复杂属性及层叠,可能会产生复杂 XML 格式,这样在渲染过程中会造成大量遍历,影响性能,也有一些特殊属性,端上并没有支持,例如滤镜、动画,这样,我们就需要有一种约束来校验生产和渲染 SVG 能够一致。

2)文档类型定义

(DTD)可定义合法的 XML 文档构建模块。它使用一系列合法的元素来定义文档的结构。DTD 可被成行地声明于 XML 文档中,也可作为一个外部引用。

DTD 被定义在 xml 的 DOCTYPE 声明中。

3)定义一个名为 note 的 DTD

如果要使用内部定义,则在 xml 文件的 xml 版本声明头下面添加如下代码块:

如果要引用外部 DTD,那么它应通过下面的语法被封装在一个 DOCTYPE 定义中: 例如,在 xml 文件的 xml 版本声明头下面添加如下代码块:

一个 DTD 的内容示例:

复制代码





其中:

!ELEMENT note 定义 note 元素有四个元素:“to、from、heading、body”

!ELEMENT to 定义 to 元素为 “#PCDATA” 类型。

PCDATA 的意思是被解析的字符数据(parsed character data)。可想象为 XML 元素的开始标签与结束标签之间的文本,PCDATA 是会被解析器解析的文本。这些文本将被解析器检查以及标记,文本中的标签会被当作标记来处理,而实体会被展开,不过,被解析的字符数据不应当包含任何 &、 字符;需要使用 &、 实体来分别替换它们。

4)例如在 DTD 中声明

它表示在 和 之间可以插入字符或者子标签,CDATA 的意思是字符数据(character data, CDATA 是不会被解析器解析的文本。在这些文本中的标签不会被当作标记来对待,其中的实体也不会被展开。

5)如何校验

在完成 DTD 文件的编写后,就是使用 DTD 了,1、一般使用代码解析的方式,进行 DTD 对 xml 的规范性校验,首先在 svg 的头部加入: 然后在解析 svg 时,声明合法性校验例如,以 SAX 解析 XML 为例:

复制代码

SAXParserFactory spf =SAXParserFactory.newInstance();
spf.setValidating(true);// 关键设置
SAXParser sp = spf.newSAXParser();
XMLReader xr = sp.getXMLReader();
XMLParser.SAXHandler handler =newXMLParser.SAXHandler();
xr.setContentHandler(handler);
xr.setErrorHandler(newSAXErrorHandler());// 输出校验出错的信息
xr.setProperty("http://xml.org/sax/properties/lexical-handler",handler);
xr.parse(newInputSource(is));

其中,必须设置 setValidating(true); 才能使 DTD 校验 xml 生效,为方面使用,提供了可以动态读取 dtd 的方式,为不需要将 dtd 信息添加到 svg 的文件中:执行 Java -jar 命令,传入两个参数:一个是 svg 的全路径;一个在同目录下的 dtd 的文件名(带扩展名)。

  1. 选座性能优化

1)性能调研:APP 端 /H5 上渲染如何解决 10 万座位渲染,端侧通过组件复用,手机设备性能天然还是不错的,加上我们通过预加载资源与分区加载结合方式,点击区域后进行绘制策略,避免一次性加载全量 10 万数据,也给用户更好的交互体验,然而 H5 侧,浏览器就不那么流畅了,随着 H5 技术发展,H5 新特性的支持,通过实践使用 SVG 方案,每个座位都用一个 svg 元素显示,由于 svg 的矢量特性,缩放无锯齿,展现效果比较好,但也就支持到 3 万左右的座位,座位再多也会出现渲染慢和缩放卡顿等问题。Svg 的每个元素也算是一个浏览器的 dom, dom 数量一多起来,达到 3 万到 10 万,浏览器渲染显然不行。如何减少 dom 节点,又能显示这么多内容呢?很容易就能想到用 Canvas 来绘制座位,无论在 Canvas 绘制多少元素,在浏览器都是一个元素,这样就很好解决了 dom 数量的问题,网上能搜到多个应用比较多的开源 Canvas 组件,能实现绘制元素的实时更新,还支持元素的鼠标 / 手势时间响应。但是都有一个问题,基本上的原理都是采用帧刷新重绘 Canvas 画布的方式来实现视图更新,确定采用 Canvas +svg 渲染座位图,模拟了 10 万座位的渲染并实现了缩放拖拽等操作,达到了预期的效果。

图 7 选座性能优化 – 预加载

图 8端选座性能交互图

  1. SVG 场馆彩虹图实现

1)SVG 彩虹图介绍:

在售票选座业务中,需要为用户显示场馆图(SVG 格式),给用户一个场馆的整体印象,同时方便用户选择场馆的看台,进而展示看台座位进行选座。但是,在使用彩虹图展示场馆图之前,场馆图的看台区域仅展示看台当前可售座位中的最高票价对应的颜色进行展示,如下图所示:

图 9 非彩虹场馆图

每个看台都是单一颜色的,不能反映出当前看台中可售的座位的价格分布情况,很容易迷惑用户,造成每个看台只有一种票价的印象,同时不方便用户快速定位他的目标价位所在的区域。为了准确的反映出场馆每个看台的价格情况,需要将看台中所售的所有的座位的价格展示出来,因此,采用彩虹图的形式。目标效果如下所示:

图 10 彩虹场馆图

每个区域的所有的座位价格以彩虹的形式显示出来,相比以前的只显示最高票价的颜色,彩虹图可以清晰的展示每个区域中座位的价格情况。

2)总体思路:

在每个看台区域中以彩虹图形式展示多个颜色,就需要将每个看台区域进行划分,放弃之前用一个这类的绘制标签来展示一个看台区域,一个区域内应该包含多个排,对每个排按照座位价格进行着色,进而对每个看台进行同样的处理,总体上形成彩虹样式展示。

因此,一个看台区域,应该是多个 svg 标签组合而成的。如图:

图 11SVG 文件说明

对 SVG 底图进行改造,将老的一个 SVG 标签代表一个看台区域的形式,改造成每个看台区域由标签进行包裹的多个标签的组合。

为了方便降级,处理不显示彩虹图的业务需求,同时约定标签下的第一个标签,表示整个区域,同时不再解析渲染后面的排信息。

3)算法生成彩虹图方案

a)介绍:

彩虹图生成算法主要是通过座位的分布和座位的票档圈出一个看台中相同票档座位的范围,然后生成一个 path 路径。将这些 path 路径加入到 svg 底图中去并且和相应的票档绑定,就能实现一个区域多种颜色的效果。

b)步骤如下:

①对看台中所有排和座位进行分组排序;

②计算看台方向;

③获取同种颜色座位边界;

④计算色块的方向;

⑤获取色块的路径;

⑥生成看台的彩虹图效果;

⑦遍历所有看台生成完整的彩虹图。

首先将某个看台所有座位按排分组,然后将排按排号从小到大进行排序。数据结构如下:

图 12 看台编号与座位号示例

座位数据是必备的基础数据,后续一切的计算都依赖于座位数据。

通过第一排和第二排座位的相对位置算出看台的方向。

比如: 拿到 1 排 1 号和 2 排 1 号座位的坐标,从 2 排 1 号向 1 排 1 号画一条射线,这个射线的角度就当做看台的方向。

后期如果在生产 SVG 的时候将舞台位置标记出来的话就可以利用舞台来确定看台方向。

图 13 看台方向 -1

每个看台的座位分布可以分成两种,一排一种价格和一排多种价格。

其中一排一种价格的情况就以同色最后一排为边界。如下图绿色的线。

一排多种价格的情况就需要把每一排不同颜色转换处的那两个点记录下来连成边界线。如下图红色和黄色的线。

图 14 看台方向 -2

红黄绿三条线所在的座位就是我们需要的色块边界。

拿到色块边界座位数组之后还需要知道色块的角度。一排同色的色块方向就直接使用看台的方向。一排多色的色块方向计算方法如下:

图 15 确定颜色区域 -1

将第一个座位 P1 和最后一个座位 P2 连线,取这个线段的垂直线 a1 和 a2。用这两条垂直线分别和看台方向取夹角。夹角小于 90 度的垂直线的角度作为色块的方向。图中 a1 就是色块的方向。得到色块边界和色块方向后就可以计算色块 path 的路径了。

①得到包含这个看台 path 路径的最小矩形 rect (图 1);

②从上一步获取的座位边界数组中取四个点分别为: 第一个点 P1,第二个点 P2,倒数第二个点 P3,最后一个点 P4 (图 2);

③由 P2 向 P1 方向做一条射线得到和 rect 的交点 A1,由 P3 向 P4 方向做一条射线得到和 rect 的交点 A2 (图 2);

④根据色块方向获取色块路径的几个关键点:A1 A2 A3 (图 2);

⑤将几个关键点和座位边界所有点连接起来生成一个闭合的路径 (图 3 橙色线框)。

图 16 确定颜色区域 -2

拿到所有色块路径后就可以将色块填充对应的颜色并且按顺序叠加到看台上形成彩虹图效果。

色块的叠加方式如下: 色块的生成顺序是 path1->path2->path3->path4,然后倒序叠加到看台上 path4->path3->path2->path1。最后就是遍历所有的看台,生成一张完整的彩虹图。

图 17 确定颜色区域 -3

四、总结

本文主要讲解了大麦核心链路选座 SVG 应用,并结合实际场景做了一些创新尝试,包括:丰富 SVG 应用的业务场景、SVG 标签属性及扩展、CSS 着色、渲染性能优化等,目的是让端解析接近浏览器解析效果,并提供更好的端选座性能体验。

作者简介:

阿里文娱无线开发专家 波涛