Flutter小知識--what is key?

ershixiong發表於2019-07-11

在Flutter中,每個Widget都是唯一標記的。這個唯一標識是有框架在編譯/渲染期間定義的。
Widget的唯一標識與其可選引數Key一致。如果不傳Flutter會為你生成一個。
在某些情況下,你可能需要強制指定它的key,這樣你就能根據key來訪問一個widget。
為了實現這樣一個需求,你可以使用下面輔助工具中的一個:GlobalKey,LocalKey,UniqueKey或者ObjectKey。
其中GlobalKey可以保證在整個應用程式中唯一。

GlobalKey相關概念

整個應用程式唯一的key。

Global keys可以唯一標識elements。Global keys提供了訪問與elements關聯的其他物件的能力,比如 StatefulWidgets的BuildContextStateBuildContext

擁有global keys的Widgets當他們從樹的一個位置挪到另一個位置,可以為子樹重定Widget。為了能夠重定父級,一個widget必須在同一個tree中,同一個動畫幀中,離開原來原位置,併到達新的位置。

Global keys是相當昂貴的。如果你不需要上面列舉的功能,可以考慮 Key,ValueKey,ObjectKey或者UniqueKey來代替。

你不能在同一個樹下包含兩個擁有相同global key的widget。如果嘗試這麼做將會促發執行時斷言。

GlobalKey的定義如下:

abstract class GlobalKey<T extends State<StatefulWidget>> extends Key
複製程式碼

T必須要繼承自State,可以說這個GlobalKey專門用於元件了.

static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
複製程式碼

GlobalKey裡含有一個Map,key和value分別為自身和Element。
那什麼時候會用到這個Map尼?
跟蹤程式碼很快就找到Element類的mount方法:

void mount(Element parent, dynamic newSlot) {
    ...
    if (widget.key is GlobalKey) {
      final GlobalKey key = widget.key;
      key._register(this);
    }
   ...
  }
複製程式碼

可見GlobalKey會在元件Mount階段把自身放到一個Map裡面快取起來。
快取又有何作用尼?
答案依然是為了效能。
思考一個場景,A頁面是一個商品列表有許多商品圖片(大概就單列這樣),B頁面是一個商品詳情頁(有商品大圖),當使用者在A頁面點選一個其中詳情,可能會出現一個過渡動畫,A頁面的商品圖片慢慢放大然後下面的介紹文字也會跟著出現,然後就這樣平滑的過渡到B頁面。
此時A頁面和B頁面都其實共用了一個商品圖片的元件,B頁面沒必要重複建立這個元件可以直接把A頁面的元件“借”過來。

總之框架要求同一個父節點下子節點的Key都是唯一的就可以了,GlobalKey可以保證全域性是唯一的,所以GlobalKey的元件能夠依附在不同的節點上。
而從GlobalKey物件上,你可以得到幾個有用的屬性currentElement,currentWidget,currentState。

接下來看一下Widget本身的key定義.

Widget.key

用來控制在樹中,一個widget如何替換另一個widget。

如果兩個widgets的runtimeTypekey屬性x相等(operator==),那麼新的widget替換就得widget通過更新底層的element(呼叫新的widget的Element.update)。 除此之外,舊的element將會從tree中移除,新的widget將會轉化成element,並被插入到tree中。

另外,使用GlobalKey作為widget的key將允許element在tree中移動(通過變化parent),並不會丟失state.當一個新的widget被發現(它的key和type跟上一個在同一個位置的widget都不同), 但是有一個擁有相同global key的widget在上一幀的tree的其他地方,那麼這個widget的element將被移動到新的位置。

通常來說,一個widget如果是另外一個widget的唯一child,此時不必擁有一個明確的key.

這裡多次提到element,那麼element到底是什麼?

Element

提到Element,需要再提一下Widget的官方定義。

/// Describes the configuration for an [Element].
///
/// Widgets are the central class hierarchy in the Flutter framework. A widget
/// is an immutable description of part of a user interface. Widgets can be
/// inflated into elements, which manage the underlying render tree.
複製程式碼

可以看到,Widget 的實際工作也就是描述如何建立 ElementWidget 是一個不可變物件,它可以被複用, 請注意,這裡的複用不是指在兩次渲染的時候將物件從舊樹中拿過來放到新樹,而是在同一個 Widget Tree 中,某個子 Widget 可以出現多次,因為它只是一個 description。
Widget 只是 Element 的一個配置描述 ,告訴 Element 這個例項如何去渲染。

/// A given widget can be included in the tree zero or more times. In particular
/// a given widget can be placed in the tree multiple times. Each time a widget
/// is placed in the tree, it is inflated into an [Element], which means a
/// widget that is incorporated into the tree multiple times will be inflated
/// multiple times.
複製程式碼

從上面這段註釋可以看出,Widget 和 Element 之間是一對多的關係。實際上渲染樹是由 Element 例項的節點構成的樹,而作為配置檔案的 Widget 可能被複用到樹的多個部分,對應產生多個 Element 物件。

這也就是你每次都可以在 build() 函式中新建 widget 的原因。構建 widget 的過程並不耗費資源,因為 Wiget 只是用來儲存屬性的容器。

如果widget只是提供給使用者的包裝殼,那麼實際進行渲染的是什麼呢?
答案是RenderingObject。有一個很好的原始碼案例可參見Opacity的原始碼,地址如下.
Opacity原始碼

選用這個例子的原因是 普通的Stateless / StatefulWidget 只是將其他 Widget 組裝起來,而 Opacity 會真正地影響 Widget 的繪製。
最終會跟到RenderOpacity中,會看到下面的方法:

@override
void paint(PaintingContext context, Offset offset) {
    context.pushOpacity(offset, _alpha, super.paint);
}
複製程式碼

完整程式碼
PaintingContext 就是進行繪製操作的畫布,這裡通過在 canvas 上呼叫名為pushOpacity的方法來實現不透明度的控制。

總結一下,Widget 只是一個配置,RenderObject 負責管理佈局、繪製等操作。而連線WidgetRenderObject正是Element

而在 Element的原始碼中,則可以獲取到RenderObject:

  /// The render object at (or below) this location in the tree.
  ///
  /// If this object is a [RenderObjectElement], the render object is the one at
  /// this location in the tree. Otherwise, this getter will walk down the tree
  /// until it finds a [RenderObjectElement].
  RenderObject get renderObject {
    RenderObject result;
    void visit(Element element) {
      assert(result == null); // this verifies that there's only one child
      if (element is RenderObjectElement)
        result = element.renderObject;
      else
        element.visitChildren(visit);
    }
    visit(this);
    return result;
  }
複製程式碼

可以大致總結出三者的關係是:配置檔案 Widget 生成了 Element,而後建立 RenderObject 關聯到 Element 的內部 renderObject 物件上,最後Flutter 通過 RenderObject 資料來佈局和繪製。

參考資料如下,感謝:
www.stephenw.cc/2018/05/28/…
juejin.im/post/5b4c60…


如果你覺得這篇文章對你有益,還請幫忙轉發和點贊,萬分感謝。

Flutter爛筆頭