关于flutter:淘特-Flutter-流式场景的深度优化

41次阅读

共计 14674 个字符,预计需要花费 37 分钟才能阅读完成。

作者:江泽军(眞意)

淘特在很多业务场景都应用了 Flutter,加上业务场景自身具备肯定的复杂性,使得 Flutter 在低端机流式场景的滑动浏览过程中卡顿、跳帧比照应用原生 (Android/iOS) 开发显著。通过剖析业务层在 Flutter 渲染流程中的每个阶段存在的性能问题进行了一系列的深度优化后,均匀帧率曾经达到 50 帧之上超过了原生的体现,但卡顿率仍然达不到最佳的体验成果,遇到了难以冲破的瓶颈和技术挑战,须要进行技术尝试和冲破。

本文会从底层原理、优化思路、理论场景的优化策略、核心技术实现、优化成绩等方面进行讲述,冀望能够为大家带来肯定的启发和帮忙,也欢送多多交换与斧正,共建美妙的 Flutter 技术社区。

渲染机制

原生 vs Flutter

Flutter 自身是基于原生零碎之上的,所以渲染机制和 Native 是十分靠近的,援用 Google Flutter 团队 Xiao Yu 分享[1],如下图所示:

渲染流程

如图左中,Flutter 从接管到 VSync 信号之后整体经验 8 个阶段,其中 Compositing 阶段后会将数据提交给 GPU。

Semantics 阶段会将 RenderObject marked 须要做语义化更新的信息传递给零碎,实现辅助性能,通过语义化接口能够帮忙有视力阻碍的用户来了解 UI 内容,和整体绘制流程关联不大。

Finalize Tree 阶段会将所有增加到 _inactiveElements 的不沉闷 Element 全副 unmount 掉,和整体绘制流程关联不大。

所以,Flutter 整体渲染流程次要关注 上图图右 中的阶段:

GPU Vsync

Flutter Engine 在收到垂直同步信号后,会告诉 Flutter Framework 进行 beginFrame,进入 Animation 阶段。

Animation

次要执行了 transientCallbacks 回调。Flutter Engine 会告诉 Flutter Framework 进行 drawFrame,进入 Build 阶段。

Build

构建要出现的 UI 组件树的数据结构,即创立对应的 Widget 以及对应的 Element。

Layout

目标是要计算出每个节点所占空间的实在大小进行布局,而后更新所有 dirty render objects 的布局信息。

Compositing Bits

对须要更新的 RenderObject 进行 update 操作。

Paint

生成 Layer Tree,生成 Layer Tree 并不能间接应用,还须要 Compositing 合成为一个 Scene 并进行 Rasterize 光栅化解决。层级合并的起因是因为个别 Flutter 的层级很多,间接把每一层传递给 GPU 效率很低,所以会先做 Composite 提高效率。光栅化之后才会交给 Flutter Engine 解决。

Compositing

将 Layout Tree 合成为 Scene,并创立场景以后状态的栅格图像,即进行 Rasterize 光栅化解决,而后提交给 Flutter Engine,最初 Skia 通过 Open GL or Vulkan 接口提交数据给 GPU,GPU 通过解决后进行显示。

外围渲染阶段

Widget

咱们平时在写的大都是 Widget,Widget 其实能够了解为是一个组件树的数据结构,是 Build 阶段的次要局部。其中 Widget Tree 的深度、StatefulWidget 的 setState 合理性、build 函数中是否有不合理逻辑以及应用了调用 saveLayer 的相干 Widget 往往会成为性能问题。

Element

关联 Widget 和 RenderObject,生成 Widget 对应的 Element 寄存上下文信息,Flutter 通过遍历 Element 来生成 RenderObject 视图树撑持 UI 构造。

RenderObject

RenderObject 在 Layout 阶段确定布局信息,Paint 阶段生成为对应的 Layer,可见其重要水平。所以 Flutter 中大部分的绘图性能优化产生在这里。RenderObject 树构建的数据会被退出到 Engine 所需的 LayerTree 中。

性能优化思路

理解底层渲染机制和外围渲染阶段,能够将优化分为三层:

这里不具体开展讲每一层的优化细节,本文次要从理论的场景来讲述。

流式场景

流式组件原理

在原生开发下,通常应用 RecyclerView/UICollectionView 进行列表场景的开发;在 Flutter 开发下,Flutter Framework 也提供了 ListView 的组件,它的本质其实是 SliverList。

外围源码

咱们从 SliverList 的外围源码来进行剖析:

class SliverList extends SliverMultiBoxAdaptorWidget {

  @override
  RenderSliverList createRenderObject(BuildContext context) {
    final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement;
    return RenderSliverList(childManager: element);
  }
}

abstract class SliverMultiBoxAdaptorWidget extends SliverWithKeepAliveWidget {

  final SliverChildDelegate delegate;

  @override
  SliverMultiBoxAdaptorElement createElement() => SliverMultiBoxAdaptorElement(this);

  @override
  RenderSliverMultiBoxAdaptor createRenderObject(BuildContext context);
}

通过查看 SliverList 的源代码可知,SliverList 是一个 RenderObjectWidget,构造如下:

咱们首先看它的 RenderObject 的外围源码:

class RenderSliverList extends RenderSliverMultiBoxAdaptor {

  RenderSliverList({@required RenderSliverBoxChildManager childManager,}) : super(childManager: childManager);

  @override
  void performLayout(){
    ...
    // 父节点对子节点的布局限度
    final SliverConstraints constraints = this.constraints;
    final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
    final double remainingExtent = constraints.remainingCacheExtent;
    final double targetEndScrollOffset = scrollOffset + remainingExtent;
    final BoxConstraints childConstraints = constraints.asBoxConstraints();
    ...
    insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
    ...
    insertAndLayoutChild(childConstraints,after: trailingChildWithLayout,parentUsesSize: true);
    ...
    collectGarbage(leadingGarbage, trailingGarbage);
    ...
  }
}

abstract class RenderSliverMultiBoxAdaptor extends RenderSliver ...{
  @protected
  RenderBox insertAndLayoutChild(BoxConstraints childConstraints, {@required RenderBox after,...}) {_createOrObtainChild(index, after: after);
    ...
  }

  RenderBox insertAndLayoutLeadingChild(BoxConstraints childConstraints, {@required RenderBox after,...}) {_createOrObtainChild(index, after: after);
    ...
  }

  @protected
  void collectGarbage(int leadingGarbage, int trailingGarbage) {_destroyOrCacheChild(firstChild);
    ...
  }

  void _createOrObtainChild(int index, { RenderBox after}) {_childManager.createChild(index, after: after);
    ...
  }

  void _destroyOrCacheChild(RenderBox child) {if (childParentData.keepAlive) {
      // 为了更好的性能体现不会进行 keepAlive,走 else 逻辑.
      ...
    } else {_childManager.removeChild(child);
      ...
    }
  }
}

查看 RenderSliverList 的源码发现,对于 child 的创立和移除都是通过其父类 RenderSliverMultiBoxAdaptor 进行。而 RenderSliverMultiBoxAdaptor 是通过 _childManager 即 SliverMultiBoxAdaptorElement 进行的,整个 SliverList 绘制过程中布局大小由父节点给出了限度。

在流式场景下:

  • 在滑动过程中是通过 SliverMultiBoxAdaptorElement.createChild 进行对进入可视区新的 child 的创立;(即业务场景的每一个 item 卡片)
  • 在滑动过程中是通过 SliverMultiBoxAdaptorElement.removeChild 进行对不在可视区旧的 child 的移除。

咱们来看下 SliverMultiBoxAdaptorElement 的外围源码:

class SliverMultiBoxAdaptorElement extends RenderObjectElement implements RenderSliverBoxChildManager {final SplayTreeMap<int, Element> _childElements = SplayTreeMap<int, Element>();

  @override
  void createChild(int index, { @required RenderBox after}) {
    ...
    Element newChild = updateChild(_childElements[index], _build(index), index);
    if (newChild != null) {_childElements[index] = newChild;
    } else {_childElements.remove(index);
    }
    ...
  }

  @override
  void removeChild(RenderBox child) {
    ...
    final Element result = updateChild(_childElements[index], null, index);
    _childElements.remove(index);
    ...
  }

  @override
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    ...
    final Element newChild = super.updateChild(child, newWidget, newSlot);
    ...
  }
}

通过查看 SliverMultiBoxAdaptorElement 的源码能够发现,对于 child 的操作其实都是通过父类 Element 的 updateChild 进行的。

接下来,咱们来看下 Element 的外围代码:

abstract class Element extends DiagnosticableTree implements BuildContext {
  @protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {if (newWidget == null) {if (child != null)
        deactivateChild(child);
      return null;
    }
    Element newChild;
    if (child != null) {
      ...
      bool hasSameSuperclass = oldElementClass == newWidgetClass;;
      if (hasSameSuperclass && child.widget == newWidget) {if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        newChild = child;
      } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        newChild = child;
      } else {deactivateChild(child);
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else {newChild = inflateWidget(newWidget, newSlot);
    }
    ...
    return newChild;
  }

  @protected
  Element inflateWidget(Widget newWidget, dynamic newSlot) {
    ...
    final Element newChild = newWidget.createElement();
    newChild.mount(this, newSlot);
    ...
    return newChild;
  }

  @protected
  void deactivateChild(Element child) {
    child._parent = null;
    child.detachRenderObject(); 
    owner._inactiveElements.add(child); // this eventually calls child.deactivate() & child.unmount()
    ...
  }
}

能够看到次要调用 Element 的 mount 和 detachRenderObject,这里咱们来看下 RenderObjectElement 的 这两个办法的源码:

abstract class RenderObjectElement extends Element {
  @override
  void mount(Element parent, dynamic newSlot) {super.mount(parent, newSlot);
    ...
    _renderObject = widget.createRenderObject(this);
    attachRenderObject(newSlot);
    ...
  }

  @override
  void attachRenderObject(dynamic newSlot) {
    ...
    _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
    _ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
    ...
  }

  @override
  void detachRenderObject() {if (_ancestorRenderObjectElement != null) {_ancestorRenderObjectElement.removeChildRenderObject(renderObject);
      _ancestorRenderObjectElement = null;
    }
    ...
  }
}

通过查看下面源码的追溯,可知:

在流式场景下:

  • 在滑动过程中进入可视区新的 child 的创立,是通过创立全新的 Element 并 mount 挂载到 Element Tree;而后创立对应的 RenderObject,调用了 _ancestorRenderObjectElement?.insertChildRenderObject;
  • 在滑动过程中不在可视区旧的 child 的移除,将对应的 Element 从 Element Tree unmount 移除挂载;而后调用了_ancestorRenderObjectElement.removeChildRenderObject。

其实这个 _ancestorRenderObjectElement 就是 SliverMultiBoxAdaptorElement,咱们再来看下 SliverMultiBoxAdaptorElement:

class SliverMultiBoxAdaptorElement extends RenderObjectElement implements RenderSliverBoxChildManager {

  @override
  void insertChildRenderObject(covariant RenderObject child, int slot) {
    ...
    renderObject.insert(child as RenderBox, after: _currentBeforeChild);
    ...
  }

  @override
  void removeChildRenderObject(covariant RenderObject child) {
    ...
    renderObject.remove(child as RenderBox);
  }
}

其实调用的都是 ContainerRenderObjectMixin 的办法,咱们再来看下 ContainerRenderObjectMixin:

mixin ContainerRenderObjectMixin<ChildType extends RenderObject, ... {void insert(ChildType child, { ChildType after}) {
        ...
    adoptChild(child);// attach render object
    _insertIntoChildList(child, after: after);
  }

  void remove(ChildType child) {_removeFromChildList(child);
    dropChild(child);// detach render object
  }
}

ContainerRenderObjectMixin 保护了一个双向链表来持有以后 children RenderObject,所以在滑动过程中创立和移除都会同步在 ContainerRenderObjectMixin 的双向链表中进行增加和移除。

最初总结下来:

  • 在滑动过程中进入可视区新的 child 的创立,是通过创立全新的 Element 并 mount 挂载到 Element Tree;而后创立对应的 RenderObject,通过调用 SliverMultiBoxAdaptorElement.insertChildRenderObject attach 到 Render Tree,并同步将 RenderObject 增加到 SliverMultiBoxAdaptorElement 所 mixin 的双链表中;
  • 在滑动过程中不在可视区旧的 child 的移除,将对应的 Element 从 Element Tree unmount 移除挂载;而后通过用 SliverMultiBoxAdaptorElement.removeChildRenderObject 将对应的 RenderObject 从所 mixin 的双链表中移除并同步将 RenderObject 从 Render Tree detach 掉。

渲染原理

通过外围源码的剖析,咱们能够对流式场景的 Element 做如下分类:

上面咱们来看用户向上滑动查看更多商品卡片并触发加载下一页数据进行展现时,整体的渲染流程和机制:

  • 向上滑动时,顶部 0 和 1 的卡片移出 Viewport 区域(Visible Area + Cache Area),咱们定义它为进入 Detach Area,进入 Detach Area 后将对应的 RenderObject 从 Render Tree detach 掉,并且将对应的 Element 从 Element Tree unmount 移除挂载,并同步从双向链表中移除;
  • 通过监听 ScrollController 的滑动计算地位来判断是否须要开始加载下一页数据,而后底部 Loading Footer 组件会进入可视区 or 缓存区,须要对 SliverChildBuilderDelegate 的 childCount +1,最初一个 child 返回 Loading Footer 组件,同时调用 setState 对整个 SliverList 刷新。update 会调用 performRebuild 进行重构建,两头局部在用户可视区会全副进行 update 操作;而后创立 Loading Footer 组件对应新的 Element 和 RenderObject,并同步增加到双向链表中;
  • 当 loading 完结数据返回后,会再次调用 setState 对整个 SliverList 刷新,update 会调用 performRebuild 进行重构建,两头局部在用户可视区会全副进行 update 操作;而后将 Loading Footer 组件将对应的 RenderObject 从 Render Tree detach 掉,并且将对应的 Element 从 Element Tree unmount 移除挂载,并同步从双向链表中移除;
  • 底部新的 item 会进入可视区 or 缓存区,须要创立对应新的 Element 和 RenderObject,并同步增加到双向链表中。

优化策略

下面用户向上滑动查看更多商品卡片并触发加载下一页数据进行展现的场景,能够从五个方向进行优化:

Load More

通过监听 ScrollController 的滑动一直进行计算,最好无需判断,自动识别到须要加载下一页数据而后发动 loadMore() 回调。新建 ReuseSliverChildBuilderDelegate 减少 loadMore 以及和 item Builder 同级的 footerBuilder,并默认蕴含 Loading Footer 组件,在 SliverMultiBoxAdaptorElement.createChild(int index,…) 判断是否须要动静回调 loadMore() 并主动构建 footer 组件。

部分刷新

参考了闲鱼之前在长列表的晦涩度优化[2],在下一页数据回来之后调用 setState 对整个 SliverList 刷新,导致两头局部在用户可视区会全副进行 update 操作,理论只需刷新新创建的局部,优化 SliverMultiBoxAdaptorElement.update(SliverMultiBoxAdaptorWidget newWidget) 的局部实现部分刷新,如下图:

[]()

Element & RenderObject 复用

参考了闲鱼之前在长列表的晦涩度优化[2] 和 Google Android RecyclerView ViewHolder 复用设计[3],在有新的 item 创立时,能够做相似 Android RecyclerView 的 ViewHolder 对组件进行持有并复用。基于对渲染机制原理剖析,在 Flutter 中 Widget 其实能够了解为是一个组件树的数据结构,即更多是组件构造的数据表白。咱们须要对移除的 item 的 Element 和 RenderObject 分组件类型进行缓存持有,在创立新的 item 的时候优先从缓存持有中取出进行复用。同时不毁坏 Flutter 自身对 Key 的设计,当如果 item 有应用 Key 的时候,只复用和它 Key 雷同的 Element 和 RenderObject。但在流式场景列表数据都是不同的数据,所以在流式场景中应用了 Key,也就无奈进行任何的复用。如果对 Element 和 RenderObject 进行复用,item 组件不倡议应用 Key。

咱们在对原有流式场景下 Element 的分类减少一个缓存态:

[]()

如下图:

[]()

GC 克制

Dart 本身有 GC 的机制,相似 Java 的分代回收,能够在滑动的过程中对 GC 进行克制,定制 GC 回收的算法。针对这项和 Google 的 Flutter 专家探讨,其实 Dart 不像 Java 会存在多线程切换进行垃圾回收的状况,单线程(主 isolate)垃圾回收更快更轻量级,同时须要对 Flutter Engine 做深度的革新,思考收益不大暂不进行。

异步化

Flutter Engine 限度非 Main Isolate 调用 Platform 相干 Api,将非跟 Platform Thread 交互的逻辑全副放至新的 isolate 中,频繁 Isolate 的创立和回收也会对性能有肯定的影响,Flutter compute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, { String debugLabel}) 每次调用会创立新的 Isolate,执行完工作后会进行回收,实现一个相似线程池的 Isolate 来进行解决非视图工作。通过理论测试晋升不显著,不开展讲述。

核心技术实现

咱们能够将调用链路的代码做如下分类:

[]()

所有渲染外围在继承自 RenderObjectElement 的 SliverMultiBoxAdaptorElement 中,不毁坏原有功能设计以及 Flutter Framework 的构造,新增了 ReuseSliverMultiBoxAdaptorElement 的 Element 来进行优化策略的实现,并且能够间接搭配原有 SliverList 的 RenderSliverList 应用或者自定义的流式组件(例如:瀑布流组件)的 RenderObject 应用。

部分刷新

调用链路优化

在 ReuseSliverMultiBoxAdaptorElement 的 update 办法做是否为部分刷新的判断,如果不是部分刷新仍然走 performRebuild;如果是部分刷新,只创立新产生的 item。

[]()

外围代码

@override
void update(covariant ReuseSliverMultiBoxAdaptorWidget newWidget) {
  ...
  // 是否进行部分刷新
  if(_isPartialRefresh(oldDelegate, newDelegate)) {
      ...
      int index = _childElements.lastKey() + 1;
      Widget newWidget = _buildItem(index);
      // do not create child when new widget is null
      if (newWidget == null) {return;}
      _currentBeforeChild = _childElements[index - 1].renderObject as RenderBox;
      _createChild(index, newWidget);
    } else {
       // need to rebuild
       performRebuild();}
}

Element & RenderObject 复用

调用链路优化

  • 创立:在 ReuseSliverMultiBoxAdaptorElement 的 createChild 办法读取 _cacheElements 对应组件类型缓存的 Element 进行复用;如果没有同类型可复用的 Element 则创立对应新的 Element 和 RenderObject。
  • 移除:在 ReuseSliverMultiBoxAdaptorElement 的 removeChild 办法将移除的 RenderObject 从双链表中移除,不进行 Element 的 deactive 和 RenderObject 的 detach,并将对应的 Element 的 _slot 更新为 null,使下次能够失常复用,而后将对应的 Element 缓存到 _cacheElements 对应组件类型的链表中。

[]()

注:不 deactive Element 其实不进行调用即可实现,但不 detach RenderObject 无奈间接做到,须要在 Flutter Framework 层的 object.dart 文件中,新增一个办法 removeOnly 就是只将 RenderObject 从双链表中移除不进行 detach。

外围代码

  • 创立
// 新增的办法,createChild 会调用到这个办法
_createChild(int index, Widget newWidget){
  ...
  Type delegateChildRuntimeType = _getWidgetRuntimeType(newWidget);
  if(_cacheElements[delegateChildRuntimeType] != null
      && _cacheElements[delegateChildRuntimeType].isNotEmpty){child = _cacheElements[delegateChildRuntimeType].removeAt(0);
  }else {child = _childElements[index];
  }
  ...
  newChild = updateChild(child, newWidget, index);
  ...
}
  • 移除
@override
void removeChild(RenderBox child) {
 ...
 removeChildRenderObject(child); // call removeOnly
 ...
 removeElement = _childElements.remove(index);
 _performCacheElement(removeElement);
 }

Load More

调用链路优化

在 createChild 时候判断是否是构建 footer 来进行解决。

[]()

外围代码

@override
void createChild(int index, { @required RenderBox after}) {
    ...
    Widget newWidget;
    if(_isBuildFooter(index)){ // call footerBuilder & call onLoadMore
      newWidget = _buildFooter();}else{newWidget = _buildItem(index);
    }
    ...
    _createChild(index, newWidget);
    ...
}

整体结构设计

  • 将外围的优化能力内聚在 Element 层,提供底层能力;
  • 将 ReuseSliverMultiBoxAdaptorWidget 做为基类默认返回优化后的 Element;
  • 将 loadMore 和 FooterBuilder 的能力对立由继承自 SliverChildBuilderDelegate 的 ReuseSliverChildBuilderDelegate 对下层裸露;
  • 如有本人独自定制的流式组件 Widget,间接把继承关系从 RenderObjectWidget 换为 ReuseSliverMultiBoxAdaptorWidget 即可,例如自定义的单列表组件 (ReuseSliverList)、瀑布流组件(ReuseWaterFall) 等。

优化成绩

基于在之前的一系列深度优化以及切换 Flutter Engine 为 UC Hummer 之上,独自管制流式场景的优化变量,应用 PerfDog 获取晦涩度数据,进行了晦涩度测试比照:

能够看到整体性能数据都有优化晋升,联合替换 Engine 之前的测试数据均匀来看,对帧率有 2-3 帧的晋升,卡顿率降落 1.5 个百分点。

总结

应用形式

和原生 SliverList 的应用形式一样,Widget 换成对应能够进行复用的组件 (ReuseSliverList/ReuseWaterFall/ CustomSliverList),delegate 如果须要 footer 和 loadMore 应用 ReuseSliverChildBuilderDelegate; 如果不须要间接应用原生的 SliverChildBuilderDelegate 即可。

须要分页场景

return ReuseSliverList( // ReuseWaterFall or CustomSliverList
delegate: ReuseSliverChildBuilderDelegate((BuildContext context, int index) {return getItemWidget(index);
  }, 
  // 构建 footer
  footerBuilder: (BuildContext context) {return DetailMiniFootWidget();
  },
  // 增加 loadMore 监听
  addUnderFlowListener: loadMore,
  childCount: dataOfWidgetList.length
)
);

无需分页场景

return ReuseSliverList( // ReuseWaterFall or CustomSliverList
delegate: SliverChildBuilderDelegate((BuildContext context, int index) {return getItemWidget(index);
  }, 
  childCount: dataOfWidgetList.length
)
);

留神点

应用的时候 item/footer 组件不要加 Key,否则认为只对同 Key 进行复用。因为复用了 Element,尽管表白组件树数据后果的 Widget 会每次进行更新,但 StatefulElement 的 State 是在 Element 创立的时候生成的,同时也会被复用下来,和 Flutter 自身设计保持一致,所以须要在 didUpdateWidget(covariant T oldWidget) 将 State 缓存的数据从新从 Widget 获取即可。

Reuse Element Lifecycle

将每个 item 的状态进行回调,下层能够做逻辑解决和资源开释等,例如之前在 didUpdateWidget(covariant T oldWidget) 将 State 缓存的数据从新从 Widget 获取能够搁置在 onDisappear 里或者自动播放的视频流等;

/// 复用的生命周期
mixin ReuseSliverLifeCycle{

  // 前台可见的
  void onAppear() {}

  // 后盾不可见的
  void onDisappear() {}
}

参考资料

[1]:Google Flutter 团队 Xiao Yu:Flutter Performance Profiling and Theory:https://files.flutter-io.cn/e…

[2]:闲鱼云从: 他把闲鱼 APP 长列表晦涩度翻了倍

[3]:Google Android RecyclerView.ViewHolder:RecyclerView.Adapter#onCreateViewHolder:https://developer.android.com…(android.view.ViewGroup,%20int)

关注【阿里巴巴挪动技术】官网公众号,每周 3 篇挪动技术实际 & 干货给你思考!

正文完
 0