给Stretch(Rust编写的Flexbox布局引擎)新增特性,我掉了好多头发 | GaiaX开源解读

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 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,这也是业内的主流技术选型。

● 包大小体积

● 支持多线程的局部计算

● 支持跨平台的能力(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问题

原因分析

  1. Stretch库是通过Rust实现,并通过cargo将Rust的运行时和stretch跨平台编译iOS armv7,arm64等平台的静态库集成到GaiaX的项目中,但是我们在测试的过程中发现在 armv7以及armv7s(32位机器)上出现了crash,crash在 _modsi3 macros.rs:255 ,经过分析Rust运行时代码,这个宏是模运算的功能,在编译stretch静态库的时候,会将Rust运行时一起打包编译,并将需要的宏展开,覆盖iOS动态链接 _modesi3运算导致异常。
  2. 我们查看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问题。

解决方案

  1. 我们将Stretch打包生成的静态.a库,并通过工具将modis3的symbol进行重命名,然后集成到GaiaX库中,在生成的link map文件中可以发现重命名的几个symbol和iOS动态库中的modis3符号都已经能够正常link,。
  2. 同时我们也进行大量的机型测试,以及针对性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_columnfn 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_columnfn 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团队一己之力,很难快速响应社区开发者及文娱业务本身的双线需求。因此,团队希望通过开源的方式,让更多的开发者参与进来,借助社区力量,让更多有跨端动态化技术诉求的个人和团体在技术上受益。


相关文章
|
6月前
|
Rust 区块链
学Rust不学Cargo,等于没学Rust:features特性详解
在 Rust 中,Cargo 的 "features" 是一种条件编译机制,允许在编译 crate 时编译部分代码。这样可以在一个 crate 中提供多个功能,并根据需要选择性地启用或禁用这些功能。
202 1
|
6月前
|
Rust Linux iOS开发
【一起学Rust | 开源项目】效率提升工具espanso——在日常输入中使用代码提示
【一起学Rust | 开源项目】效率提升工具espanso——在日常输入中使用代码提示
119 0
|
6月前
|
存储 Rust 程序员
【一起学Rust | 基础篇 | rust新特性】Rust 1.65.0——泛型关联类型、let-else语句
【一起学Rust | 基础篇 | rust新特性】Rust 1.65.0——泛型关联类型、let-else语句
103 0
|
20天前
|
Rust 安全 Java
编程语言新宠:Rust语言的特性、优势与实战入门
【10月更文挑战第27天】Rust语言以其独特的特性和优势在编程领域迅速崛起。本文介绍Rust的核心特性,如所有权系统和强大的并发处理能力,以及其性能和安全性优势。通过实战示例,如“Hello, World!”和线程编程,帮助读者快速入门Rust。
42 1
|
21天前
|
Rust 安全 编译器
编程语言新宠:Rust语言的特性、优势与实战入门
【10月更文挑战第26天】Rust语言诞生于2006年,由Mozilla公司的Graydon Hoare发起。作为一门系统编程语言,Rust专注于安全和高性能。通过所有权系统和生命周期管理,Rust在编译期就能消除内存泄漏等问题,适用于操作系统、嵌入式系统等高可靠性场景。
34 2
|
3月前
|
Rust JavaScript 前端开发
Rust! 无VDom! 尤雨溪解析 Vue.js 2024 新特性
Rust! 无VDom! 尤雨溪解析 Vue.js 2024 新特性
|
3月前
|
Rust 安全 Java
神秘编程世界惊现独特规则,Rust 核心特性究竟隐藏着怎样的秘密?快来一探究竟!
【8月更文挑战第31天】在编程领域,Rust 以所有权、借用和生命周期等核心特性著称,提供强大的内存安全保障。所有权确保每个值有唯一所有者并自动管理内存,避免手动管理内存带来的问题。借用则允许临时访问值而不转移所有权,确保内存安全访问。生命周期机制在编译时保证引用的有效性,无需运行时检查。通过对比 Rust 与 C/C++ 的示例,可以更清晰地理解这些特性带来的优势。
31 1
|
3月前
|
开发者 C# Android开发
震惊!Xamarin 跨平台开发优势满满却也挑战重重,代码复用、熟悉语言与性能优势并存,学习曲线与差异处理何解?
【8月更文挑战第31天】Xamarin 与 C# 结合,为移动应用开发带来高效跨平台解决方案,使用单一语言和框架即可构建 iOS、Android 和 Windows 原生应用。本文通过问答形式探讨 Xamarin 和 C# 如何塑造移动开发的未来,并通过示例代码展示其实际应用。Xamarin 和 C# 的组合不仅提高了开发效率,还支持最新的移动平台功能,帮助开发者应对未来挑战,如物联网、人工智能和增强现实等领域的需求。
53 0
|
5月前
|
Rust 安全 开发者
探索Rust语言的内存安全特性
【6月更文挑战第8天】Rust语言针对内存安全问题提供了创新解决方案,包括所有权系统、借用规则和生命周期参数。所有权系统确保值与其所有者绑定,防止内存泄漏;借用规则保证同一时间只有一个可变引用或多个不可变引用,消除数据竞争和野指针;生命周期参数则强化了引用的有效范围,提升安全性。通过这些特性,Rust帮助开发者编写出更健壮、安全的高性能软件,有望成为系统编程领域的领头羊。
|
6月前
|
Rust 安全 程序员
Rust vs Go:解析两者的独特特性和适用场景
在讨论 Rust 与 Go 两种编程语言哪种更优秀时,我们将探讨它们在性能、简易性、安全性、功能、规模和并发处理等方面的比较。同时,我们看看它们有什么共同点和根本的差异。现在就来看看这个友好而公平的对比。