即便是哥布林也能分分鐘實現一個支持複用的ListView?——關於flutter listView複用的實驗性想法和發現

前言

這篇文章呢,能夠說是對最近flutter的listView研究作個小小的彙總;git

其實呢,我一直對flutter的設計沒太有什麼好感,尤爲是完成那個支持仿真翻頁的小說閱讀器的那段時期,不止一次的想:md這作的啥玩意,看看隔壁Android 的xxx,就你這還好意思對標Android原生?github

PS:其實如今關於文字繪製這塊我仍是這麼想的;算法

不過最近看了閒魚的 Flutter 高性能、多功能的全場景滾動容器,必定要看!,有了些想法改進重寫那個小說閱讀器,研究了下,發現flutter的listView設計,好像仍是蠻不錯的,思路很清晰很輕量,相較之下,Android 就比較沉重了;(可能也是如今的listView太初期的緣故,不過設計思路卻是蠻好讀的)windows

PPS:話說是否是閒魚的自定義engine版本過低了,因此纔沒那些功能?像曝光、複用這塊都已經有現成的部分了啊……爲啥要費勁自定義呢?緩存

演員就位,好戲開始

首先呢,默認你們都對flutter的一些基礎知識、好比說三棵樹及其做用啊什麼的都已經瞭解;固然沒了解的也沒太大關係,百度谷歌下,這玩意的講解已經爛大街了,看個5分鐘瞭解下大概就夠了;markdown

PS:若是懶得看,直接翻後面總結部分,一步到胃;ide

由於widget樹和element樹並不參與繪製過程,因此相對輕量,因此在我看來,複用renderObject,便可提升不少性能,因此問題來了:oop

怎麼去複用renderObject?post

要解決這個問題,咱們就要開始追蹤下RenderObject跟element的愛恨情仇;性能

固然關於他倆的雞毛蒜皮或陳穀子爛芝麻的事就不在這裏提了,直接看相關的部分;

widget樹?那玩意就是個舔狗,召之即來揮之即去的傢伙,不用管;

衆所周知,一個View要想展現,必需要走三步:measure、layout、draw;flutter中一樣道理,只不過這事是renderObject來作的;在listView中找下相關方法就能找到相關部分:

class RenderSliverList extends RenderSliverMultiBoxAdaptor {
  /// Creates a sliver that places multiple box children in a linear array along
  /// the main axis.
  ///
  /// The [childManager] argument must not be null.
  RenderSliverList({
    required RenderSliverBoxChildManager childManager,
  }) : super(childManager: childManager);

  @override
  void performLayout() {
    …………一堆不相關的
    
    /// 好哥哥看過來
    bool advance() { // returns true if we advanced, false if we have no more children
      // This function is used in two different places below, to avoid code duplication.
      assert(child != null);
      if (child == trailingChildWithLayout)
        inLayoutRange = false;
      child = childAfter(child!);
      if (child == null)
        inLayoutRange = false;
      index += 1;
      if (!inLayoutRange) {
        if (child == null || indexOf(child!) != index) {
          // We are missing a child. Insert it (and lay it out) if possible.
          child = insertAndLayoutChild(childConstraints,
            after: trailingChildWithLayout,
            parentUsesSize: true,
          );
          if (child == null) {
            // We have run out of children.
            return false;
          }
        } else {
          // Lay out the child.
          child!.layout(childConstraints, parentUsesSize: true);
        }
        trailingChildWithLayout = child;
      }
      assert(child != null);
      final SliverMultiBoxAdaptorParentData childParentData = child!.parentData as SliverMultiBoxAdaptorParentData;
      childParentData.layoutOffset = endScrollOffset;
      assert(childParentData.index == index);
      endScrollOffset = childScrollOffset(child!)! + paintExtentOf(child!);
      return true;
    }

    // Find the first child that ends after the scroll offset.
    while (endScrollOffset < scrollOffset) {
      leadingGarbage += 1;
      if (!advance()) {
        assert(leadingGarbage == childCount);
        assert(child == null);
        // we want to make sure we keep the last child around so we know the end scroll offset
        collectGarbage(leadingGarbage - 1, 0);
        assert(firstChild == lastChild);
        final double extent = childScrollOffset(lastChild!)! + paintExtentOf(lastChild!);
        geometry = SliverGeometry(
          scrollExtent: extent,
          paintExtent: 0.0,
          maxPaintExtent: extent,
        );
        return;
      }
    }

    // Now find the first child that ends after our end.
    while (endScrollOffset < targetEndScrollOffset) {
      if (!advance()) {
        reachedEnd = true;
        break;
      }
    }
    
    ……

    collectGarbage(leadingGarbage, trailingGarbage);

    ……
  }
}

複製代碼

注意其中的advance(),其中有個insertAndLayoutChild 方法,從字面上來看,就是它負責插入子RenderObject的

@protected
  RenderBox? insertAndLayoutChild(
    BoxConstraints childConstraints, {
    required RenderBox? after,
    bool parentUsesSize = false,
  }) {
    assert(_debugAssertChildListLocked());
    assert(after != null);
    final int index = indexOf(after!) + 1;
    _createOrObtainChild(index, after: after);
    final RenderBox? child = childAfter(after);
    if (child != null && indexOf(child) == index) {
      child.layout(childConstraints, parentUsesSize: parentUsesSize);
      return child;
    }
    childManager.setDidUnderflow(true);
    return null;
  }
複製代碼

唉,其中有個_createOrObtainChild 方法,從字面上翻譯,意思是,建立或獲取child?獲取child?

再點進去看看

void _createOrObtainChild(int index, { required RenderBox? after }) {
    invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
      assert(constraints == this.constraints);
      if (_keepAliveBucket.containsKey(index)) {
        final RenderBox child = _keepAliveBucket.remove(index)!;
        final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
        assert(childParentData._keptAlive);
        dropChild(child);
        child.parentData = childParentData;
        insert(child, after: after);
        childParentData._keptAlive = false;
      } else {
        _childManager.createChild(index, after: after);
      }
    });
  }
複製代碼

下面那個createChild確定不是緩存相關的了,因此這個_keepAliveBucket 嫌疑很大唉,看下他的方法內容,應該就是它作複用了;可是被複用的對象是誰加進來的呢?

點一下看下都是誰在用這個_keepAliveBucket

em,好像還很多人用,可是沒有關西;反正我就想知道是誰往裏面塞數據,這麼再一看,只剩下兩個方法:

一個是move方法,看名字就不像; 一個是_destroyOrCacheChild 方法,看上去就是它,點進去看看;

void _destroyOrCacheChild(RenderBox child) {
    final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
    if (childParentData.keepAlive) {
      assert(!childParentData._keptAlive);
      remove(child);
      _keepAliveBucket[childParentData.index!] = child;
      child.parentData = childParentData;
      super.adoptChild(child);
      childParentData._keptAlive = true;
    } else {
      assert(child.parent == this);
      _childManager.removeChild(child);
      assert(child.parent == null);
    }
  }
複製代碼

因此又冒出一個新東西:SliverMultiBoxAdaptorParentData,那麼是否是隻要讓這玩意的 keepAlive爲true 就好了

先看下是誰在調用這個_destroyOrCacheChild;

唉,就一個方法 collectGarbage ,而這個方法只在performLayout 調用,跟那個_createOrObtainChild 方法同樣,都是直接或間接在performLayout方法中調用,看來跟它同樣,屬於比較直接的生命週期類方法;

那麼直接重寫這個collectGarbage 方法,講要回收的對象的keepAlive改成true,不就能夠了麼?

就這?????

最後試了下,好像還真是這樣,建立過的RenderObject,不會再建立第二次,createChild 方法每一個index只調用一次;應該是複用成功了;

拓展與猜測

複用問題就這麼解決了?就這 兩個字真的是我當時的想法,甚至感受好像不會這麼簡單,是否是哪裏有坑啊……

不過我所以產生了一些猜測:

若是這樣的話,可否像RecyclerView那樣完全複用RenderObject?

如今是根據index來複用的,很簡單;若是滑動到新的item,並且緩存中不存在,仍是會走create;

可是若是像Android的RecyclerView那樣,直接拿緩存區的RenderObject,而後從新塞回去替代建立,並經過更新機制更新展現數據,這樣是否可行呢?記得看過某篇文章中說過,flutter內部更新使用一個diff算法來作的,挺高效的,這樣用高效的diff算法應該比單純建立高效吧,應該吧~~~

總結

複用這塊直接用flutter官方提供的就完事了,固然純屬試驗性質,有沒有坑還真沒試出來,也沒作全面測試

這是個人flutter doctor -v 關於flutter部分的信息,若是你那代碼有差異的話,能夠對比參考下:

[√] Flutter (Channel stable, 1.22.5, on Microsoft Windows [Version 10.0.17763.1577], locale zh-CN)
    • Flutter version 1.22.5 at D:\Program File\sdk\flutter\flutter_windows_v1.9.1+hotfix.2-stable
    • Framework revision 7891006299 (9 weeks ago), 2020-12-10 11:54:40 -0800
    • Engine revision ae90085a84
    • Dart version 2.10.4
複製代碼

下面上關鍵部分代碼,默認自定義的 RenderSliverList 已經經過繼承和重寫引入進去

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

  @override
  void collectGarbage(int leadingGarbage, int trailingGarbage) {
    /// 若是從頭開始要回收的垃圾數量+從尾開始要回收的垃圾數量 不等於 0(也就是大於0)
    if (leadingGarbage + trailingGarbage != 0) {
      print("collectGarbage : " +
          " leadingGarbage : " +
          leadingGarbage.toString() +
          ", trailingGarbage : " +
          trailingGarbage.toString());

      if (childCount >= leadingGarbage + trailingGarbage) {
        int tempLeadingGarbage = leadingGarbage;
        int tempTrailingGarbage = trailingGarbage;

        RenderObject tempFirstChild = firstChild;
        RenderObject tempLastChild = lastChild;

        while (tempLeadingGarbage > 0) {
          /// 標記keepAlive爲true
          (tempFirstChild.parentData as SliverMultiBoxAdaptorParentData)
              .keepAlive = true;
          tempFirstChild = childAfter(tempFirstChild);
          tempLeadingGarbage -= 1;
        }

        while (tempTrailingGarbage > 0) {
          /// 標記keepAlive爲true
          (tempLastChild.parentData as SliverMultiBoxAdaptorParentData)
              .keepAlive = true;
          tempLastChild = childBefore(tempLastChild);
          tempTrailingGarbage -= 1;
        }
      }
    }

    /// 剩下的flutter都作好了
    super.collectGarbage(leadingGarbage, trailingGarbage);
  }
}

複製代碼

就這麼簡單…………

固然這塊是徹底複用,貌似沒上限的那種,就是那種你要有一萬個item,他就給你緩存一萬個,因此理論上會很是吃內存,正確的作法應該要結合本身定義的緩存規則來作,不過那塊還沒搞~

因此還須要進一步測試研究~

另外補充一小點:

曝光這塊其實也蠻簡單的,這幫renderObject的parentData都是SliverMultiBoxAdaptorParentData ,裏面都帶上了index……

因此其實只要遍歷下子child,看下保存的offset的數值,就能夠得知當前第一個可見項什麼的……而後直接從parentData中拿index就完事了

這個我是真的感受沒啥問題的

題外話

若是還有須要,能夠拉下 flutter_novel 的dev分支,裏面的reader2文件夾就是相關部分的,不過須要本身找,目前那塊都是試驗性質的,搞的其實有點亂;

在這裏先給你們拜個年,若是不出意外的話,接下來的12天我就要蹲在提瓦特大陸了~因此嘛,失個聯,很正常嘛,除非你們能在提瓦特大陸相遇,而後問候一句:

原來你也玩原神?

咳咳,真tmd尬

相關文章
相關標籤/搜索