flutter系列之:flutter架構什麼的,看完這篇文章就全懂了

簡介

Flutter是google開發的一個跨平台的UI構建工具,flutter目前最新的版本是3.0.5。使用flutter你可以使用一套程式碼搭建android,IOS,web和desktop等不同平台的應用。做到一次編寫到處運行的目的。

說到一次編寫處處運行,大家可能會想到java。那麼flutter跟java是不是類似呢?

對於JAVA來說,在編寫完JAVA程式碼之後,將其編譯成為class位元組碼,然後這個class位元組碼就可以不需要進行任何轉換的在任何平台上運行。其底層原理是JAVA開發了適配不同作業系統和平台的JVM,class實際運行在JVM中,所以對底層到底運行在哪個平台是無感的。一切的適配都是由JVM來執行的。

Flutter其實更像是C或者C++,雖然程式碼是一樣的,但是需要根據不同的平台編譯成不同的二進位文件。而Flutter也是一樣,雖然我們使用同一套dart程式碼編寫了Flutter程式,但是需要不同的命令編譯成不同平台的命令和安裝包。

當然,在開發過程中,flutter提供了虛擬機,實現了hot reload的功能,在程式碼進行修改之後,可以立刻重載,而不需要重新編譯整個程式碼。

FLutter這麼神奇,那麼它到底是怎麼工作的呢?

Flutter的架構圖

我們先來看下Flutter的架構圖,當然這個架構圖是官方來的,官方的架構圖表示的是權威:

從上圖中,我們可以看到Flutter的架構可以分為三部分,從下到上分別是embedder,Engine和Framework。

embedder

embedder可以稱為嵌入器,這是和底層的作業系統進行交互的部分。因為flutter最終要將程式打包到對應的平台中,所以這個嵌入器需要和底層的平台介面進行交互。

具體而言,對於Android平台使用的是Java和C++,對於iOS和macOS平台,使用的是Objective-C/Objective-C++,對應Windows平台和Linux平台的是C++。

為什麼C++這麼強大? 這裡就可以看出來了,基本上所有底層的東西都是用C++寫的。

回到embedder,為什麼叫做嵌入器呢?這是因為Flutter打包的程式,可以作為整個應用程式,也可以作為現有程式的一部分被嵌入使用。

engine

engine也叫做flutter engine,它是flutter中最核心的部分。

Flutter engine基本上使用C++寫的。engine的存在是為了支援Dart Framework的運行。它提供了Flutter的核心API,包括作圖、文件操作、網路IO、dar運行時環境等核心功能。

engine主要是通過dart:ui暴露給Flutter framework層的。

Flutter framework

這一層是用戶編程的介面,我們的應用程式需要和Flutter framework進行交互,最終構建出一個應用程式。

Flutter framework主要是使用dart語言來編寫的。

framework從下到上,我們有最基礎的foundational包,和構建在其上的 animation, painting和 gestures 。

再上面就是rendering層,rendering為我們提供了動態構建可渲染對象樹的方法,通過這些方法,我們可以對布局進行處理。

接著是widgets layer,它是rendering層中對象的組合,表示一個小掛件。

最後是Material和Cupertino庫,這些庫使用widegts層中提供的小部件,組合成了不同風格的控制項集。

Flutter framework就是這樣一層層的構建起來的。

當然,上面的embedder和engine屬於比較底層的東西,我們只需要知道Flutter有這麼一個東西,是這麼使用的即可。

真正和我們程式設計師相關的,就是Flutter framework了。因為我們在編寫程式碼的過程中,需要和Flutter framework打交道。

接下來,我們重點關注下Flutter framework中的幾個核心部分。

Widgets

Widgets翻譯成中文就是小插件的意思。Widgets是Flutter中用戶介面的基礎。你在flutter介面中能夠觀察到的用戶介面,都是Widgets。

當然這些大的Widgets又是由一個個的小的Widgets組成的,而這些小的Widgets又是由更小的Widgets組成的。

這樣就構成了Widgets的層次依賴結構,這些層次結構的關聯關係是通過Widget中的child Widget進行關聯的。

在這種層次結構中,子Widgets可以共享父Widgets的上下文環境。

Flutter中的Widgets跟其他語言中的類似的Widgets組合有什麼不同呢?

他們最大的不同是,Flutter中的Widgets更多,每個Widgets專註的功能更小。即便是一個很小很小功能,在Flutter中都可以找到與之對應的Widgets。

這樣做的好處就是,你可以使用不同的,非常基礎的Widgets任意組合,從而構建出非常複雜的,個性化的大的Widgets。

當然,它的缺點也非常明顯,就是程式碼裡面的Widgets太多了,導致程式碼中的層級結構特別的多,可能會看的眼花繚亂。

舉個簡單的例子,Container是flutter提供的一個基本的容器Widget,我們通常這樣來使用它:

 Container(
   constraints: BoxConstraints.expand(
     height: Theme.of(context).textTheme.headline4!.fontSize! * 1.1 + 200.0,
   ),
   padding: const EdgeInsets.all(8.0),
   color: Colors.blue[600],
   alignment: Alignment.center,
   child: Text('Hello World',
     style: Theme.of(context)
         .textTheme
         .headline4!
         .copyWith(color: Colors.white)),
   transform: Matrix4.rotationZ(0.1),
 )

我們向Container中傳入了constraints,padding,color,alignment,child,transform等資訊。

我們先來猜一下,這些資訊中,哪些是用來構建Widget的?

大家第一時間想到的應該是child,它本身就是一個Widget,用來表示Container中包含的子對象,這個很好理解。

但是,除了child這個Widget之外,其他的constraints,padding,color,alignment,transform等都是構成Widget的元素!

我們來看下Container的build方法:

 Widget build(BuildContext context) {
    Widget? current = child;

    if (child == null && (constraints == null || !constraints!.isTight)) {
      current = LimitedBox(
        maxWidth: 0.0,
        maxHeight: 0.0,
        child: ConstrainedBox(constraints: const BoxConstraints.expand()),
      );
    }

    if (alignment != null)
      current = Align(alignment: alignment!, child: current);

    final EdgeInsetsGeometry? effectivePadding = _paddingIncludingDecoration;
    if (effectivePadding != null)
      current = Padding(padding: effectivePadding, child: current);

    if (color != null)
      current = ColoredBox(color: color!, child: current);

    if (clipBehavior != Clip.none) {
      assert(decoration != null);
      current = ClipPath(
        clipper: _DecorationClipper(
          textDirection: Directionality.maybeOf(context),
          decoration: decoration!,
        ),
        clipBehavior: clipBehavior,
        child: current,
      );
    }

    if (decoration != null)
      current = DecoratedBox(decoration: decoration!, child: current);

    if (foregroundDecoration != null) {
      current = DecoratedBox(
        decoration: foregroundDecoration!,
        position: DecorationPosition.foreground,
        child: current,
      );
    }

    if (constraints != null)
      current = ConstrainedBox(constraints: constraints!, child: current);

    if (margin != null)
      current = Padding(padding: margin!, child: current);

    if (transform != null)
      current = Transform(transform: transform!, alignment: transformAlignment, child: current);

    return current!;
  }

從程式碼中可以看到,Container先是創建了LimitedBox,然後將其嵌入到Align中,再依次嵌入到Padding,ColoredBox,ClipPath,DecoratedBox,ConstrainedBox,Padding和Transform中。這些所有的對象都是Widget。

這裡應該可以理解Flutter中Widget的設計思想了。在Flutter中一切皆可為Widget。

Widgets的可擴展性

和其他的編譯成原生語言特性的跨平台實現如React native相比,Flutter對於每個UI都有自己的實現,而不是依賴於作業系統提供的介面。

這樣做的好處就是一切都是由Flutter自己控制的,使用者可以在Flutter的基礎上進行無限擴展,而不用受限於系統底層的實現限制。

另一方面,這樣可以減少Flutter在呈現過程中在Flutter程式碼和平台程式碼之間來迴轉換,減少了性能瓶頸,提升效率。

最後,因為UI的實現和底層的作業系統是分離的,所以Flutter的APP在不同的平台上面可以有統一的外觀和實現,可以保證風格的統一。

Widgets的狀態管理

Widgets表示的是不可變的用戶UI介面結構。雖然結構是不能夠變化的,但是Widgets裡面的狀態是可以動態變化的。

根據Widgets中是否包含狀態,Widgets可以分為stateful和stateless widget,對應的類是StatefulWidget和StatelessWidget。

對於有些Widgets來說,比如icon或者Label,它裡面本身就不需要狀態,這些Widgets就是StatelessWidget。

但是如果有些Widgets中的某些內容可能需要根據用戶或者其他原因來動態變化,則就需要使用StatefulWidget。

之前提到了Widgets是不可變的,StatefulWidget中的可變數據是存放在對應的State中的,所以StatefulWidgets本身並沒有build方法,所有用戶介面都是通過State對象來構建的。

當State發生變化的時候,需要調用setState() 方法來通知flutter框架來調用State的build方法,從而將變化回饋到用戶介面中。

既然StatefulWidget是帶有狀態的,那麼這些狀態是怎麼進行管理和傳遞的呢?

State本身提供了一個build方法,用於構建初始的狀態:

Widget build(BuildContext context);

如果在一個StatefulWidget中需要嵌入另外一個StatefulWidget,那麼可以在其對應的State中調用另外一個StatefulWidget的構造函數,將要傳遞的數據,以構造函數參數的形式傳遞給子Widget。

當然這樣做是沒問題的。但是如果組件的嵌套層數過多的話,這種構造函數的傳遞方式,顯然不能滿足我們的需求。

於是Flutter提供了一個InheritedWidget類,如果我們自定義的類需要共享數據給子Widgets,則可以繼承InheritedWidget。

Inherited widgets有兩個作用: 第一,子Widget可以通過Inherited widgets提供的靜態of方法拿到離他最近的父Inherited widgets實例。

第二,當Inherited widgets改變state之後,會自動觸發state消費者的rebuild行為。

先來看一下inherited widgets類的定義:

abstract class InheritedWidget extends ProxyWidget {

  const InheritedWidget({ Key? key, required Widget child })
    : super(key: key, child: child);

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

  @protected
  bool updateShouldNotify(covariant InheritedWidget oldWidget);
}

可以看到InheritedWidget是對實際Widget對象的代理,另外還將InheritedWidget封裝到了InheritedElement中。

這裡不多講解InheritedElement,InheritedElement是底層通知機制的實現。

我們看到InheritedWidget還添加了一個updateShouldNotify,這個方法可以提供給我們控制當前InheritedWidget rebuilt的時候,是否需要rebuilt繼承它的子Widget。

下面我們看一個InheritedWidget的具體實現:

class FrogColor extends InheritedWidget {
  const FrogColor({
    Key? key,
    required this.color,
    required Widget child,
  }) : super(key: key, child: child);

  final Color color;

  static FrogColor of(BuildContext context) {
    final FrogColor? result = context.dependOnInheritedWidgetOfExactType<FrogColor>();
    assert(result != null, 'No FrogColor found in context');
    return result!;
  }

  @override
  bool updateShouldNotify(FrogColor old) => color != old.color;
}

FrogColor中定義了一個Color屬性,當Color發生變化的時候,就會調用updateShouldNotify。

另外,FrogColor還提供了一個of方法,接受的參數是BuildContext,然後調用context.dependOnInheritedWidgetOfExactType去查找離該context最近的FrogColor。

為什麼要使用of方法對context.dependOnInheritedWidgetOfExactType進行封裝呢?這是因為,context.dependOnInheritedWidgetOfExactType方法不一定能夠找到要找的對象,所以我們需要進行一些異常值的處理。

另外,有可能of方法返回的對象和context.dependOnInheritedWidgetOfExactType中查找的對象不一樣,這都是可以的。

我們看下of方法的具體使用:

class MyPage extends StatelessWidget {
  const MyPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FrogColor(
        color: Colors.green,
        child: Builder(
          builder: (BuildContext innerContext) {
            return Text(
              'Hello Frog',
              style: TextStyle(color: FrogColor.of(innerContext).color),
            );
          },
        ),
      ),
    );
  }
}

還有一個問題,of方法傳入的是BuildContext對象,注意,這裡的BuildContext必須是InheritedWidget對象本身的後輩,也就是說在對象樹中,必須是InheritedWidget的子樹。再看下面的例子:

class MyOtherPage extends StatelessWidget {
  const MyOtherPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FrogColor(
        color: Colors.green,
        child: Text(
          'Hello Frog',
          style: TextStyle(color: FrogColor.of(context).color),
        ),
      ),
    );
  }
}

這個例子中,FrogColor.of方法中的context是FrogColor的父context,所以是找不到FrogColor對象的,這樣的使用是錯誤的。

當然,除了InheritedWidget,Flutter還提供了很多狀態管理的工具,比如provider,bloc,flutter_hooks等,也是非常好用的。

渲染和布局

渲染就是將上面我們提到的widgets轉換成用戶肉眼可以感知的像素的過程。

Flutter作為一種跨平台的框架,它和普通的跨平台的框架或者原生的框架有什麼區別呢?

首先來考慮一下原生框架。以android為例,首先調用的是andorid框架的java程式碼,通過調用android系統庫提供的進行繪製的組件,最後調用底層的Skia來進行繪製。Skia 是一種用 C/C++ 編寫的圖形引擎,它調用 CPU 或 GPU 在設備上完成繪製。

那麼常見的跨平台框架是怎麼運行的呢?它們實際上在原生的程式碼框架上面又封裝了一層。通常使用javascript這樣的解釋性語言來進行編寫,然後編寫的程式碼再和andorid的JAVA或者IOS的Objective-C系統庫進行交互。這樣的結果就是在UI交互或者調用之間會造成顯著的性能開銷。這也就是通用的跨平台語言不如原生的性能好的原因。

但是flutter不一樣,它並不是用系統自帶的UI控制項,而是擁有自己的實現。Flutter程式碼會直接被編譯成使用 Skia 進行渲染的原生程式碼,從而提升渲染效率。

接下來,我們具體看一下flutter從程式碼到渲染的整個流程。首先看一段程式碼:

Container(
  color: Colors.blue,
  child: Row(
    children: [
      Image.network('//www.flydean.com/1.png'),
      const Text('A'),
    ],
  ),
);

上面的程式碼是構建一個Container widget。當flutter想要渲染這個widget的時候,會去調用build() 方法,然後生成一個widget集合.

為什麼是Widget集合呢?在上面我們也分析過,Container這個widget是由很多個其他的widget組成的,所以,上面的Container會生成下面的widget樹:

上面的就是程式碼中生成的widget,這些widget在build的過程中,會被轉換為 element tree。一個element和一個widget對應。

element表示的widget的實例。flutter中有兩種類型的element,分別是:ComponentElement和RenderObjectElement.

ComponentElement是其他Element的容器,而RenderObjectElement是真正參與layout和渲染的element。

因為Widget本身是不可變的,所以任何對於Widget的修改都會返回一個新的Widget。那麼是不是所有的變動,都會導致整個element tree重新渲染呢?

答案是不會的,flutter僅會重新渲染需要被重新繪製的element。

接下來,我們看下渲染樹是怎麼構建的,渲染樹中的每個元素叫做RenderObject,它定義了布局和繪製的抽象模型。

上面我們提到的RenderObjectElement會在渲染的時候轉換成為RenderObject,如下所示:

當然,不同的Render element會轉換成為不同的Render對象。

總結

Widget和Layout是我們實際在做flutter開發的時候,經常需要使用到的部分,大家需要深入了解和熟練掌握。

更多內容請參考 //www.flydean.com/01-flutter-architectural/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!