Chromium Views FlexLayout 的重生之路

2 阅读11分钟

关注微信公众号《V的开源笔记》,第一时间查看后续更新。

读者朋友们大家好呀,之前我已经写过一篇《我的Chromium Committer之路》

在文中,我只是简单的提了一下我如何针对 FlexLayout 进行重构,然而并没有详细 FlexLayout 的新算法。一些朋友和我说,我写的太简单了,想看更加详细的解析。

在本文当中,我将详细针对 FlexLayout 的实现进行讲解。由于本文是一篇独立的文章,所以可能有部分内容在之前的文章中已经提过,在这里如果给读者朋友带来不好的体验深感抱歉,你可以直接点击这里跳转到正文

背景

在 23 年 9 月份。我闲的无聊想给自己找点事情做,于是我就去 Chromium 社区找看看有没有我能修复的 bug. 于是我发现了 Keren Zhu 发起的无界布局向有界布局的重构,本着对 Chromium 技术的向往,以及提升自己能力的想法。我向 Keren 请求一起合作完成这个重构的事情。

Chromium 社区不愧是我认为的全球做的最好的开源社区之一(个人想法),他们欢迎任何一个免费的打工仔🐶为他们修复Bug.

所以,有了我后面对 FlexLayout 算法的重构。

FlexLayout 为什么需要重构?

首先,在社区里。不是你说你想重构就可以重构的,旧 FlexLayout 从 2018 年开始一直服务到2023年,业务依赖它稳定运行了5年之久,岂是你想换就换的。那么它遇到了一个什么问题呢? 图片 毫无疑问布局总是最容易出 bug 的地方之一. 不管是 blink 还是 WebCore, 总之只要涉及到布局的地方都是 bug 一堆,一个没有良好 API 设计的布局系统,更是开发维护人员的噩梦。

在和 Keren 合作的第一步,我就遇到了一个问题, FlexLayout 在新的有界布局 API 下, 表现出了异常行为。 FlexLayout下的多行标签(文本)在新的有界布局API会导致一个视图的关闭按钮左偏。 图片 为了解决这个问题,我前前后后花费了一个月的时间去修复(因为平时还有工作,所以能够自由使用的时间不多,都是在周末和下班时间去调查)。最后发现是现有的FlexLayout 算法是针对无界布局而设计的。在编写时没有考虑有界布局这种情况,自然也无法正常工作,没有办法,我只能重构了。

编写新的算法

感谢 Keren 在发起重构时提到:“blink 内部布局也是使用有界布局,而不是无界布局”。 我认为这应该是一个普遍错误,所以我开始学习 blink Flexiable 布局算法。嗯,超级复杂。 所以去学习了下 W3C Flexiable 算法标准。 好,总算是人看的了。同时也了解了下 Blink 最新的布局系统 LayoutNG

接下来让我们开始正文。

FlexLayout 算法实现

要说算法的实现,我们不能只单纯的说他的实现,我们还需要说,这个算法是干啥的:

  1. 我们可以 水平/垂直 排列我们的子视图

  2. 我们可以 水平/垂直 对齐我们的子视图

  3. 我们的父视图可以有 padding 和 margin/broder.

  4. 我们的子视图也可以有 margin 和 padding, 并且我们还可以压缩 margin.

  5. 我们的子视图可以有布局的优先级,优先级高的视图先布局,优先级低的视图后布局

  6. 我们可以给每个子视图指定自定义的布局规则(一个给你一个尺寸和一些规则,返回一个尺寸的函数)。这个布局规则只要满足上述的要求,可以是任意的函数。

  7. 如果一个视图在布局在分配的空间过小时,我们需要将视图的可见性设置为不可见而不是裁剪,这是和 BoxLayout 最大的不同。而且这是非常重要的!!!也是 FlexLayout 最重要的一个特性。我在重构时,Keren 特意和我强调这一点。

    可能单纯看说明,可能感觉不出这里有多复杂。我可以说,如果没有这个特性,FlexLayout 的复杂度可以降低很多. 不过这样的话,他就和 BoxLayout 的特性相同了

    一个简单的例子,如果一个子视图是不可见的,那么这个子视图的 margin 同样应该忽略。如果你再考虑一下,每个子视图的margin大小不相等,且本身左右也不相等。 那么这就意味着如果在分配过程中存在一个视图被标记为不可见,基本上整个视图的算法分配就失效了,需要重新分配。

  8. 每个子视图都可以获取他的可用空间。eg: Chromium 标签页排列就是依赖这个,在标签页过多时,他会压缩每个标签的宽度。

是不是感觉比 css flex 复杂多了。实际上确实是的。至少在一维上比 css flex 语义复杂多了 (不过css flex 是二维的)。

和上一篇 BoxLayout 重构的文章 一样,先贴代码。 看不懂的话可以看后面一步一步的解析。

ProposedLayout FlexLayout::CalculateProposedLayout(
    const SizeBounds& size_bounds) const {
  FlexLayoutData data;

  if (include_host_insets_in_layout()) {
    // Combining the interior margin and host insets means we only have to set
    // the margin value; we'll leave the insets at zero.
    data.interior_margin =
        Normalize(orientation(), interior_margin() + host_view()->GetInsets());
  } else {
    data.host_insets = Normalize(orientation(), host_view()->GetInsets());
    data.interior_margin = Normalize(orientation(), interior_margin());
  }
  NormalizedSizeBounds bounds = Normalize(orientation(), size_bounds);
  bounds.Inset(data.host_insets);
  bounds.set_cross(
      std::max<SizeBound>(bounds.cross(), minimum_cross_axis_size()));

  // The main idea of the new algorithm comes from css flexbox:
  // https://www.w3.org/TR/css-flexbox-1/#box-manip Based on the css flexbox
  // algorithm, combined with the old algorithm. Redesigned new algorithm.
  //
  // But there are some differences:
  // 1. In css flex box, there is no situation where elements suddenly become
  //    invisible during layout. But in views it will.
  // 2. CSS flex box does not have multiple layout orders. So we need to make
  //    special adjustments here
  //
  // Other more specific details will be explained in subsequent gazes.

  // Populate the child layout data vectors and the order-to-index map.
  FlexOrderToViewIndexMap order_to_view_index;
  InitializeChildData(bounds, data, order_to_view_index);

  // Do the initial layout update, calculating spacing between children.
  ChildViewSpacing child_spacing(
      base::BindRepeating(&FlexLayout::CalculateChildSpacing,
                          base::Unretained(this), std::cref(data)));
  UpdateLayoutFromChildren(bounds, data, child_spacing);

  // We now have a layout with all views at the absolute minimum size and with
  // those able to drop out dropped out. Now apply flex rules.
  //
  // This is done in two primary phases:
  // 1. If there is insufficient space to provide each view with its preferred
  //    size, the deficit will be spread across the views that can flex, with
  //    any views that bottom out getting their minimum and dropping out of the
  //    calculation.
  // 2. If there is excess space after the first phase, it is spread across all
  //    of the remaining flex views that haven't dropped out.
  //
  // The result of this calculation is extremely *correct* but it is possible
  // there are some pathological cases where the cost of one of the steps is
  // quadratic in the number of views. Again, this is unlikely and numbers of
  // child views tend to be small enough that it won't matter.

  CalculateNonFlexAvailableSpace(
      std::max<SizeBound>(0, bounds.main() - data.total_size.main()),
      order_to_view_index, child_spacing, data);

  // If there are multiple orders. We need to first limit the maximum size to
  // the preferred size. To ensure that subsequent views have a chance to reach
  // the preferred size
  if (order_to_view_index.size() > 1) {
    std::vector<NormalizedSize> backup_size(data.num_children());
    for (size_t i = 0; i < data.num_children(); ++i) {
      FlexChildData& flex_child = data.child_data[i];
      backup_size[i] = flex_child.maxiumize_size;
      flex_child.maxiumize_size = flex_child.preferred_size;
    }
    AllocateFlexItem(bounds, order_to_view_index, data, child_spacing, true);
    for (size_t i = 0; i < data.num_children(); ++i) {
      FlexChildData& flex_child = data.child_data[i];
      flex_child.maxiumize_size = backup_size[i];
    }
  }
  AllocateFlexItem(bounds, order_to_view_index, data, child_spacing, true);

  // This is a different place too. Because css flexbox does not have dimensions
  // that can be changed freely: Custom flex rules.
  //
  // So we may have unallocated space.
  AllocateRemainingSpaceIfNeeded(bounds, order_to_view_index, data,
                                 child_spacing);

  // Calculate the size of the host view.
  NormalizedSize host_size = data.total_size;
  host_size.Enlarge(data.host_insets.main_size(),
                    data.host_insets.cross_size());
  data.layout.host_size = Denormalize(orientation(), host_size);

  // Size and position the children in screen space.
  CalculateChildBounds(size_bounds, data);

  return data.layout;
}

同样的 FlexLayout 算法也分为两个阶段。 第一阶段,无边界条件下的自由布局,让我们给每个子视图他们想要的宽度。第二阶段,有界情况下的子视图尺寸调整。

算法拆解

ProposedLayout FlexLayout::CalculateProposedLayout(
    const SizeBounds& size_bounds) const {
  FlexLayoutData data;

  if (include_host_insets_in_layout()) {
    // Combining the interior margin and host insets means we only have to set
    // the margin value; we'll leave the insets at zero.
    data.interior_margin =
        Normalize(orientation(), interior_margin() + host_view()->GetInsets());
  } else {
    data.host_insets = Normalize(orientation(), host_view()->GetInsets());
    data.interior_margin = Normalize(orientation(), interior_margin());
  }
  NormalizedSizeBounds bounds = Normalize(orientation(), size_bounds);
  bounds.Inset(data.host_insets);
  bounds.set_cross(
      std::max<SizeBound>(bounds.cross(), minimum_cross_axis_size()));

  // The main idea of the new algorithm comes from css flexbox:
  // https://www.w3.org/TR/css-flexbox-1/#box-manip Based on the css flexbox
  // algorithm, combined with the old algorithm. Redesigned new algorithm.
  //
  // But there are some differences:
  // 1. In css flex box, there is no situation where elements suddenly become
  //    invisible during layout. But in views it will.
  // 2. CSS flex box does not have multiple layout orders. So we need to make
  //    special adjustments here
  //
  // Other more specific details will be explained in subsequent gazes.

  // Populate the child layout data vectors and the order-to-index map.
  FlexOrderToViewIndexMap order_to_view_index;
  InitializeChildData(bounds, data, order_to_view_index);

  ...
}

注意看这里的注释,这是我为了方便 reviewer 审查以及其他感兴趣的人查看编写的,这里说明了我算法重构的来源,以及为我算法的正确性添加一些支撑。

这里写到,我们的新算法主要来源于 W3C Flexiable 标准算法。但是它结合了 Views 下 FlexLayout 本身的一些独特特性,我们编写了这套新的算法。

我们来看InitializeChildData(bounds, data, order_to_view_index); 函数。 不同于 BoxLayout 算法的直接一笔带过初始化阶段。 FlexLayout 算法的初始化阶段就开始表明了他的复杂性。

我们组要来看这一段:

W3C 标准算法 $9.2.3.C:

If the used flex basis is content or depends on its available space, and the flex container is being sized under a min-content or max-content constraint (e.g. when performing automatic table layout [CSS21]), size the item under that constraint. The flex base size is the item’s resulting main size.

InitializeChildData(bounds, data, order_to_view_index); 函数就是根据这一段说明而来。

void FlexLayout::InitializeChildData(
    const NormalizedSizeBounds& bounds,
    FlexLayoutData& data,
    FlexOrderToViewIndexMap& flex_order_to_index) const {
  // Step through the children, creating placeholder layout view elements
  // and setting up initial minimal visibility.
  const bool main_axis_bounded = bounds.main().is_bounded();
  for (View* child : host_view()->children()) {
    if (!IsChildIncludedInLayout(child))
      continue;

    ....

    // According to css flexbox:
    // https://www.w3.org/TR/css-flexbox-1/#algo-main-item $9.2.3 All layout
    // algorithms in views should follow the rule listed in $9.2.3, subsection
    // 'C'. So here the basic size is set according to the C rule.
    flex_child.preferred_size =
        GetPreferredSizeForRule(flex_child.flex.rule(), child, available_cross);
    flex_child.miniumize_size =
        GetCurrentSizeForRule(flex_child.flex.rule(), child,
                              NormalizedSizeBounds(0, available_cross));
    flex_child.maxiumize_size = GetCurrentSizeForRule(
        flex_child.flex.rule(), child,
        NormalizedSizeBounds(bounds.main(), available_cross));

    data.SetCurrentSize(view_index, main_axis_bounded
                                        ? flex_child.miniumize_size
                                        : flex_child.preferred_size);
    
    ....


    if (main_axis_bounded) {
      flex_child.flex_base_content_size = std::min<NormalizedSize>(
          std::max<NormalizedSize>(flex_child.miniumize_size,
                                   flex_child.preferred_size),
          flex_child.maxiumize_size);
    } else {
      flex_child.flex_base_content_size = flex_child.maxiumize_size;
    }
  }
}

在 Views 的 FlexLayout 算法中,我们总是认为视图是可以计算最大最小值的。这也就是说,我们符合 W3C 标准算法中的 $9.2.3 条,第C项规则。所以我们先将每个子视图的最大最小值,以及首选值计算出来。并且我们将子视图的基本内容大小设置为首选大小(如果无边界约束,那么每个视图都使用最大宽度)。

ProposedLayout FlexLayout::CalculateProposedLayout(
    const SizeBounds& size_bounds) const {
  ...
  
  // Populate the child layout data vectors and the order-to-index map.
  FlexOrderToViewIndexMap order_to_view_index;
  InitializeChildData(bounds, data, order_to_view_index);

  // Do the initial layout update, calculating spacing between children.
  ChildViewSpacing child_spacing(
      base::BindRepeating(&FlexLayout::CalculateChildSpacing,
                          base::Unretained(this), std::cref(data)));
  UpdateLayoutFromChildren(bounds, data, child_spacing);

  // We now have a layout with all views at the absolute minimum size and with
  // those able to drop out dropped out. Now apply flex rules.
  //
  // This is done in two primary phases:
  // 1. If there is insufficient space to provide each view with its preferred
  //    size, the deficit will be spread across the views that can flex, with
  //    any views that bottom out getting their minimum and dropping out of the
  //    calculation.
  // 2. If there is excess space after the first phase, it is spread across all
  //    of the remaining flex views that haven't dropped out.
  //
  // The result of this calculation is extremely *correct* but it is possible
  // there are some pathological cases where the cost of one of the steps is
  // quadratic in the number of views. Again, this is unlikely and numbers of
  // child views tend to be small enough that it won't matter.

  CalculateNonFlexAvailableSpace(
      std::max<SizeBound>(0, bounds.main() - data.total_size.main()),
      order_to_view_index, child_spacing, data);

  ...
}

这里计算不可以发生尺寸变化的视图。首先把他们剔除出去,我们不需要在后续的步骤中关心他们。接下来我们看这个算法最关键的点,也是 W3C 标准算法的应用:

ProposedLayout FlexLayout::CalculateProposedLayout(
    const SizeBounds& size_bounds) const {
  ...

  // If there are multiple orders. We need to first limit the maximum size to
  // the preferred size. To ensure that subsequent views have a chance to reach
  // the preferred size
  if (order_to_view_index.size() > 1) {
    std::vector<NormalizedSize> backup_size(data.num_children());
    for (size_t i = 0; i < data.num_children(); ++i) {
      FlexChildData& flex_child = data.child_data[i];
      backup_size[i] = flex_child.maxiumize_size;
      flex_child.maxiumize_size = flex_child.preferred_size;
    }
    AllocateFlexItem(bounds, order_to_view_index, data, child_spacing, true);
    for (size_t i = 0; i < data.num_children(); ++i) {
      FlexChildData& flex_child = data.child_data[i];
      flex_child.maxiumize_size = backup_size[i];
    }
  }
  AllocateFlexItem(bounds, order_to_view_index, data, child_spacing, true);

  // This is a different place too. Because css flexbox does not have dimensions
  // that can be changed freely: Custom flex rules.
  //
  // So we may have unallocated space.
  AllocateRemainingSpaceIfNeeded(bounds, order_to_view_index, data,
                                 child_spacing);

  ...
}

这里最重要的就是 AllocateFlexItem 函数的实现。 他的作用就是调整每个视图实际应该分配的空间。而在AllocateFlexItem 中,最重要的就是 ResolveFlexibleLengths函数:


void FlexLayout::AllocateFlexItem(const NormalizedSizeBounds& bounds,
                                  const FlexOrderToViewIndexMap& order_to_index,
                                  FlexLayoutData& data,
                                  ChildViewSpacing& child_spacing,
                                  bool skip_zero_preferred_size_view) const {
  for (const auto& flex_elem : order_to_index) {
    ...

    // Solve the problem of flexible size allocation.
    while (ResolveFlexibleLengths(bounds, remaining_free_space, view_indices,
                                  data, child_spacing)) {
      continue;
    }

    ...
  }
}

ResolveFlexibleLengths

Resolve Flexible Lengths是 W3C 标准算法 $9.7 节的标题,它的实现主体,也就是针对9.7的一个代码实现。所以这里其实只要阅读 9.7 节就可以了。

9.7. Resolving Flexible Lengths To resolve the flexible lengths of the items within a flex line:

  1. Determine the used flex factor. Sum the outer hypothetical main sizes of all items on the line. If the sum is less than the flex container’s inner main size, use the flex grow factor for the rest of this algorithm; otherwise, use the flex shrink factor.
  2. Size inflexible items. Freeze, setting its target main size to its hypothetical main size…
    • any item that has a flex factor of zero
    • if using the flex grow factor: any item that has a flex base size greater than its hypothetical main size
    • if using the flex shrink factor: any item that has a flex base size smaller than its hypothetical main size
  3. Calculate initial free space. Sum the outer sizes of all items on the line, and subtract this from the flex container’s inner main size. For frozen items, use their outer target main size; for other items, use their outer flex base size.
  4. Loop: a. Check for flexible items. If all the flex items on the line are frozen, free space has been distributed; exit this loop. b. Calculate the remaining free space as for initial free space, above. If the sum of the unfrozen flex items’ flex factors is less than one, multiply the initial free space by this sum. If the magnitude of this value is less than the magnitude of the remaining free space, use this as the remaining free space. c. Distribute free space proportional to the flex factors.
    • If the remaining free space is zero
      • Do nothing.
    • If using the flex grow factor
      • Find the ratio of the item’s flex grow factor to the sum of the flex grow factors of all unfrozen items on the line. Set the item’s target main size to its flex base size plus a fraction of the remaining free space proportional to the ratio.
    • If using the flex shrink factor
      • For every unfrozen item on the line, multiply its flex shrink factor by its inner flex base size, and note this as its scaled flex shrink factor. Find the ratio of the item’s scaled flex shrink factor to the sum of the scaled flex shrink factors of all unfrozen items on the line. Set the item’s target main size to its flex base size minus a fraction of the absolute value of the remaining free space proportional to the ratio. Note this may result in a negative inner main size; it will be corrected in the next step.
    • Otherwise
      • Do nothing.
    d. Fix min/max violations. Clamp each non-frozen item’s target main size by its used min and max main sizes and floor its content-box size at zero. If the item’s target main size was made smaller by this, it’s a max violation. If the item’s target main size was made larger by this, it’s a min violation. e. Freeze over-flexed items. The total violation is the sum of the adjustments from the previous step (clampedsizeunclampedsize)\sum(clamped size - unclamped size). If the total violation is:
    • Zero
      • Freeze all items.
    • Positive
      • Freeze all the items with min violations.
    • Negative
      • Freeze all the items with max violations.
    Return to the start of this loop. f. Set each item’s used main size to its target main size.

这里是一个C++ 实现:

bool FlexLayout::ResolveFlexibleLengths(const NormalizedSizeBounds& bounds,
                                        SizeBound& remaining_free_space,
                                        ChildIndices& child_list,
                                        FlexLayoutData& data,
                                        ChildViewSpacing& child_spacing) const {
  if (!remaining_free_space.is_bounded()) {
    return false;
  }

  // Assume all subviews are visible. Calculate the total space change required
  // to adjust from the current size to the main size.
  ChildViewSpacing proposed_spacing(child_spacing);
  int delta = 0;
  for (size_t child_index : child_list) {
    const FlexChildData& flex_child = data.child_data[child_index];
    delta += proposed_spacing.GetTotalSizeChangeForNewSize(
        child_index, flex_child.current_size.main(),
        flex_child.flex_base_content_size.main());
    if (!proposed_spacing.HasViewIndex(child_index)) {
      proposed_spacing.AddViewIndex(child_index);
    }
  }
  SizeBound temp_remaining_free_space = remaining_free_space - delta;

  // According to css flexbox:
  // https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths $9.7
  // The following algorithm mainly comes from it.

  int flex_total = CalculateFlexTotal(data, child_list);
  ChildIndices min_violations;
  ChildIndices max_violations;
  int total_violation = 0;
  for (auto view_index : child_list) {
    FlexChildData& flex_child = data.child_data[view_index];
    // We think it's already in the main sizes. Adjust according to remaining
    // space.
    SizeBound child_size = flex_child.flex_base_content_size.main();

    const int weight = flex_child.flex.weight();
    DCHECK_GT(weight, 0);
    const SizeBound extra_space =
        base::ClampFloor(temp_remaining_free_space.value() * weight /
                             static_cast<float>(flex_total) +
                         0.5f);
    child_size += extra_space;

    // $9.7.4.C Constrain new dimensions under maximum and minimum dimensions.
    const NormalizedSize new_size =
        ClampSizeToMinAndMax(data, view_index, child_size);
    flex_child.pending_size = new_size;

    int violation = new_size.main() - child_size.value();
    if (violation > 0) {
      min_violations.push_back(view_index);
    } else if (violation < 0) {
      max_violations.push_back(view_index);
    }
    total_violation += violation;
    temp_remaining_free_space -= extra_space;
    flex_total -= weight;
  }

  // $9.7.4.d: Fix min/max violations.
  if (total_violation) {
    FreezeViolations(child_list, remaining_free_space,
                     total_violation > 0 ? min_violations : max_violations,
                     child_spacing, data);
    return true;
  } else {
    ChildIndices temp_list(child_list);
    return FreezeViolations(child_list, remaining_free_space, temp_list,
                            child_spacing, data);
  }
}

最后,我们回到函数最开始的地方。这里就是最后的调整以及对齐的实现了。比较简单,就不在详细讲解了。

ProposedLayout FlexLayout::CalculateProposedLayout(
    const SizeBounds& size_bounds) const {
  ...
  
  // Calculate the size of the host view.
  NormalizedSize host_size = data.total_size;
  host_size.Enlarge(data.host_insets.main_size(),
                    data.host_insets.cross_size());
  data.layout.host_size = Denormalize(orientation(), host_size);

  // Size and position the children in screen space.
  CalculateChildBounds(size_bounds, data);

  return data.layout;
}

总结

FlexLayout 由于独特的特性,在算法复杂度上,最差情况下为达到了 O(n2)O(n^2) 其主要就是来源于下面这行语句以及第7个需求点。 视图空间不足时隐藏,这带来了最大的算法性能退化。登录主仓库之后也受到了非常多来自 google 内部的挑战。

    // Solve the problem of flexible size allocation.
    while (ResolveFlexibleLengths(bounds, remaining_free_space, view_indices,
                                  data, child_spacing)) {
      continue;
    }

正好最近重构的 BoxLayout 可以作为在不需要这一特性的情况下的更好选择。 未来,应该会有更多的布局回归到 BoxLayout 而不是使用 FlexLayout(在我没重构BoxLayout 之前,Chomium社区已经在推荐使用FlexLayout 而不是BoxLayout,因为重构前的BoxLayout存在一些长期存在的bug).

最后,感谢读者耐心读到这里。能够完整读完,读到这里的,应该不多。😂

再次感谢观看。

拜拜。