從mixin機制理解Flutter App啓動

1-mixin機制

1.1-前言

  轉載註明出處:https://juejin.im/post/5efbf499e51d4534b979050c
  mixin機制並非dart獨創,在其他前端語言中也有很廣泛應用。但對於一個剛開始看Flutter源碼的客戶端開發來說,各種mixin直接勸退,不得不先惡補下mixin。
  mixin首要特性就是實現函數複用,所以在開始mixin機制解析前,先從第一個問題出發:

  • 怎麼實現函數的複用?

  對應面嚮對象語言來說,通常的做法就是繼承,即在基類中實現某個函數,子類繼承該基類就可使用函數了。舉個例子:狗和鷹都可以移動,通過在基類Animal中實現moveTo方法,Dog和Eagle繼承後都能使用moveTo方法了

class Animal {
    void moveTo(){...//Do some thing}
}
class Dog extends Animal{}
class Eagle extends Animal{}

  但通過繼承的方式實現函數複用會有另外一個問題。即函數是與基類耦合的,子類繼承了基類後就繼承了基類的所有方法和屬性。如果汽車要複用moveTo方法的話顯然繼承Animal是不合適的,由此引申出問題2:

  • 怎麼才能只複用Animal的moveTo方法而不繼承其他方法和屬性?

  首先肯定要把moveTo方法從Animal中解耦出來,定義一個接口CommonBehavior來實現。在java8及Kotlin的接口支持函數的默認實現,java8需要default關鍵字。kotlin接口定義的方法同樣支持默認實現,不過爲了兼容java之前的版本,採用的是編譯時生成一個靜態類,通過調用靜態類的靜態方法moveTo方法來實現。

interface CommonBehavior {
    defalut void moveTo(){...//Do some Thing}
}
class Dog implements CommonBehavior{}

  此外Koltin還可以通過類委託來實現方法複用。除了接口外,還需聲明一個moveTo具體實現的委託類BehaviorDelegate。

interface CommonBehavior {
    fun moveTo()
}
class BehaviorDelegate : CommonBehavior {
    override fun moveTo(){...//Do some thing}
}
class Dog : CommonBehavior by BehaviorDelegate()

  Kotlin類委託機制就不再詳述了,原理是通過代理實現。Java當然也是可以通過代理實現的,不過沒有by這種語法糖用起來爽。轉到正題:

  • 在Dart中怎麼去實現代碼複用呢?

  Dart中沒有interface關鍵字,而是用mixin進行混合,將moveTo抽離到一個mixin修飾的CommonBehavior。這樣就能通過混入CommonBehavior直接使用moveTo方法了。

class Animal{}
mixin CommonBehavior{
  moveTo(){...//Do some thing};
}
class Dog extends Animal with CommonBehavior {}

1.2-mixin特性

  實現代碼複用只是mixin的基本功能,mixin還有其他強大的特性。
混入多個mixin時會向前覆蓋,即後混入的mixin類中的方法會覆蓋前面繼承或混入的相同方法。我們先來看一個簡單的例1

//例1
class SuperClass{
	fun()=>print('SuperClass');
}
mixin MixA{
  fun()=>print('MixA');
}
mixin MixB{
	fun()=>print('MixB'); 
}
class Child extends SuperClass with MixA,MixB {}
main(){
  Child child = Child();
  child.fun();
}

運行後的結果:
MixB
  先混入的MixA含有fun(),覆蓋了SuperClass的fun()。而後混入的MixB也有fun(),覆蓋了MixA的方法,最終調用的是MixB的fun()方法。由此也可以知道後混入的mixin類的方法是最先調用的。爲了驗證這一調用順序我們對例1進行如下改動:

//例2
class SuperClass{
  fun(){
    print('-->SuperClass.fun()');
    print('-->SuperClass');
  }

}
mixin MixA on SuperClass{
  fun(){
    print('-->MixA.fun()');
    super.fun();
    print('-->MixA');
  }
}
mixin MixB on SuperClass{
  fun(){
    print('-->MixB.fun()');
    super.fun();
    print('-->MixB');
  }
}
class Child extends SuperClass with MixA,MixB {}
main(){
  Child child = Child();
  child.fun();
}

輸出結果:

-->MixB.fun()
-->MixA.fun()
-->SuperClass.fun()
-->SuperClass
-->MixA
-->MixB

  由輸出結果可以看出通過mixin機制的調用關係,在形式上實現了類似"多繼承"一樣的繼承鏈。
  這裏使用了mixin on。mixin MixA on SuperClass 這樣支持在MixA中像繼承一樣通過super來調用SuperClass的方法。同時也限定了要混入MixA的類必須繼承自SuperClass。
  在繼承關係方面,輸出結果給人一種child–>MixB–>MixA–>SuperClass繼承關係的錯覺,其實不然。混合機制相當於在SuperClass的頂層混入mixin類並生成一個新類,類似於Android中的幀佈局SuperClass屬於最下層父佈局,mixin類屬於其中的子元素,mixin類之間並無父子關係相互解耦。後加入的mixin類在“幀佈局”中層級越靠上,會覆蓋下層的相同位置方法。用僞代碼來描述上面例子中的繼承關係:

class SuperMixA = SuperClass with MixA;
class SuperMixAMixB = SuperMixA with MixB;
class Child extends SuperMixAMixB {}

  這種"繼承鏈"如下圖所示,Child最終繼承的是在Super、MixA、MixB的一個混合,Child 的實例child 類型 屬於Super、MixA、MixB的混合,用類型判讀is得到的結果都是ture。但MixA與MixB直接卻並沒有直接關係,這也就符合了開閉原則,在不修改Child的基礎上通過mixin對其進行擴展。

  我們對例2稍加修改,就更接近Flutter App啓動過程的調用關係了:

//例3
class SuperClass{
  SuperClass() {
    print('-->SuperClass init');
    fun();
  }
  fun(){
    print('-->SuperClass.fun() start');
    print('-->SuperClass.fun() end');
  }

}
mixin MixA on SuperClass{
  fun(){
    print('-->MixA.fun() start');
    super.fun();
    print('-->MixA.fun() end');
  }
}
mixin MixB on SuperClass{
  fun(){
    print('-->MixB.fun() start');
    super.fun();
    print('-->MixB.fun() end');
  }
}
class Child extends SuperClass with MixA,MixB {
  Child() {
    print('-->Child init');
  }
}
main(){
  Child child = Child();
}

輸出結果:

-->SuperClass init
-->MixB.fun() start
-->MixA.fun() start
-->SuperClass.fun() start
-->SuperClass.fun() end
-->MixA.fun() end
-->MixB.fun() end
-->Child init

  至此,mixin機制的講解就先告一段落,這些都是便於我們理解第2章講到的Flutter App啓動初始化過程。至於mixin其實還有其他相關特性,沒有構造函數、with還可以混入非mixin類等等,這裏就不再展開了。

2-runApp啓動

  FlutterApp啓動過程在Android中主要是從

  • FlutterApplication.onCreate完成加載引擎libflutter.so、註冊JNI方法等
  • FlutterActivity.onCreate中通過FlutterJNI的attachJNI來初始化引擎Engine、Dart VM、UI/GPU/IO線程初始化等
  • main.dart 中runApp

  本文主要是結合mixin機制從main.dart中的main()開始,講解dart層面的初始化啓動過程

void main() => runApp(MyApp());

  接着是binding.dart中的runApp(),這裏是核心。這裏也是runApp啓動的三個主流程,我們從這三行代碼來一一解析。

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();
}

2.1-binding初始化

  WidgetsFlutterBinding.ensureInitialized()其實就是一個獲取WidgetsFlutterBinding單例的過程,真正的初始化實現代碼在其7個mixin中。7個mixin分別完成不同部分的初始化工作,且根據mixin機制具有嚴格的先後調用鏈關係。至於這7個mixin的具體分工我們後面再細說。

class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
	static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null)
      WidgetsFlutterBinding();
    return WidgetsBinding.instance;
  }
}

  WidgetsFlutterBinding繼承了BindingBase,而mixin是沒有構造函數的。所以先執行了父類BindingBase構造函數。

BindingBase() {
  developer.Timeline.startSync('Framework initialization');
  assert(!_debugInitialized);
  initInstances();
  assert(_debugInitialized);
  assert(!_debugServiceExtensionsRegistered);
  initServiceExtensions();
  assert(_debugServiceExtensionsRegistered);
  developer.postEvent('Flutter.FrameworkInitialization', <String, String>{});
  developer.Timeline.finishSync();
}

  7個mixin都重寫了initInstances()方法,BindingBase.initInstances()會從最後混入的WidgetsBinding進行調用,而WidgetsBinding的initInstances函數中先通過super向上調用,屬於後續遍歷,所以調用順序和函數邏輯執行順序是相反的。回過頭看看第1章最後的例3,是不是很像。調用鏈如圖:
mixin2.png
  由於是通過super實現了後序遍歷的調用,所以函數的邏輯執行順序是相反的,BindingBase的initInstances先執行,然後是GestureBinding…最後到WidgetsBinding,依次完成了各mixin的相關初始化工作。
  (1)GestureBinding.initInstances 手勢事件綁定。進行一些變量初始化。GestureBinding中主要處理觸屏幕指針事件的分發以及事件最終回調處理。

void initInstances() {
  super.initInstances();
  _instance = this;
  //將事件處理回調賦值給window,供window收到屏幕指針事件後調用
  window.onPointerDataPacket = _handlePointerDataPacket;
}

  這裏將事件處理回調_handlePointerDataPacket賦值給window,供window收到屏幕指針事件後調用。window類似Android中的WindowManager,是framework層與engine層處理屏幕相關事件的橋樑。
  發生屏幕指針事件後會回調window.onPointerDataPacket即這裏的_handlePointerDataPacket。_handlePointerDataPacket中會先調用hitTest進行命中測試。GestureBinding及RenderBinding都實現了hitTest方法,按照mixin順序會優先調用RenderBinding.hitTest。RenderBinding.hitTest會從renderTree的根節點遞歸調用命中測試,返回命中的深度最大的節點到根節點路徑上的所有節點。然後再執行dispatchEvent根據返回的hitTest命中節點列表遍歷分發事件,事件分發的順序是先子節點後父節點最終到根節點,類似前端的事件冒泡機制。
  (2)ServicesBinding.initInstances Flutter與Platform通信服務綁定。

void initInstances() {
  super.initInstances();
  _instance = this;
  //構建一個_DefaultBinaryMessenger實例用於platform與flutter層通信,消息信使
  _defaultBinaryMessenger = createBinaryMessenger();
  //window設置監聽回調,處理platform發送的消息
  window.onPlatformMessage = defaultBinaryMessenger.handlePlatformMessage;
  initLicenses();
  //設置處理platform發送的系統消息
  SystemChannels.system.setMessageHandler(handleSystemMessage);
}

  ServicesBinding主要就是platform與flutter層通信相關服務的初始化,BinaryMessenger作爲二者之間通信的信使,在這裏被初始化,且同樣是交給window來處理消息。最後設置處理system消息handleSystemMessage,而ServicesBinding的handleSystemMessage是空實現,PaintingBinding及WidgetsBinding都實現了該方法。調用順序是WidgetsBinding.handleSystemMessage–>PaintingBinding.handleSystemMessage–>ServicesBinding.handleSystemMessage。同樣是通過super後續遍歷調用,先在PaintingBinding中處理系統字體變動事件,後在WidgetsBinding中處理系統發送的內存緊張信號。
  (3)SchedulerBinding.initInstances 繪製調度綁定

void initInstances() {
  super.initInstances();
  _instance = this;
  //設置AppLifecycleState生命週期回調
  SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
  //根據生命週期變化設置window處理回調
  //resumed || inactive狀態時才允許響應Vsync信號進行繪製
  readInitialLifecycleStateFromNativeWindow();
  //debug編譯模式時統計繪製流程時長,開始、運行、構建、光柵化。
  if (!kReleaseMode) {
    int frameNumber = 0;
    addTimingsCallback((List<FrameTiming> timings) {
      for (final FrameTiming frameTiming in timings) {
        frameNumber += 1;
        _profileFramePostEvent(frameNumber, frameTiming);
      }
    });
  }
}

  SchedulerBinding.initInstances 主要就是註冊監聽了flutter app的生命週期變化事件,根據生命週期狀態決定是否允許發起繪製任務。而SchedulerBinding的作用就是在window監聽到Vsync信號後,通過SchedulerBinding來發起繪製任務。
  (4)PaintingBinding 繪製綁定。除了前面講的監聽系統字體變化事件,這裏主要是在繪製熱身幀之前預熱Skia渲染引擎。

void initInstances() {
  super.initInstances();
  _instance = this;
  //初始化圖片緩存
  _imageCache = createImageCache();
  if (shaderWarmUp != null) {
    //第一幀繪製前的預熱工作
    shaderWarmUp.execute();
  }
}

  (5)SemanticsBinding.initInstances 渲染輔助類綁定。SemanticsBinding主要負責關聯語義樹與Flutter Engine。

void initInstances() {
  super.initInstances();
  _instance = this;
  _accessibilityFeatures = window.accessibilityFeatures;
}

  (6)RendererBinding.initInstances 渲染綁定,RendererBinding是render tree 與 Flutter engine的粘合劑,因爲它持有了render tree的根節點renderView。

void initInstances() {
  super.initInstances();
  _instance = this;
  //初始化PipelineOwner管理渲染流程
  _pipelineOwner = PipelineOwner(
    onNeedVisualUpdate: ensureVisualUpdate,
    onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
    onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
  );
  //設置window回調,處理屏幕參數、文本縮放因子、亮度等變化時回調。
  window
    ..onMetricsChanged = handleMetricsChanged
    ..onTextScaleFactorChanged = handleTextScaleFactorChanged
    ..onPlatformBrightnessChanged = handlePlatformBrightnessChanged
    ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
    ..onSemanticsAction = _handleSemanticsAction;
  //初始化一個RenderView作爲render tree的根節點,作爲渲染流水線執行入口
  initRenderView();
  //設置是否根據render tree生成語義樹
  _handleSemanticsEnabledChanged();
  assert(renderView != null);
  //繪製流水線回調
  addPersistentFrameCallback(_handlePersistentFrameCallback);
  initMouseTracker();//鼠標監聽
}

  回過頭看看(1)GestureBinding.initInstances方法中的事件處理,調用的就是這裏的renderView.hitTest從根節點開始命中測試的。正因爲RenderBinding創建並持有了RenderView實例,所以GestureBinding中通過mixin機制將RenderBinding的hitTest方法混入,從而可以實現命中測試,相當於需要用到命中測試的地方都通過mixin委託給RenderBinding來實現了。
  addPersistentFrameCallback將繪製處理回調_handlePersistentFrameCallback加入到Persistent類型回調列表,_handlePersistentFrameCallback中的drawFrame方法是實現繪製流水線的地方,包括佈局和繪製流程,後面繪製熱身幀會用到。
  (7)WidgetsBinding.initInstances 組件綁定

void initInstances() {
  super.initInstances();
  _instance = this;
  assert(() {
    _debugAddStackFilters();
    return true;
  }());
	//初始化BuildOwnder,處理需要繪製的Element的構建工作
  _buildOwner = BuildOwner();
  //通過SchedulerBinding初始化window的onBeginFrame、onDrawFrame回調
  //如果app可見,通過window.scheduleFrame向engine發起繪製請求
  buildOwner.onBuildScheduled = _handleBuildScheduled;
  //語言環境變化處理
  window.onLocaleChanged = handleLocaleChanged;
  //platform訪問權限變化處理
  window.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
  //處理系統發送的push/pop頁面請求
  SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
  FlutterErrorDetails.propertiesTransformers.add(transformDebugCreator);
}

  WidgetsBinding屬於最外層的mixin,作爲處理Widget相關事件的入口。在初始化過程中主要是生成了BuildOwner實例,以及window的onBeginFrame、onDrawFrame回調,後面渲染流程會用到。
  BindingBase先通過按順序執行7個mixin的initInstances方法,完成了相關初始化工作,以及兩個重要類的實例化PipelineOwner、BuildOwner。然後就是執行了initServiceExtensions方法,實現了該方法的mixin按調用順序爲WidgetsBinding–>RendererBinding–>SchedulerBinding–>ServicesBinding主要就是在debug模式下注冊相關拓展服務。

2.2-綁定根節點

  ensureInitialized完成後,就開始執行scheduleAttachRootWidget(app)將用戶傳入的Widget綁定到一個跟節點並構建三棵樹。

void scheduleAttachRootWidget(Widget rootWidget) {
  Timer.run(() {
    attachRootWidget(rootWidget);
  });
}

  由於是組件相關,attachRootWidget具體的實現在WidgetsBinding裏

void attachRootWidget(Widget rootWidget) {
  _readyToProduceFrames = true;
  _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: renderView,
    debugShortDescription: '[root]',
    child: rootWidget,
  ).attachToRenderTree(buildOwner, renderViewElement as RenderObjectToWidgetElement<RenderBox>);
}

  類似Android中將DecorView與ViewRootImpl綁定,通過ViewRootImpl來作爲視圖操作根節點入口。Flutter中也是將app的主widget(即用戶定義的MyApp)和根節點綁定。其中render tree的根節點就是前面初始化流程中RendererBinding.initInstances過程創建的RenderView,RenderView是繼承自RenderObject的,所以還需要創建Element和Widget與之關聯,而創建的Element和Widget分別對應另外兩棵樹的根節點。
  (1)先是通過傳入的MyApp及RenderView實例化了一個RenderObjectToWidgetAdapter對象,而RenderObjectToWidgetAdapter是繼承自RenderObjectWidget,即創建了Widget樹的根節點。
  (2)createElement創建根element,並通過BuildOwner構建需要構建的element

RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) {
  if (element == null) {
    owner.lockState(() {
      //創建了一個RenderObjectToWidgetElement實例作爲element tree的根節點
      element = createElement();
      assert(element != null);
      //綁定BuildOwner
      element.assignOwner(owner);
    });
    //標記需要構建的element,並rebuild
    owner.buildScope(element, () {
      element.mount(null, null);
    });
    SchedulerBinding.instance.ensureVisualUpdate();
  } else {
    element._newWidget = this;
    element.markNeedsBuild();
  }
  return element;
}

2.3-繪製熱身幀

  綁定完根節點後,就開始立即執行scheduleWarmUpFrame()繪製首幀的工作了。前面window.scheduleFrame發起繪製請求是在收到Vsync信號後纔開始的,app初始化時爲了節省時間並未等待Vsync信號直接開始繪製,所以叫熱身Frame。和普通繪製一樣,熱身幀也是通過handleBeginFrame、handleDrawFrame這兩個回調來進行繪製流程,在前面WidgetBinding初始化時將這兩個回調交給了window,具體代碼邏輯是在SchedulerBinding。

void scheduleWarmUpFrame() {
  if (_warmUpFrame || schedulerPhase != SchedulerPhase.idle)
    return;
  _warmUpFrame = true;
  Timeline.startSync('Warm-up frame');
  final bool hadScheduledFrame = _hasScheduledFrame;
  // Timer任務會加入到event queue
  // 所以在執行繪製前先處理完microtask queue中的任務
  Timer.run(() {
    assert(_warmUpFrame);
		// 繪製Frame前工作,主要是處理Animate動畫
    handleBeginFrame(null);
  });
  // 繪製前有機會執行完microtask queue
  Timer.run(() {
    assert(_warmUpFrame);
    // 開始Frame繪製
    handleDrawFrame();
    resetEpoch();
    _warmUpFrame = false;
    if (hadScheduledFrame)
      //後續Frame繪製請求
      scheduleFrame();
  });
  lockEvents(() async {
    await endOfFrame;
    Timeline.finishSync();
  });
}

  handleBeginFrame處理動畫相關邏輯,動畫回調後並不立即執行動畫,而是改變了animation.value,並調用setSate()來發起繪製請求。動畫的過程就是在Vsync信號到來時根據動畫進度計算出對應的value,而對應的Widget也會隨着animation.value的變化而重建,從而形成動畫,是不是和Android的屬性動畫原理差不多。

void handleBeginFrame(Duration rawTimeStamp) {
    ...
    _hasScheduledFrame = false;
    try {
      // 處理回調前設置爲瞬態
      _schedulerPhase = SchedulerPhase.transientCallbacks;
      final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
      _transientCallbacks = <int, _FrameCallbackEntry>{};
      //處理Animation回調
      callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
        if (!_removedIds.contains(id))
          _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack);
      });
      _removedIds.clear();
    } finally {
        //回調處理完,設置爲中間態,即先處理microTask任務隊列
      _schedulerPhase = SchedulerPhase.midFrameMicrotasks;
    }
}

  handleBeginFrame處理完後,會優先處理microTask任務隊列。然後纔是event Task,window.onDrawFrame(),對應SchedulerBinding.handleDrawFrame()。(Timer任務會加入到event queue,flutter的事件處理機制是優先處理micro queue中任務)

void handleDrawFrame() {
    try {
      // 處理Persistent類型回調,主要包括build\layout\draw流程
      _schedulerPhase = SchedulerPhase.persistentCallbacks;
      for (FrameCallback callback in _persistentCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp);
    
      // 處理Post-Frame回調,主要是狀態清理,準備調度下一幀繪製請求
      _schedulerPhase = SchedulerPhase.postFrameCallbacks;
      final List<FrameCallback> localPostFrameCallbacks =
          List<FrameCallback>.from(_postFrameCallbacks);
      _postFrameCallbacks.clear();
      for (FrameCallback callback in localPostFrameCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp);
    } finally {
        //處理完成,狀態idle
      _schedulerPhase = SchedulerPhase.idle;
      _currentFrameTimeStamp = null;
    }
}

  WidgetsBinding.drawFrame()爲Persistent類型的一個回調,在前面講到的RendererBinding初始化時通過addPersistentFrameCallback中加入了RendererBinding.drawFrame,所以這裏也是用到了mixin機制,在WidgetsBinding.drawFrame()中完成組件的構建任務,在RendererBinding.drawFrame完成組件的佈局、繪製任務。是不是分工明確。

//WidgetsBinding.drawFrame()
void drawFrame() {
   try {
    if (renderViewElement != null)
      //調用BuildOwner.buildScope開始構建
      buildOwner.buildScope(renderViewElement);
      //調用RendererBinding.drawFrame,開始佈局、繪製階段。
    super.drawFrame();
    //從element tree中移除不需要的element,unmount
    buildOwner.finalizeTree();
  } finally {
     ...
  }
}

  繪製流程結束後會產生這一幀的數據Scene,由window.render交給Engine,最終顯示到屏幕。整個熱身幀繪製流程如圖:
繪製.png

總結

(1) mixin機制在FlutterApp啓動過程可謂秀的飛起,通過如上分析也得到了mixin機制帶來的優勢有哪些:

  • 高內聚低耦合。適合應用於需要多個功能模塊配合完成的場景,將功能模塊通過mixin解耦,各模塊職責單一,相互之間不直接引用。
  • 代碼複用。通過混入模塊,就可以像調用自身方法一樣調用混入模塊的方法。
  • 保證調用順序。mixin配合super調用,可以實現同名方法的“繼承鏈”式調用,保證串行執行順序。


(2)Flutter App的啓動過程總結:

  • ensureInitialized 通過7個mixin 按順序完成相關初始化工作
  • scheduleAttachRootWidget 綁定app 應用啓動Widget到根節點,主要是render tree的根節點RenderView,RenderView又關聯了widget tree 的根節點和 element tree的根節點
  • scheduleWarmUpFrame 立即完成首幀繪製


參考文章:
[1] Dart: What are mixins?
[2] Dart 2 Mixin Declarations
[3] 徹底理解 Dart mixin 機制

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章