背景
这篇文章起源于一个业务上的需求:小程序上需要展示查关联、企业关系图等关系图。显然这次需求的关键点有两个:
- 关系图可视化
- 移动端小程序
我们在之前的业务中有过不少关系图可视化的实践(感兴趣的同学可以查看 《图可视化解决方案:企业风控》),除了蚂蚁自研开源的 antv,我们也接触过 echarts、d3 等不少图可视化引擎,但支持小程序甚至移动端的图可视化引擎几乎还没见过。
由于需求紧急,我们的初版方案是在小程序内通过嵌套 webview 的方式做了一个粗糙版的实现,显然在性能、体验都存在问题,开发方式也不够优雅。
之后我们联合 antv 团队一起推动了 图可视化引擎 G6 对移动端的支持,针对移动端特性做了适配,让关系图在小程序中的操作和表现都更顺滑,同时完全贴合原生小程序开发方式,最终以 G6-mobile 包的形式发布。
g6-mobile
有关 G6-mobile 的内容有很多。本文的重点是:梳理一遍从可视化基础原理到 G6-mobile 诞生的脉络,讲述我们在 G6-mobile 实现中遇到的问题、方案选择和思考。有关 G6-mobile/G6 的 api 、具体源码实现,这次不会涉及,可以参考 从零开始学G6。
可视化
开发绝大多数 web 前端应用的时候,我们关心的是文字/多媒体、区块排版、布局、样式和交互,关心的是如何给用户『阅读』体验更好的『网页』。
而开发可视化应用,我们关心的则是数据计算、不止是区块的各种图形的绘图渲染,当然也少不了交互,希望通过一张图让用户看清数据背后隐藏的信息。可视化的本质也就是把数据组织起来(比如从非结构化变成结构化)之后绘图呈现。
关系图可视化
除了最常见的图表,还有一类关系图可视化,这也是我们这次业务的需求,这类需求通常会在安全或风控的业务中出现。关系图相对于通用图表,数据之间关系复杂或难以确定,所以可视化时要考虑:
- 各种布局,它决定了如何展示图形的关系
- 复杂的关系节点
- 多样化的交互
这里先提到这,后面会详细讲。
可视化开发和渲染基本原理
可视化开发中我们可能会相对少的用到 html 和 css,更多用到 canvas、webgl、svg 之类的图形 api。因为可能要处理视觉细节,很多时候即时用了主流的可视化图或图表库,我们依然需要接触底层渲染层 api 精细控制图形渲染细节。
浏览器如何绘制图形
不管可视化和一般 web 前端开发有多少不同,最终都还是通过浏览器渲染呈现的。浏览器通过自身的渲染引擎绘制图形,主要有两类方式:
- HTML/SVG + CSS
- canvas(canvas2d/webgl)
1、HTML/SVG + CSS
可能提到可视化我们第一反应不会想到 html 和 css (实际中也极少用) ,但这个方式确实适合实现布局简单(区块式)的常规图表可视化。 这种方式主要靠 css 技巧,比如:
- 柱状图可以通过 grid layout + linear-gradient 实现;
- 饼图可以用 conic-gradient 圆锥渐变实现;
- 折线区块图可以用 clip-path 拼多边形实现等等。
这里还提到了 svg,由于浏览器也支持直接内嵌 svg 标签,支持我们通过 dom api 操作 svg、css 处理 svg,同时提供了很多图形元素标签(html中需要 css 实现)。所以 svg 本质上是一种有特殊属性的 dom ,也可以实现可视化,本质上跟用 html 实现没有区别。
由于 svg 基于 xml 语法,我们可以很直接地做视图和数据的解耦:
<rect x="50" y="50" width="100" height="100" fill="#000"/>
相比之下 css 至少需要理解数据和 css 属性之间的映射关系,比如可能需要做下面这样的百分比转换:
/**
data = [4, 4, 8] ---> 对应比例 25%, 25%, 50%
*/
.rect {
background: linear-gradient(to bottom, yellow 25%, blue 25%, blue 50%, red 50%);
}
- 优点
- 开发方便
- 缺点
- css 语法难以和数据解耦,难以拓展维护。
- 一个 svg 元素只能表示一种图形,数据量大的时候回产生大量的 svg 元素,耗内存、影响性能。
- 浏览器解析 html/svg 和 css 需要走一遍 构建 dom tree -> renderObject tree -> renderLayer tree 的渲染引擎流程,重渲会带来较大的性能开销(可视化重点在数据计算和绘图渲染)。
2、canvas(2d/webgl/webgl2)
提到 canvas 我们一般是指代浏览器的 canvas2d 上下文,而准确的说浏览器的 canvas elements 提供了 2d、webgl 和 webgl2 三种我们常用的上下文 api (其实还有 bitmaprenderer, placeholder, 这里不展开,感兴趣的可以看 html spec canvas context mode)。
从 api 规范角度看
- canvas api 规范有 canvas2d 规范和 webgl 规范两类
- webgl 规范是 opengl es (opengl es 是 opengl api 的子集,针对手机等终端设计)规范在 web 端的实现,webgl2 和 webgl 分别对应 opengl es 3 和 opengl es 2。
- 最新的还有 webgpu,它是最新的 3d 图形api,相比 webgl 有很大升级和差异,不再基于 opengl 而是 vulkan/direct3d,这里不展开讲。
不同于 html/svg 的声明式语法,我们通过 canvas api 是以指令式的方式绘图。canvas 元素在浏览器上创建画布并提供上下文接口,我们调用 api 绘制点线面登记本图形、设置图形属性、颜色、形状变换,从而完成绘图。
另一点, canvas2d 和 webgl 的绘制最终也都是给到 gpu 渲染 (大部分现代浏览器都会 gpu 加速,dom渲染最后也是给到 gpu 位图),他们相比 html/svg 方式最大的不同是不存在 dom renderObjectTree -> renderLayer 的流程。
不过 canvas2d 和 webgl 对 gpu 的控制有所不同,我们无法通过 canvas2d 直接操作 gpu,我们只能调用 canvas2d 简陋的api,浏览器在底层控制 gpu。webgl 预渲染到帧缓存后,再执行 webgl api(webglprogram)操作shader,因此我们可以在这里控制 gpu 渲染,在 shader 中做更精细化的操作或计算,性能远超 js。尤其是对于图形数量庞大的绘制,我们可以利用 webgl 实例化渲染并让 gpu 并行处理实现批量渲染,而 canvas2d 在浏览器底层并不会主动做这种优化。
-
canvas 优点
- 指令式语法,开发友好易于拓展。
- 直接操作绘图上下文,没有 dom 渲染带来的性能问题。
-
canvas 缺点
- canvas api 比较简陋
- canvas 只有一张画布,没有图形元素的概念( html/svg 的 dom 对应某个基本图形元素),导致canvas中很难做到局部渲染、局部控制、事件操作等,会有整个画布重绘的问题。
- 绘制图形数量大时也会有性能问题。
-
webgl 优点
- 绘制图形数量大时、处理像素点多的精细绘制时,可以操作 gpu 调优
- 3d 绘制,内置了 3d 投影和处理。
-
webgl 缺点
- 设备兼容性问题
- 开发成本
Native 如何绘制图形
还有点题外话,我们这里说的是 canvas elements 都是浏览器实现提供的,浏览器提供的东西最重要的就是通用性、跨平台、标准化。浏览器实现 canvas 至少要编译适配几个通用图形库,同时还需要再封装出一套 js api 给开发者用。所以浏览器上 canvas 的绘制调用流程是:canvas js api -> js引擎 -> 浏览器接口逻辑 -> 图形库 -> gpu 绘制。比如 webkit 封装了 skia 图形库,chromium 会用 skia 的 software rasterizer 绘制成位图,作为纹理上传到 gpu,或由 skia 的 opengl 直接绘制纹理到 gpu (感兴趣可以看 GPU Accelerated Compositing in Chrome)。
但这一层层的封装避免不了性能消耗,尤其在 js 引擎这一换。而 native 可以直接调用opengl 或其他底层接口。 所以 浏览器 canvas 的渲染表现会比 native 差很多。当然 native 也不具备浏览器的通用开放跨平台性。
G6
可视化引擎/库需要做什么
可以看到在浏览器中,无论用哪种方式绘图都依然有缺点。首要缺点就是,浏览器是通用平台,通用平台都只会提供基础 api,不会提供跟应用场景强相关的高级 api(所以前面说 canvas api 简陋),而开发者实际中需要的是高级 api,比如 drawGraph、drawshape 之类的。这种高级 api 需要可视化引擎/库去实现。
因此最常见的是这种提供丰富绘制 api 的图表库,比如 echarts、g2;以及一些专业的图绘制,比如画地图地理的 mapbox、L7;还有一类无关应用场景、更灵活的绘制图形或物理模型,2d 的 spritejs、3d 的 threejs。让人不用通过 canvas2d/webgl 等底层 api 从基础图形画起。
前面提到可视化除了绘图之外的另一个关键是数据组织,所以还有一类专门用于数据处理、结构转换的框架,比如 d3.js。
事实上绝大多数可视化绘图(由于2d场景较多)都是基 canvas2d(3d场景肯定是用 webgl)。canvas2d 的两大缺点之一,绘制图形数量大时会有性能问题,我们前面提了可以用过 webgl 并行批量渲染优化;另一大缺点难以局部渲染、局部控制、存在画布重绘,这些问题就需要引擎/库在内部重点处理了。
G6 是个什么样的引擎
来到我们的重点 g6,g6 是 antv 图可视化体系下的一个关系图可视化引擎, 如果了解复杂网络图论的同学应该能 get 到 6 (六度分离)的含义,而 g 指的是图形语法(The Grammar of Graphic)。
图形语法
图形语法的详细介绍可以参考 图形语法,这里简单总结就是:把数据字段映射到图形属性,再把图形属性和图形正交,就可以组成描述能力丰富的可视化图。
- 几何图形:点、线、面、多边形等
- 图形属性:位置、颜色、形状、大小
- 数据如何映射到属性:就是 g2/g6这类引擎要做的事,提供语法 api。
再讲个题外话,事实上 antv 体系下各类图或图表库都是以图形语法为基础拓展的,包括 g2、g6,这是不同于其他配置形式的图表库 echarts、highcharts的地方。配置形式图表库的好处是比较适合视觉描述(产品描述)思路,上手开发比较容易,但不易拓展。比如想画一张柱状图,我们会先找出基础柱状图组件,再看这个组件提供了哪些 api 、能配置出哪些视觉效果,但如果 api 不支持就无法配置,数据到组件属性的映射关系在我们选定基础柱状图的时候就已经固定死了。
myChart.setOption({
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [{
data: [100, 200, 20, 10, 300, 150, 400],
type: 'line'
}]
});
而走图形语法的思路,我们会先思考需要显示什么图形,再考虑数据映射到图形属性的逻辑即可,不用担心图形是否支持这么映射,因此可以更灵活拆解式的开发。
chart
.line()
.color('#f3964a')
.position(`date*value`);
配置形式的开发方式缺失了灵活性,但方便业务使用,@antv/g2-plot 就是封装了 g2 的链式语法,提供配置形式的接口,这两种方式怎么平衡是个值得思考的问题。
G6 的架构
之前提到关系图可视化需要重点考虑布局、节点、和交互,所以 g6 的设计远比图形语法描述的复杂,g6 的源码需要以后再具体讲,这次我们主要讲 g6 现在 4.x 的架构,这关系到我们如何产出 g6-mobile,感兴趣还可以参考 g6的历史架构
上面是来自官方文档的架构图,不过跟实际代码比起来有些出入和模糊,我们逐个来讲。
从下到上来看:
- 最底层是基础渲染层,这一层被抽到另一个包 @antv/g 中, 对应我们上文提到的浏览可视化渲染方式 canvas2d、svg、webgl,其中对小程序的支持其实是 f2 在实现过程中 fork 的 g 3.x 的版本改造的,这个 g-mobile 3.x 目前是整合到 f2 中的并没有单独抽包。
-
上一层是一个拓展中间层,这一层的目的是为了把 g2 和 g6 的架构统一,提到的名词都是概念,有些 g6 已经实现、有些还没有,有的在 @antv/g6 里实现、有的在 @antv/g 里实现。
-
约束布局:解决的问题是布局过程中图形之间互相有依赖关系互相影响,约束布局把这些依赖关系在约束计算中内聚,外部布局并不感知,比如可以让多图形在同一方向的时候,均分区域大小,这是衍生自 g2 的方法,g6还没实现,感兴趣参考 约束布局在可视化中的应用
-
状态管理:@antv/g6 中实现,这是处理关系图可视化交互的解法之一,图中的节点或边是有状态的,g6 中默认处理交互状态,也支持自定义的业务状态。
-
事件模型:@antv/g 中实现,配合状态管理处理交互,g6 内置了事件、也支持自定义事件,g6 上的所有基础图形、Item(节点/边)都能通过事件进行操作。注意这里是构建了事件模型、注册和代理事件,我们之前说到 canvas 一大缺点是难以局部控制、事件操作,根本原因是canvas 绘制的是一张图片,用户在图片上点击时时不能获取对应的图形信息,所以还有个重点是定位。
-
定位:已经在 @antv/g 里实现。canvas 中定位获取图形元素的方法主要有这几种:
- 缓存一个隐藏的 canvas 通过颜色获取(渲染开销翻倍)
- canvas 内置 api( isPointInPath 可以判断获取对应的点是否在绘制的图形内部,所以可以遍历每个图形,isPointInPath 方法判断点是否在图形中,缺点就是遍历会导致性能差)
- 几何运算,canvas 的画布是有坐标的,根据图形的边缘坐标就可以建立一个最小覆盖图形的矩形(g6 里只计算了起点 xy 和 宽高),把它作为图形的包围盒 bbox;理论上只要我们的包围盒划分的足够细,我们就可以拾取定位任意位置元素。这样我们检测所有的图形,判断点是否在图形的包围盒内,如果图形绘制线,判断是否在线上,如果图形被填充,则判断是否被包围。
- g6 综合了几何计算 + isPointInPath 方法: 简单的图形使用几何算法,复杂的很多填充的图形使用包围盒 + isPointInPath 来检测。
-
-
再往上是组件层,这一层是 g6 无关 g2 架构的能力
-
拓展 shape:这里提到了 shape,这是 @antv/g 给出的『图形』概念,其中做的事就是我们前面提到通过 canvas2d api 绘制基础图形。
- shape 的基类 element (event-emit 模式)实现了一个绘图元素,实现可见性属性、矩阵变换、移动等绘制中用到的基本方法。
- shape 继承 element 实现一个基础图形的基类,这一层包装之后对外暴露统一的绘制和定位方法(获取包围盒)
- 通过拓展 shape 类,g6 可以创建 circle、rect 等不同的基础『图形』方便开发者调用,因此在 @antv/g6 中也实现一个 shape 类(即拓展 shape 类)。这个 shape 类是工厂模式,circle、rect 等具体图形在这个工厂中注册,代理 @antv/g 中 shape 的可见属性、绘制接口实现具体图形的绘制。
-
分组管理:g6 中 group 的概念,是对基础图形的合并处理,比如同一个 group 的多个绘图元素可以合并包围盒、统一创建销毁、设置状态。group 同样继承自 element 实现。
element、shape、group 就是 g 架构图中的三层渲染模型。
-
状态管理:上一层提过。
-
plugin:提供一些拓展分析工具组件,比如网格、tooltip之类的,这块被单独拆成了一个 @antv/g6-plugin 包。
-
animation:配置是否展示动画、以什么函数显示动画。动画的实现在 @antv/g 中,做法就是创建一个 timeline 定时器去执行动画函数,改变 shape 属性、执行 shape 的移动变换绘制。
-
layout: 前面提到布局是关系图可视化的一个核心,这里被抽到 @antv/layout 中,内置多种布局算法。
-
-
最上方是用户接口层,提供了 graph、treegraph(由于数据结构和布局自成一派,单独抽出,想具体了解参考g6 文档)、item (g6 中的 item 就是 node 和 edge,这是关系图中最基本的两种元素,他们也是通过拓展 shape 类注册具体图形实现的,g6 内置了多种具体图形作为默认 node 和 edge 供选择)。
g6 体系中还有一部分是数据处理,涉及到不少模块:
- 数据转换(@antv/data-set 包中专门实现)
- 并行计算(我们前面提到为了提供布局渲染效率可以利用 gpu 并行计算,@antv/webgpu 包中独立封装了 webgpu 的接口,涉及到许多依赖,后面会讲)
- 图算法(@antv/algorithim 包中单独实现,调用 dfs 之类的算法处理 node 和 edge)
尽管这架构图看起来还算清晰,但每一块的实现分散在 g6 相关的多个包里,所以最后需要梳理一下 g6、 g 各个包之间的关系:
G6 mobile
G6 mobile 面临的问题
了解完 g6,我们可以开始实现 g6-mobile 了,先看下面临的问题:
- 移动端包括 h5 和小程序,h5 中的事件及部分 api 在小程序中不能使用,需要做一定适配来同时支持 h5 和小程序。
- 小程序封装的 canvas api 和 浏览器上有差异,导致 g-canvas 绘制需要调整。
- g-base 里包含 document、window 的引用,在小程序上不能用,需要兼容。
- 别忘了之前我们提到 f2 在是实现是 fork g 3.x 内部做了一套 g-mobile。所以我们也就面临多个方案选择:
- 把 f2 内置的 g-moblie 独立并兼容 g 4.x。
- 从 g 4.0 产出 g-mobile,g-mobile 和 g-canvas 的渲染和 api 保持一致,只是针对事件特殊处理。
最后权衡多方工作量和未来拓展性,我们选择了第二种
G6 mobile 的整体思路
- 渲染:抹平小程序 canvas api 差异
- 通过 proxy 把 canvascontext 的调用转成了 小程序 canvas context 的接口。
- 交互:增加移动端手势事件、交互 behavor 对接移动端事件
- 事件我们想到了 hammer.js,这个专门处理移动端手势交互(尤其是多指触控)的库,但它同样依赖浏览器环境,所以我们精简了一份,事件由外部触发,hammer 只做事件解析,独立成 g6-hammer 包。
- 由于 g6-hammer 只做事件解析,我们还需要在 g-mobile 中增加 mini-event 模型移除 document 和 window 同时触发事件。
- 另外在 g6-mobile 中也需要将 behavior 改造适配移动端事件。
- g6-mobile 中重写 event controller 去除 document 和 window。
- 布局
- g6-mobile 中重写 layout controller 去除 webworker、webgpu。
根据这些任务我们同样需要拆分到 g6、g 的多包中分别进行,调整之后 g6 的包变成了下面这样:
G6 mobile 拆包优化
到此,我们看似完成了 g6-mobile 的开发,但打包构建出来之后我们发现了新的问题,g6-mobile 初始版本打包之后发现体积很大,parse 之后 1.3 M,gzip 之后 330k,这个尺寸移动端难以接受。具体打包状况如下:
分析了一下几个比较大的部分:
- 480k node_modules, 包括重复的 g-base 80k,regl 100k,重复的 gl-matrix、inversify 和 hierarchy 等。
- 590k 的 g6-mobile,g6-core 及其依赖。
- 210k 的本地 g-mobile 和 g-base。
- 30k 的 g6-core 中的重复依赖 gl-matrix 和 algorithm。
可以看出有一下几点问题:
1、g-base 有重复版本
monorepo
从前面的架构图可以注意到,g6、g 都包含多个子包,在包管理上都采用了 monorepo 的包管理思路,用 lerna(yarn workspace)进行包管理。lerna 会对子包 symlink、并在配置 yarn workspace 的情况下会做公共包提升(hoist)。
使用 lerna 中开启 yarn workspace,lerna bootstrap命令由yarn install代理,等价于在workspace的根目录下执行yarn install,这么做是因为 yarn 本身提供了 更好的依赖分析、hoisting 功能。但 lerna 的上层指令方便开发者的使用。
// lerna.json
{
"npmClient": "yarn",
"useWorkspaces": true,
}
// package.json
{
"workspaces": [
"packages/*"
],
}
但 g6 和 g 两个主包之间的调用、项目和 g6 主包之前的调用关系不再是 monorepo 的关系了,我们就只能继续用 npm link了。
从前面的 g6、g 包关系图可以看出: g6-mobile 依赖了本地 link 的 g-mobile;而 g-mobile 依赖了同样是本地 link 的 g-base;而 g6-mobile 本身也会直接引用 g-base,间接通过依赖 g-core 进而引用 g-base。不记得就再看一下:
所以打包中包含重复的 g-base,大概有 80k 大小。
这种情况在正式发包的时候不会出现。手动把 g-base 全部指向 本地之后,不再有重复的 g-base,总包大小来到 1.22 M。
同样的重复问题还有 @antv/util,g-mobile 发包之后正常引入应该就不会有问题,会释放 20k。
2、子包版本不一致导致的重复包
- 遵循不改动 g6-core 的原则,不到 200k 的g6-core 需要全部保留,没有优化空间, 不到 50k 的 g6-mobile 源码全部保留,没有优化空间。
- 另外有重复的 gl-matrix,子包中 gl-matrix 跟 主包中的版本不一致,看起来也可以优化,大概有 60k 的 优化空间。
g6-core node_modules 中的 重复依赖 gl-matrix 和 algorithm,跟 g6-mobile node modules 重复,跟其他包间接引入的 gl-matrix 版本不一致(原因是 g-mobile 中引入了 3.0 版本 的 g-math, @antv/matrix-util,g6-core 和 g6-mobile 中都是 2.0 版本),这块同步后,释放出 60k。
有重复的 algorithm,原因是 g6-core 里和 g6-mobile 用的版本不一致,(直接 build:g6-mobile 导致的,没有build g6-core,从 core 子包下找的 依赖 )同步后释放 10k。
3、layout 有冗余代码
- 重点是加在一起超过 300k 的、 @antv/g-webgpu 及其引入的 @antv/g-webgpu-core、 @antv/g-webgpu-engine 相关依赖。这部分我们在移动端暂时不需要,溯源查了下,发现均是通过 @antv/layout 内置布局需要开启 gpu 渲染引入的。这里奇怪的事情出现了,我们之前明明已经在 g6 layout controller 中移除了 webworker、webgpu 的引用,竟然还有这些依赖。仔细分析我们发现目前 @antv/layout 直接引入了所有布局并自动注册,github.com/antvis/layo…。因此我们需要分两步改造:
- @antv/layout 支持 export 各类布局,同时不自动注册布局。
- 把 g6-mobile 主包不提供内置布局、只提供拓展布局的接口,把布局作为拓展包,让用户按需引入。
这样可以把这里优化出大约 350k 的空间。
4、node_modules 中其他无用的包
接下来重点放在 370k 的 其他 node modules 上:
- 100k 的 regl,是 webgl 需要的包,移动端不需要。溯源查了下,发现只有 @antv/g-webgpu 中引了 regl,这在 layout 和 webgpu 优化完之后,空间也可以释放掉。
- 33k 的 hierarchy,tree graph 需要,由于 treeGraph 的数据接构和配置项跟通用 graph 有很大差异,考虑把 tree graph 拆到 拓展包,主包不提供、只提供拓展 treeGraph 的接口。
- 30k 的 darge,也是布局引入的,拆出 layout 之后可以释放。
- 45k 的 inversify, webgpu 引入,拆出 layout 之后可以释放。
最后我们总结一下:目前包体积 1.3M,经过一系列优化
- g-mobile 发布后,不再有重复的 g-base , @antv/util 依赖,释放 120k。
- @antv/layout 拆到拓展包,释放 @antv/g-webgpu 相关包、regl、inversify、darge,释放520k。
- tree graph 抽到 拓展包,释放 hierarchy,释放 30k。
- 解决 g6-core、g6-mobile 中的 gl-matrix 和 algorithm 版本不同步的问题,释放 60k。
- 总计核心包 500k 左右,包括:
- g-mobile 100k
- g-base 100k
- g6-mobile 50k
- g6-core 150k
- node_modules 100k
- 拓展包用户按需引入
G6 mobile beta 版
经过上述操作,我们最终完成了 g6-mobile 的开发调整。发布了两个包
g6-mobile
g-mobile
《G6-mobile beta 版也跟随 G6 4.2.0 一起面世》
如果你也想在小程序中使用 g6,可以参考 《如何使用 G6 在小程序里面画关系图》
如果你想看 G6/G6-mobile 更详细的源码分析,欢迎持续关注,《从零开始学G6》。
参考资料
- html.spec.whatwg.org/multipage/c…
- developer.mozilla.org/en-US/docs/…
- www.chromium.org/developers/…
- www.yuque.com/antv/blog/b…
- www.yuque.com/antv/g6-blo…
- citeseer.ist.psu.edu/viewdoc/dow…
- github.com/antvis/layo…
最后
微信搜索公众号Eval Studio,关注更多动态。