技術雜談 | Flutter 的性能分析、工程架構與細節處理

出品/ 有道智雲
編輯/ Ryan
來源:有道技術團隊(ID:youdaotech)

1、爲什麼 Flutter

跨端技術衆多,爲什麼選擇 Flutter?它能帶來哪些優點,有哪些缺點?html

先看看具體的工程效果:前端

Flutter 工程效果​v.qq.comreact

web 端效果體驗:android

flutter_exerciseios

1.1 Flutter VS 原生

不管如何,原生的運行效率毋庸置疑是最高的,可是從工程工做量的角度來對比的話,特別是快速試錯和業務擴展階段,Flutter 是目前爲止比較推薦的利器。git

1.2 Flutter VS Web

任何跨端的技術都是基於一碼多端的思惟,解決工程效率的問題,以前不少的跨端技術,例如 React Native 等都是基於web的跨端性解決方案,可是你們都知道,web 在移動端上的運行效率和 PC 上有巨大差距的,這就致使 RN 不能頗有效地在移動端完成各類複雜的交互式運算(例如複雜的動畫運算,交互的執行性能等),即使是引入了 Airbnb 的 Lottie 引擎依然會在低端的手機上面顯得很卡頓(固然也可使用一些自研的引擎技術來針對各端來解決,不過這樣就失去了跨端的意義)。github

1.3 Flutter 性能

Flutter 的編譯方式和產物是決定其高效運行效率的前提,不一樣於 web 的跨端編譯同樣(web 的跨端編譯大可能是選擇了使用 "橋" 的概念來調用編譯產物,一般是使用了原生端的入口 + web 端的橋來實現),Flutter 幾乎是把 dart 的源碼經過不一樣平臺的編譯原理生成各平臺的產物,這種"去橋"的產物正式咱們所但願獲得的、貼近原生運行性能的編譯產物(固然,在 dart 最初設計的時候,是參考了不少前端的結構來完成的,特別從語法上面可以很明顯地感覺到前端的痕跡,並且最初的 dart2js 的原理也是一樣"橋"的概念)。web

例如 9月23號 google 發佈的新 Flutter 版本中,在支持的 Windows 編譯產物上,就是經過相似 Visual Studio 的編譯工具(若是要將你的 Flutter 工程編譯成 Windows 產物,須要提早安裝一些 VS 相關的編譯插件),生成了 Windows 下的工程解決方案 .sln,最終生成 dll 的調用方式,運行起來很流暢,能夠下載附件中的 Release.zip 來嘗試運行。(Release.zip 下載chrome

(PS:這裏全部編譯工程都是經過同一套代碼完成,包括上文中的 web 地址、移動端案例還有這裏的 Windows 案例)編程

1.4 與 RN 的性能對比

以上是一樣功能模塊下,Flutter 和 RN 的一些數據上的對比,是從衆多的數據中抽取出來比較有表明性的一組。

1.5 跨端平臺的多樣性

1.6 引擎

Flare-Flutter 是一款十分優秀的 Flutter 動畫引擎,編譯出的動畫已經在 Windows、移動端、web 上親測驗證過。

1.7 語法糖

A?.B
若是 A 等於 null,那麼 A?.B 爲 null
若是 A 不等於 null,那麼 A?.B 等價於 A.B
Animal animal = new Animal('cat');
Animal empty = null;
//animal 非空,返回 animal.name 的值 cat
print(animal?.name);
//empty 爲空,返回 null
print(empty?.name);
A??B
若是 A 等於 null,那麼 A??B 爲 B
若是 A 不等於 null,那麼 A??B 爲 A

1.8 綜合測評

1.9 互動應用

Flutter 生成的互動能夠嵌入到任何端中使用精簡的指令集進行互動,爲互動場景(教學場景等帶來巨大的但願),如下是直播同步互動的 demo 場景。

image

2、Flutter 業務架構

Flutter 中目前是沒有現成的 mvvm 框架的,可是咱們能夠利用 Element 樹特性來實現 mvvm。

2.1 ViewModel

abstract class BaseViewModel {
  bool _isFirst = true;
  BuildContext context;

  bool get isFirst => _isFirst;

  @mustCallSuper
  void init(BuildContext context) {
    this.context = context;
    if (_isFirst) {
      _isFirst = false;
      doInit(context);
    }
  }

  // the default load data method
  @protected
  Future refreshData(BuildContext context);

  @protected
  void doInit(BuildContext context);

  void dispose();
  
class ViewModelProvider<T extends BaseViewModel> extends StatefulWidget {
  final T viewModel;
  final Widget child;

  ViewModelProvider({
    @required this.viewModel,
    @required this.child,
  });

  static T of<T extends BaseViewModel>(BuildContext context) {
    final type = _typeOf<_ViewModelProviderInherited<T>>();
    _ViewModelProviderInherited<T> provider =
        // 查詢Element樹中緩存的InheritedElement
        context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
    return provider?.viewModel;
  }

  static Type _typeOf<T>() => T;

  @override
  _ViewModelProviderState<T> createState() => _ViewModelProviderState<T>();
}

class _ViewModelProviderState<T extends BaseViewModel>
    extends State<ViewModelProvider<T>> {
  @override
  Widget build(BuildContext context) {
    return _ViewModelProviderInherited<T>(
      child: widget.child,
      viewModel: widget.viewModel,
    );
  }

  @override
  void dispose() {
    widget.viewModel.dispose();
    super.dispose();
  }
}

// InheritedWidget能夠被Element樹緩存
class _ViewModelProviderInherited<T extends BaseViewModel>
    extends InheritedWidget {
  final T viewModel;

  _ViewModelProviderInherited({
    Key key,
    @required this.viewModel,
    @required Widget child,
  }) : super(key: key, child: child);

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) => false;

2.2 DataModel

import 'dart:convert';

import 'package:pupilmath/datamodel/base_network_response.dart';
import 'package:pupilmath/datamodel/challenge/challenge_ranking_list_item_data.dart';
import 'package:pupilmath/utils/text_utils.dart';

///歷史榜單
class ChallengeHistoryRankingListResponse
    extends BaseNetworkResponse<ChallengeHistoryRankingData> {
  ChallengeHistoryRankingListResponse.fromJson(Map<String, dynamic> json)
      : super.fromJson(json);

  @override
  ChallengeHistoryRankingData decodeData(jsonData) {
    if (jsonData is Map) {
      return ChallengeHistoryRankingData.fromJson(jsonData);
    }
    return null;
  }
}

class ChallengeHistoryRankingData {
  String props;
  int bestRank; //最佳排名
  int onlistTimes; //上榜次數
  int total; //總共挑戰數
  List<ChallengeHistoryRankingItemData> ranks; //先給10天

  //二維碼
  String get qrcode =>
      TextUtils.isEmpty(props) ? '' : json.decode(props)['qrcode'] ?? '';

  ChallengeHistoryRankingData.fromJson(Map<String, dynamic> json) {
    props = json['props'];
    bestRank = json['bestRank'];
    onlistTimes = json['onlistTimes'];
    total = json['total'];
    if (json['ranks'] is List) {
      ranks = [];
      (json['ranks'] as List).forEach(
          (v) => ranks.add(ChallengeHistoryRankingItemData.fromJson(v)));
    }
  }
}

///歷史戰績的item
class ChallengeHistoryRankingItemData {
  ChallengeRankingListItemData champion; //當天最好成績
  ChallengeRankingListItemData user;

  ChallengeHistoryRankingItemData.fromJson(Map<String, dynamic> json) {
    if (json['champion'] is Map)
      champion = ChallengeRankingListItemData.fromJson(json['champion']);
    if (json['user'] is Map)
      user = ChallengeRankingListItemData.fromJson(json['user']);
  }

2.3 View

import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:pupilmath/datamodel/challenge/challenge_history_ranking_list_data.dart';
import 'package:pupilmath/entity_factory.dart';
import 'package:pupilmath/network/constant.dart';
import 'package:pupilmath/network/network.dart';
import 'package:pupilmath/utils/print_helper.dart';
import 'package:pupilmath/viewmodel/base/abstract_base_viewmodel.dart';
import 'package:rxdart/rxdart.dart';

//每日挑戰歷史戰績
class ChallengeHistoryListViewModel extends BaseViewModel {
  BehaviorSubject<ChallengeHistoryRankingData> _challengeObservable =
      BehaviorSubject();

  Stream<ChallengeHistoryRankingData> get challengeRankingListStream =>
      _challengeObservable.stream;

  @override
  void dispose() {
    _challengeObservable.close();
  }

  @override
  void doInit(BuildContext context) {
    refreshData(context);
  }

  @override
  Future refreshData(BuildContext context) {
    return _loadHistoryListData();
  }

  _loadHistoryListData() async {
    Map<String, dynamic> parametersMap = {};
    parametersMap["pageNum"] = 1;
    parametersMap["pageSize"] = 10; //拿10天數據

    handleDioRequest(
      () => NetWorkHelper.instance
          .getDio()
          .get(challengeHistoryListUrl, queryParameters: parametersMap),
      onResponse: (Response response) {
        ChallengeHistoryRankingListResponse rankingListResponse =
            EntityFactory.generateOBJ(json.decode(response.toString()));

        if (rankingListResponse.isSuccessful) {
          _challengeObservable.add(rankingListResponse.data);
        } else {
          _challengeObservable.addError(null);
        }
      },
      onError: (error) => _challengeObservable.addError(error),
    );
  }

  Future<ChallengeHistoryRankingData> syncLoadHistoryListData(
    int pageNum,
    int pageSize,
  ) async {
    Map<String, dynamic> parametersMap = {};
    parametersMap["pageNum"] = pageNum;
    parametersMap["pageSize"] = pageSize;

    try {
      Response response = await NetWorkHelper.instance
          .getDio()
          .get(challengeHistoryListUrl, queryParameters: parametersMap);
      ChallengeHistoryRankingListResponse rankingListResponse =
          EntityFactory.generateOBJ(json.decode(response.toString()));
      if (rankingListResponse.isSuccessful) {
        return rankingListResponse.data;
      } else {
        return null;
      }
    } catch (e) {
      printHelper(e);
    }
    return null;
  }

2.4 一些基礎架構

2.5 View 和 ViewModel 如何實現初始化和相互做用

2.6 Flutter 業務架構抽離

若是是統一系列的產品業務形態,還能夠抽離出一套核心的架構,複用在一樣的生產產品線上,例如當前產品線以教育爲主,利用 Flutter 的一碼多端性質,則能夠把題版生產工廠、渲染題版引擎、 適配框架、 以及跨端接口的框架都抽離出來,迅速地造成能夠推廣複用的模板,能夠事半功倍地解決掉業務上的試錯成本問題,固然,其餘產品性質的業務線都可如此。

3、Flutter 適配

任何框架中的 UI 適配都是特別繁重的工做,跨端上的適配更是如此,所以在同一套佈局裏面,各個平臺的換算過程顯得尤其重要,起初的時候,Flutter 中並無提供某種諸如 dp 或者 sp 的適配方式,並且考慮到直接更改底層 Matrix 換算比例的話可能會讓本來高清分辨率的手機顯示不是那麼清楚,而 Flutter 的寬高單位都是 num,最後編譯的時候纔會去對應到各個平臺的單位尺寸。

爲了減輕設計師的設計負擔,這裏一般使用一套 iOS 的設計稿便可,以375 x 667的通用設計稿爲例,轉換過來到android上是360 x 640 (對應1080 x 1920),這裏flutter的單位也是和對應手機的像素密度有關的。

3.1 構造一個轉換工具類:

//目前適配iPhone和iPad機型尺寸
import 'dart:io';
import 'dart:ui';
import 'dart:math';

import 'package:pupilmath/utils/print_helper.dart';

bool initScale = false;
//針對iOS平臺的scale係數
double iosScaleRatio = 0;
//針對android平臺的scale係數
// (由於全部設計稿均使用iOS的設計稿進行,因此須要轉換爲android設計稿上的尺寸,
// 不然沒法進行小屏幕上的適配)
double androidScaleRatio = 0;
//文字縮放比
double textScaleRatio = 0;

const double baseIosWidth = 375;
const double baseIosHeight = 667;
const double baseIosHeightX = 812;

const double baseAndroidWidth = 360;
const double baseAndroidHeight = 640;

void _calResizeRatio() {
  if (Platform.isIOS) {
    final width = window.physicalSize.width;
    final height = window.physicalSize.height;
    final ratio = window.devicePixelRatio;
    final widthScale = (width / ratio) / baseIosWidth;
    final heightScale = (height / ratio) / baseIosHeight;
    iosScaleRatio = min(widthScale, heightScale);
  } else if (Platform.isAndroid) {
    double widthScale = (baseAndroidWidth / baseIosWidth);
    double heightScale = (baseAndroidHeight / baseIosHeight);
    double scaleRatio = min(widthScale, heightScale);
    //取兩位小數
    androidScaleRatio = double.parse(scaleRatio.toString().substring(0, 4));
  }
}

bool isFullScreen() {
  return false;
}

//縮放
double resizeUtil(double value) {
  if (!initScale) {
    _calResizeRatio();
    initScale = true;
  }

  if (Platform.isIOS) {
    return value * iosScaleRatio;
  } else if (Platform.isAndroid) {
    return value * androidScaleRatio;
  } else {
    return value;
  }
}

//縮放還原
//每一個屏幕的縮放比不同,若是在iOS設備上出題,則題目座標值須要換算成原始座標,加載的時候再經過不一樣平臺換算回來
double unResizeUtil(double value) {
  if (iosScaleRatio == 0) {
    _calResizeRatio();
  }

  if (Platform.isIOS) {
    return value / iosScaleRatio;
  } else {
    return value / androidScaleRatio;
  }
}

//文字縮放大小
_calResizeTextRatio() {
  final width = window.physicalSize.width;
  final height = window.physicalSize.height;
  final ratio = window.devicePixelRatio;
  double heightRatio = (height / ratio) / baseIosHeight / window.textScaleFactor;
  double widthRatio = (width / ratio) / baseIosWidth / window.textScaleFactor;
  textScaleRatio = min(heightRatio, widthRatio);
}

double resizeTextSize(double value) {
  if (textScaleRatio == 0) {
    _calResizeTextRatio();
  }
  return value * textScaleRatio;
}

double resizePadTextSize(double value) {
  if (Platform.isIOS) {
    final width = window.physicalSize.width;
    final ratio = window.devicePixelRatio;
    final realWidth = width / ratio;
    if (realWidth > 450) {
      return value * 1.5;
    } else {
      return value;
    }
  } else {
    return value;
  }
}

double autoSize(double percent, bool isHeight) {
  final width = window.physicalSize.width;
  final height = window.physicalSize.height;
  final ratio = window.devicePixelRatio;
  if (isHeight) {
    return height / ratio * percent;
  } else {
    return width / ratio * percent;
  }

3.2 具體使用:

這樣每次若是有分辨率變更或者適配方案變更的時候,直接修改 resizeUtil 便可,可是這樣帶來的問題就是,在編寫過程當中單位變得很冗長,並且不熟悉團隊工程的人會容易忘寫,致使查錯時間變長,代碼侵入性較高,因而利用 dart 語言的擴展函數特性,爲 resizeUtil 作一些改進。

3.3 低侵入式的 resizeUtil

經過擴展 dart 的 num 來構造想要的單位,這裏用 dp 和 sp 來舉例,在 resizeUtil 中加入擴展:

extension dimensionsNum on num {
  ///轉爲dp
  double get dp => resizeUtil(this.toDouble());

  ///轉爲文本大小sp
  double get sp => resizeTextSize(this.toDouble());

  ///轉爲pad文字適配
  double get padSp => resizePadTextSize(this.toDouble());

而後在佈局中直接書寫單位便可:

4、Flutter 中的一些坑

4.1 泛型上的坑

剛開始在移動端上使用泛型來作數據的自動解析時,使用了 T.toString 來判斷類型,可是當編譯成 web 的 release 版本時,在移動端正常運行的程序在web上沒法正常工做:

剛開始的時候把目標一直定位在編譯的方式上,由於存在 dev profile release 三種編譯模式,只有在 release 上沒法運行,誤覺得是 release 下編譯有 bug,隨着和 Flutter 團隊的深刻討論後,發現實際上是泛型在 release 模式下的坑,即在 web 版本的 release 模式下,一切都會進行壓縮(包含類型的定義),因此在 release 下,T.toString() 返回的是 null,所以沒法識別出泛型特徵,具體的討論連接:

Flutter application which use canvas to build self-CustomPainter cannot work on browser if i used the release mode by command "flutter run -d chrome --release" or "flutter build web". · Issue #47967 · flutter/flutter​github.com

In release mode everything is minified, the (T.toString() == "Construction2DEntity") comparison fails and you get entity null returned.
If you change the code to (T ==Construction2DEntity) it will fix your app.

最後建議,不管在何種模式下,都直接寫成T==的形式最爲安全。

class EntityFactory {
  static T generateOBJ<T>(json) {
    if (1 == 0) {
      return null;
    } else if (T == "ChallengeRankingListDataEntity") {
      /// 每日挑戰排行榜
      return ChallengeHomeRankingListResponse.fromJson(json) as T;
    } else if (T == "KnowledgeEntity") {
      return KnowledgeEntity.fromJson(json) as T;
    }
  }

4.2 在編譯成 web 產物後如何使用 iframe 來加載其餘網頁

對於移動端來講,webview_flutter 能夠解決掉加載 web 的問題,不過編譯成 web 產物後,已經沒法直接使用 WebView 插件來進行加載,此時須要用到 dart 最初設計來編寫網頁的一些方式,即 HtmlElmentView:

import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import 'dart:html' as html;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
           child: Iframe()
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: (){},
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), 
    );
  }
}
class Iframe extends StatelessWidget {
  Iframe(){
    ui.platformViewRegistry.registerViewFactory('iframe', (int viewId) {
      var iframe = html.IFrameElement();
      iframe.src='https://flutter.dev';
      return iframe;
  });
  }
  @override
  Widget build(BuildContext context) {
    return Container(
      width:400,
      height:300,
      child:HtmlElementView(viewType: 'iframe')
    );
  }

不過這種方式會帶來新的底層刷新渲染問題(當鼠標移動到某個元素時,會不停地閃動刷新),目前在新的版本上已修復,有興趣的同窗能夠看看:

https://github.com/flutter/fl...

4.3 Flutter 如何加載本地的 html 而且進行通訊

內置 html 是不少工程的需求,不少網上的資料都是經過把本地的 html 作成數據流的方式而後加載進來,這種作法的兼容性很很差,並且編寫過程當中容易出現不少文件流過大沒法讀取的問題,其實這些作法都不是很溫馨,咱們應該經過 IFrameElement 來進行加載並通訊,作法和前端很相似:

4.4 在 iOS 13.4 上 WebView 的手勢沒法正常使用

官方的 webview_flutter 在上一個版本當 iOS 升級到13.4以後會出現手勢被攔截且沒法正常使用的狀況,換成flutter_webview_plugin後暫時解決掉該問題(目前 WebView 已經作了針對性的修復,可是還未驗證),可是 flutter_webview_plugin 在 iOS 上又沒法寫入 user-agent,目前能夠經過修改本地的插件代碼進行解決:

文件位置爲:

flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_webview_plugin-0.3.11/ios/Classes/FlutterWebviewPlugin.m修改內容爲在146行(initWebview方法中初始化WKWebViewConfiguration後)添加以下代碼if (@available(iOS 9.0, *)) {if (userAgent != (id)[NSNull null]) {self.webview.customUserAgent = userAgent;}}

關於 webview_flutter 的手勢問題還在不斷的討論中:

https://github.com/flutter/fl...

5、關於佈局和運算

5.1 容器 Widget 和渲染 Widget

5.2 GlobalKey

經過 GlobalKey 獲取 RenderBox 來獲取渲染出的控件的 size 和 position 等參數:

5.3 浮點運算

在 dart 的浮點運算中,因爲都是高精度的 double 運算,當運算長度過長的時候,dart 會自動隨機最後的一位小數,這樣會致使每一次有些浮點運算每一次都是不肯定的,這時須要手動進行精度轉換,例如在計算兩條線段是否共線時:

5.4 Matrix 的平移和旋轉

在矩陣的換算過程當中,若是使用普通的matrix.translate,會致使 rotate 以後,再進行 translate 會在旋轉的基數上面作係數疊加平移運算,這樣計算後獲得的不是本身想要的結果,所以若是運算當中有 rotate 操做時,應當使用 leftTranslate 來保證每次運算的獨立性:

6、項目優化

6.1 避免 build() 方法耗時:

6.2 重繪區域優化:

6.3 儘可能避免使用 Opacity

6.4 Flutter的單線程模型

優先所有執行完 Microtask Queue 中的 Event,直到 Microtask Queue 爲空,纔會執行 Event Queue 中的 Event。

6.5 耗時方法放在 Isolate

Isolate 是 Dart 裏的線程,每一個 Isolate 之間不共享內存,經過消息通訊。

Dart 的代碼運行在 Isolate 中,處於同一個 Isolate 的代碼才能相互訪問。

7、雜談總結

經歷了對 Flutter 長期的探索和項目驗證,目前對 Flutter 有本身的一些雜談總結:

7.1

Flutter 在移動端的表現仍是很不錯的,在運行流暢度方面也是很是棒,通過優化事後的帶大量圖像運算的 app 運行在2013年的舊 Android 手機上面依然十分流暢,iOS 的流暢程度也堪比原生。

7.2

對於 web 的應用來講,Flutter 還在不斷地改進,其中還有不少的坑沒有解決,這裏包括了移動端的 WebView 以及編程成的 web 應用,還不適合大面積的投入到 web 的生產環境中。

7.3

關於和 Native 的混編,爲了不產生混合棧應用中的內存問題和渲染問題等,建議儘可能將嵌入原生的 Flutter 節點設計在葉子節點上,即業務棧跳轉到 Flutter 後儘可能完成結束後再回到Native棧中。

7.4

基於「去橋」的原生編譯方式,Flutter 在將來各個平臺上的運行應該會充滿期待,目前驗證的移動端應用打包成 Windows 應用後,運行表現仍是很不錯的,固然一些更大型的應用須要時間去摸索和完善。

7.5

語法方面,Flutter 中的 dart 正在變得愈來愈簡單,也在借鑑一些優秀的前端框架上的語法,例如 react 等,kotlin 中也有不少類似的地方,感受 Flutter 團隊正在努力地促進大前端時代的發展。


總之,Flutter 確實帶來了不少之前的跨端方案無法知足的驚喜的地方,相信不久的未來一碼多端會變得愈來愈重要,特別是在新業務的探索成本上表現得十分搶眼。

以上是一些對 Flutter 的一些粗淺的總結,歡迎有興趣的小夥伴一塊兒探討。

網易有道,與你同道,由於熱愛因此選擇, 期待志同道合的你加入咱們。
  • END -
相關文章
相關標籤/搜索