GaiaX(盖亚),是在阿里娱乐内宽泛应用的 Native 动态化计划,其外围劣势是性能、稳固和易用。本系列文章《GaiaX 开源解读》,带大家看看过来三年 GaiaX 的倒退过程。
GaiaX 的布局计划 – Flexbox
阿里娱乐业务作为一个内容散发、生产综合体,不仅笼罩 Phone 端,在 Pad、OTT、Mac、车机、IOT 带屏设施上都为宽广用户提供在线视频媒体服务。
GaiaX 作为娱乐散发场景的一个重要端渲染解决方案,在进行布局技术方案设计时,必须充分考虑多端、多屏的响应式布局诉求。家喻户晓,浏览器场景很好的解决了屏幕窗口多尺寸的动静布局问题,其采纳的布局计划即为 Flexbox。
挪动端原生 Frame 布局是 Native 计划的最优解,因为双端可最大限度的施展 OS 的渲染个性来保障渲染性能。为了验证 Flexbox 的实测性能,咱们针对单层及多层嵌套设计了测试用例,通过数据体现(耗时单位为 ms)来进一步证实 Flexbox 计划是否能够作为 GaiaX 的布局计划。
试验后果的产出,齐全打消了咱们对 Flexbox 在性能上的顾虑,再加之其合乎 W3C 标准、社区丰盛、学习复杂度低等劣势,最终团队抉择 Flexbox 作为 GaiaX 的布局技术计划。
Flexbox 高性能解析计划 – Stretch
为什么抉择 Stretch 作为布局根底
在确定了布局计划后,接下来要做的事就是确定解析的技术选型。社区反对 Flexbox 布局解析的技术计划,最为大家耳熟能详的是 facebook 推出的 yoga,这也是业内的支流技术选型。
绝对于 yoga 来说,Strectch 具备以下的劣势:
● 包大小体积
● 反对多线程的部分计算
● 反对跨平台的能力(iOS、Andriod、JS 等)
● 基于 rust 语言实现,具备高性能,低内存占用等个性
Stretch 的这些的劣势是基于 Rust 语言个性所带来的:
更平安的内存治理:
与 C/C++ 语言相比,应用 Rust 语言来进行程序设计能够有助于从源头预防呈现诸如空指针,缓存溢出和内存透露的内存问题。
更好的运行性能:
与 Java/C# 等语言相比,Rust 语言的内存治理不是依附垃圾回收器机制(GC)来实现的。这个设计进步了程序运行的性能。
原生反对多线程开发:
Rust 语言的所有权机制和内存平安的个性为没有数据竞争的并发提供了语言层面上的原生反对。
网上曾经有了很多对于 Rust 性能剖析比照的文章,以下就是性能比照后果,性能上能够跟 C ++ 性能并驾齐驱,某些场景下效率甚至优于 C ++。
Rust vs C++:
Stretch 入门
因为 Stretch 是采纳 Flexbox 布局的形式,在开始介绍 Stretch 外围布局计算逻辑之前,还须要简略理解一下 Flexbox 的根底概念
● 采纳 Flex 布局的元素,称为 Flex 容器(flex container),简称 ” 容器 ”。它的所有子元素主动成为容器成员,称为 Flex 我的项目(flex item)。
● 容器默认存在两根轴:
○ 程度的主轴(main axis):主轴的开始地位(与边框的交叉点)叫做 main start,完结地位叫做 main end;
○ 垂直的穿插轴(cross axis):穿插轴的开始地位叫做 cross start,完结地位叫做 cross end。
● 我的项目默认沿主轴排列。单个我的项目占据的主轴空间叫做 main size,占据的穿插轴空间叫做 cross size。
Flexbox 解析布局逻辑流程,实际上就是解决 Flex 容器和 Flex 我的项目的尺寸、排列方向、对齐形式、比例关系、相对布局我的项目的代码逻辑。
Stretch 实现原理
Flexbox 布局解析主链路
外围算法
Stretch 重点分析
在 Stretch 整个布局算法中,有 9 个次要的环节,接下来咱们重点介绍三个重要的环节,
● 确定 Flex 我的项目的基准值(flex_basis)
● Flex 我的项目的主轴尺寸
● Flex 我的项目的穿插轴尺寸
确定 Flex 我的项目的基准值(flex_basis)
在该阶段,会生成 Flex 我的项目汇合用作逻辑解决,随后会遍历该汇合,给每个 Flex 我的项目的 flex_basis 赋值,Flex 我的项目有了初始值之后会便于后续主轴和穿插轴的解决。
每个 Flex 我的项目的 flex_basis 的值,次要受到以下几个方面影响:
● 如果 flex_basis 曾经有值,则间接应用。
● 如果 align-items 被设置 stretch,那么 flex 我的项目的高度会默认被拉伸到最大元素的高度。
● 如果有设置 aspect-ratio 值,那么须要依据宽度计算出高度赋给 flex_basis,或者依据高度计算出宽度赋给 flex_basis。
确定 Flex 我的项目的主轴尺寸
当有了 flex_basis 之后,须要进一步确定 flex 我的项目主轴尺寸 – target_main_size。
首先要确认以后 flex 容器中曾经应用的空间,以及以后 Flex 容器中残余的空间,而后依据 Flex 我的项目的 Flex 因子来膨胀和扩大 Flex 我的项目的主轴尺寸。
所谓 Flex 因子就是 flex_grow 和 flex_shrink。
如果应用 flex_grow,须要计算 Flex 我的项目的增长比例系数,并联合残余空间计算出 Flex 我的项目的增长值,加上 Flex 我的项目的基准值,作为 Flex 我的项目的主轴尺寸。
Flex 我的项目的增长长度与 flex_grow 数值成正比关系。
if growing {
for target in &mut unfrozen {
let child: &mut FlexItem = target;
if free_space.is_normal() && sum_flex_grow > 0.0 {let grow_after = child.flex_basis + free_space * (self.nodes[child.node].style.flex_grow / sum_flex_grow);
child.target_size.set_main(dir, grow_after);
}
}
}
如果应用 flex_shrink,须要计算 Flex 我的项目的膨胀比例系数,并联合残余空间计算出 Flex 我的项目的膨胀值,加上 Flex 我的项目的基准值,作为 Flex 我的项目的主轴尺寸。
Flex 我的项目膨胀的长度与 flex_shrink 成正比关系。
if shrinking && sum_flex_shrink > 0.0 {
let sum_scaled_shrink_factor: f32 = unfrozen
.iter()
.map(|child: &&mut FlexItem| {let child_style: Style = self.nodes[child.node].style;
child.inner_flex_basis * child_style.flex_shrink
})
.sum();
for target in &mut unfrozen {
let child: &mut FlexItem = target;
let scaled_shrink_factor = child.inner_flex_basis * self.nodes[child.node].style.flex_shrink;
if free_space.is_normal() && sum_scaled_shrink_factor > 0.0 {let shrink_after = child.flex_basis + free_space * (scaled_shrink_factor / sum_scaled_shrink_factor);
child.target_size.set_main(dir, shrink_after)
}
}
}
确定 Flex 我的项目的穿插轴尺寸
穿插轴的尺寸比较复杂一些,其中波及到三个子环节:
● 确定每个 Flex 我的项目的猜想的穿插轴尺寸。
● 确定每行下的 Flex 我的项目穿插轴尺寸最大的高度。
● 依据行的穿插轴高度以及 aspect-ratio、align-self: stretch 等参数,计算出每个 Flex 我的项目穿插轴的理论宽度。
呈现哪些问题,减少哪些新个性
以后 Stretch 也并非完满的,咱们在应用的过程中遇到不少问题
● iOS 的 symbol 笼罩问题导致 crash
● Andriod 端 GC 回收 crash
● 多线程布局计算 crash 问题
● apsect-ratio 属性重定义
● Flex 多层嵌套规定下存在暗藏的问题(flex-shrink/flex-grow)
接下来就针对其中几个问题进行分析,并给出咱们对 Stretch 的改变计划。
iOS 在 32 位机器上 Crash 问题
起因剖析
- Stretch 库是通过 Rust 实现,并通过 cargo 将 Rust 的运行时和 stretch 跨平台编译 iOS armv7,arm64 等平台的动态库集成到 GaiaX 的我的项目中,然而咱们在测试的过程中发现在 armv7 以及 armv7s(32 位机器)上呈现了 crash,crash 在 _modsi3 macros.rs:255,通过剖析 Rust 运行时代码,这个宏是模运算的性能,在编译 stretch 动态库的时候,会将 Rust 运行时一起打包编译,并将须要的宏开展,笼罩 iOS 动静链接 _modesi3 运算导致异样。
- 咱们查看 rust 的底层库 a%b_modesi3 实现,发现在 a%b,在 b 为空的状况下是种未定义的行为,会导致 program panics 行为,在 iOS 零碎体现为 crash 行为。
#[maybe_use_optimized_c_shim]
pub extern "C" fn __modsi3(a: i32, b: i32) -> i32 {a.mod_(b)
}
trait Mod: Int {
/// Returns `a % b`
fn mod_(self, other: Self) -> Self {let s = other >> (Self::BITS - 1);
// NOTE(wrapping_sub) see comment in the `div`
let b = (other ^ s).wrapping_sub(s);
let s = self >> (Self::BITS - 1);
let a = (self ^ s).wrapping_sub(s);
let r = a.unsigned().aborting_rem(b.unsigned());
(Self::from_unsigned(r) ^ s) - s
}
}
fn aborting_rem(self, other: Self) -> Self {unwrap(<Self>::checked_rem(self, other))
}
// 在 a 或 b 为 none 时,unwrap 的值为 none 时会导致 program panics,在 iOS 零碎呈现 crash 问题。
解决方案
- 咱们将 Stretch 打包生成的动态.a 库,并通过工具将 modis3 的 symbol 进行重命名,而后集成到 GaiaX 库中,在生成的 link map 文件中能够发现重命名的几个 symbol 和 iOS 动静库中的 modis3 符号都曾经可能失常 link,。
- 同时咱们也进行大量的机型测试,以及针对性 case 的单元测试来保障其稳定性。
aspect-ratio 问题的修复
aspect-ratio 属性是在 CSS 中用来放弃纵横比 (width/height 的比率)。
起因剖析
Stretch 库的设计是以穿插轴为基准进行解决该属性的,并将其作为 flex_basis 应用,这就导致了如果想要正确的应用这个属性必须做到一下两点:
● flex item 必须嵌套在 flex container 中来进行应用。
● aspect-ratio 的数值,在 container 的排列方向是竖向时,须写成高宽比;在 container 的排列方向是横向时,须写成宽高比。
随着应用 aspect-ratio 的中央越来越多,咱们发现这种须要依赖于 container 的排列方向的计算形式给模板搭建的使用者带来了困惑,同时也升高开发的效率。
解决方案
首先咱们对 aspect-ratio 从新的定义,让其变的更加通俗易懂,依据双端探讨约定的如下规定:
● aspect-ratio 代表宽高比(width / height)。
● 当 flex-item 有确定的宽度时,aspect-ratio 须要以宽度计算出高度。
● 当 flex-height 有确定的高度时,aspect-ratio 须要以高度计算出宽度。
● aspect-ratio 不再受 flex container 排列方向的影响。
因为后期对 Rust 语法不够相熟,再加上 Flexbox 布局解析逻辑非常复杂,整个工作的难度还是十分高的。
采取的策略就是每批改一处逻辑必须通过单元测试保障不会影响到现有逻辑,每减少一处代码都减少相应的测试用例来保障可靠性。
次要针对 Flex 我的项目初始宽度赋值、主轴计算、穿插轴计算时进行拆解解决,适配 aspec-ratio 的逻辑。
例如,在初始宽度赋值时,依据 Flex 我的项目宽度、高度以及宽高比进行计算赋值:
// fix: aspect_ratio_both_dimensions_defined_column
fn get_aspect_ratio_size(child_style: &Style, target_size: Size<Number>) -> Size<Number> {
return Size {width: Forest::get_aspect_ratio_width(child_style, target_size),
height: Forest::get_aspect_ratio_height(child_style, target_size),
};
}
// fix: aspect_ratio_both_dimensions_defined_column
fn get_aspect_ratio_height(child_style: &Style, target_size: Size<Number>) -> Number {
// 若有定义宽度,且存在比例关系,那么应用高度计算宽度
if target_size.width.is_defined() && child_style.aspect_ratio.is_defined() {let width = target_size.width.or_else(0.0);
let aspect_ratio = child_style.aspect_ratio.or_else(0.0);
return Number::Defined(width / aspect_ratio);
}
return target_size.height;
}
fn get_aspect_ratio_width(child_style: &Style, target_size: Size<Number>) -> Number {
// 若没有定义宽度,并且有定义高度,且存在比例关系,那么应用高度计算宽度
if !target_size.width.is_defined() && target_size.height.is_defined() && child_style.aspect_ratio.is_defined() {let height = target_size.height.or_else(0.0);
let aspect_ratio = child_style.aspect_ratio.or_else(0.0);
return Number::Defined(height * aspect_ratio);
}
return target_size.width;
}
另外在计算穿插轴尺寸时,同时也须要思考到很多属性之间(stretch、aspect-ratio、flex-basis、flex-grow、flex-shrink)的关联关系等,争取将影响降到最低。
if is_row && child_style.aspect_ratio.is_defined() && node_size.height.is_defined() && child_style.flex_shrink > 0.0 {let final_cross = child.hypothetical_inner_size.cross(dir).maybe_min(node_size.height);
if !child_style.size.width.is_defined() && !child_style.min_size.width.is_defined() && !child_style.max_size.width.is_defined() {
// fix: aspect_ratio_height_as_flex_basis
// fix: aspect_ratio_flex_shrink
let desire_height = child.target_size.width / child_style.aspect_ratio.or_else(0.0);
desire_height
} else if !child_style.size.width.is_defined() && child_style.min_size.width.is_defined() && !child_style.max_size.width.is_defined() {
// fix: aspect_ratio_flex_shrink_2
let desire_height = child.target_size.width / child_style.aspect_ratio.or_else(0.0);
final_cross.maybe_min(Number::Defined(desire_height))
} else if child_style.size.width.is_defined() {
// fix: aspect_ratio_width_height_flex_grow_row
let desire_height = child.target_size.width / child_style.aspect_ratio.or_else(0.0);
desire_height
} else {final_cross}
} else {let final_cross = child.hypothetical_inner_size.cross(dir);
final_cross
}
线程问题
起因剖析
因为 Stretch 库的实现是非线程平安的。Nodes 和 Forest 都是通过 Map 以及 Array 进行存储,在多线程状况下动静构建 Forest,以及创立和移除子 Node 节点时候极易呈现数据拜访平安的问题导致 crash。
//stretch 初始化
pub fn with_capacity(capacity: usize) -> Self {
Self {id: INSTANCE_ALLOCATOR.allocate(),
nodes: id::Allocator::new(),
nodes_to_ids: crate::sys::new_map_with_capacity(capacity),
ids_to_nodes: crate::sys::new_map_with_capacity(capacity),
forest: Forest::with_capacity(capacity),
}
}
// 增加子节点
pub fn add_child(&mut self, node: Node, child: Node) -> Result<(), Error> {let node_id = self.find_node(node)?;
let child_id = self.find_node(child)?;
self.forest.add_child(node_id, child_id);
Ok(())
}
// 移除子节点
pub fn remove_child(&mut self, node: Node, child: Node) -> Result<Node, Error> {let node_id = self.find_node(node)?;
let child_id = self.find_node(child)?;
let prev_id = unsafe {self.forest.remove_child(node_id, child_id) };
Ok(self.ids_to_nodes[&prev_id])
}
解决方案
为了防止批改 Stretch 带来其余未知的影响,双端在 Stretch 下层进行单例化,并对所有的节点操作进行加锁拜访,以及动静更新 Style 指针的平安拜访。
// GXStretch
+ (instancetype)stretch{
static GXStretch *stretch = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{if (nil == stretch) {stretch = [[GXStretch alloc] init];
}
});
return stretch;
}
// 增加 child
- (void)addChild:(void *)child forNode:(void *)node{dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
stretch_node_add_child(_stretchptr, node, child);
dispatch_semaphore_signal(_semaphore);
}
// 更新 rust 指针
- (void)updateRustPtr{
// 先开释原有指针
if ([self isValidPtr:_rustptr]) {_prevRustptr = _rustptr;}
// 再生成新的指针
_rustptr = stretch_style_create(...);
}
// 开释上次的 rust 指针
- (void)freePrevRustptr{if ([self isValidPtr:_prevRustptr]) {stretch_style_free(_prevRustptr);
_prevRustptr = NULL;
}
}
问题验证
在对于 Stretch 减少新个性的同时,为了保障代码安全性和可靠性,咱们减少了大量的测试用例,笼罩绝大多数的场景对批改点的验证。
例如,在穿插轴确定的状况下,来验证通过 apsect-ratio 来计算布局:
#[test]
fn aspect_ratio_cross_defined() {let mut stretch = Stretch::new();
let root = stretch.new_node(Style {size: Size { width: Dimension::Points(100.0), height: Dimension::Points(100.0) },
..Default::default()}, &[]).unwrap();
let root_child0 = stretch.new_node(Style {size: Size { width: Dimension::Points(50.0), ..Default::default()},
aspect_ratio: Number::Defined(1.0),
..Default::default()}, &[]).unwrap();
stretch.add_child(root, root_child0).unwrap();
stretch.compute_layout(root, Size { width: Number::Defined(375.0), height: Number::Defined(1000.0) }).unwrap();
assert_eq!(stretch.layout(root_child0).unwrap().size.width, 50.0);
assert_eq!(stretch.layout(root_child0).unwrap().size.height, 50.0);
}
得益于 Rust 语言的安全性,以及麻利单元测试和丰盛的类型零碎和所有权模型,在编译期就可能打消各种各样的谬误,也防止了因为不相熟语法而导致编写谬误,在此基础上,可能顺利跑通所有测试用例的代码就能保障在应用期间的可靠性。
总结与瞻望
Stretch 开源我的项目原开发团队已不在保护,为了保障 GaiaX 我的项目的迭代诉求,团队岂但 fork 了源码,并自研新增了大量新个性。在这个过程中,咱们不仅对 Stretch 库有了全面的理解,也同时深刻到 RUST 语言中,这个过程既苦楚又享受,置信宽广开发同学在编程职业生涯中,也有过相似的经验。
随着 GaiaX 在阿里娱乐业务及开源社区越来越多的利用,双端须要反对的容器及技术能力与诉求也越来越多,仅凭借 GaiaX 团队一己之力,很难疾速响应社区开发者及娱乐业务自身的双线需要。因而,团队心愿通过开源的形式,让更多的开发者参加进来,借助社区力量,让更多有跨端动态化技术诉求的集体和个人在技术上受害。
团队介绍
咱们来自 优酷产品技术与翻新核心利用技术部 ,从日常工作中实在存在的研发效力及痛点问题登程,咱们自研推出了 GaiaX 动静模板技术体系,指标是解决多端卡片化 UI 组件的研发效力问题,帮忙晋升开发者体验及效率。目前 GaiaX 我的项目曾经在 GitHub 开源 【https://github.com/alibaba/GaiaX】,也殷切的心愿宽广挪动端及前端技术爱好者与咱们进行技术交换与共建。
系列文章
《GaiaX 开源解读》系列文章预报如下,欢送继续关注:
第一篇:《GaiaX 开源解读 | 基于优酷业务特色的跨平台技术》
第二篇:《GaiaX 开源解读 | 跨端动态化模板引擎详解,看完你也能写一个》
第三篇:《GaiaX 开源解读 | 给 Stretch(Rust 编写的 Flexbox 布局引擎)新增个性,我掉了好多头发》
第四篇:《GaiaX 开源解读 | 表达式作为逻辑动态化的根底,咱们是如何设计的》
第五篇:《GaiaX 开源解读 | 向经典致敬 ReactNaitve 与 GaiaX 渲染核心技术剖析》
第六篇:《GaiaX 开源解读 | 为了保障双端一致性,咱们做了哪些致力》
第七篇:《GaiaX 开源解读 | 一条龙的模板研发体系,你不来看看么》