Flutter技术杂谈

1,926 阅读13分钟

在这里插入图片描述

张龑(网易有道技术团队)

Flutter的性能分析、工程架构、以及一些细节处理

1.为何Flutter

跨端技术众多,为何选择(Flutter),它能带来哪些优势,有哪些缺点。

先看看具体的工程效果

web端的链接

flutter工程效果

Flutter VS 原生

无论如何,原生的运行效率毋庸置疑是最高的,但是从工程工作量的角度来对比的话,特别是快速试错和业务扩展阶段,flutter是目前为止比较推荐的利器。

在这里插入图片描述

Flutter VS Web

任何跨端的技术都是基于一码多端的思维,解决工程效率的问题,之前很多的跨端技术,例如React Native等都是基于web的跨端性解决方案,但是大家都知道,web在移动端上的运行效率和PC上有巨大差距的,这就导致RN不能很有效地在移动端完成各种复杂的交互式运算(例如复杂的动画运算,交互的执行性能等),即便是引入了Airbnb的Lottie引擎依然会在低端的手机上面显得很卡顿(当然也可以使用一些自研的引擎技术来针对各端来解决,不过这样就失去了跨端的意义)。 在这里插入图片描述

Flutter性能

lutter的编译方式和产物是决定其高效运行效率的前提,不同于web的跨端编译一样(web的跨端编译大多是选择了使用 "桥" 的概念来调用编译产物,通常是使用了原生端的入口 + web端的桥来实现),Flutter几乎是把dart的源码通过不同平台的编译原理生成各平台的产物,这种“去桥”的产物正式我们所希望得到的、贴近原生运行性能的编译产物(当然,在dart最初设计的时候,是参考了很多前端的结构来完成的,特别从语法上面能够很明显地感受到前端的痕迹,而且最初的dart2js的原理也是同样“”的概念)。

例如 9月23号 google发布的新flutter版本中,在支持的windows编译产物上,就是通过类似visual studio的编译工具(如果要将你的flutter工程编译成windows产物,需要提前安装一些VS相关的编译插件),生成了windows下的工程解决方案.sln,最终生成dll的调用方式,运行起来很流畅,可以下载附件中的Release.zip来尝试运行:

在这里插入图片描述

在这里插入图片描述

(PS:这里所有编译工程都是通过同一套代码完成,包括上文中的web地址、移动端案例还有这里的windows案例)

与RN的性能对比:

在这里插入图片描述

以上是同样功能模块下,Flutter和RN的一些数据上的对比,是从众多的数据中抽取出来比较有代表性的一组

跨端平台的多样性

![]](img-blog.csdnimg.cn/20201022165…)

引擎

Flare-Flutter是一款十分优秀的flutter动画引擎,编译出的动画已经在windows、移动端、web上亲测验证过。

语法糖

综合测评

互动应用

flutter生成的互动可以嵌入到任何端中使用精简的指令集进行互动,为互动场景(教学场景等带来巨大的希望),以下是直播同步互动的demo场景

2.Flutter业务架构

flutter中目前是没有现成的mvvm框架的,但是我们可以利用Element树特性来实现mvvm

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;
}

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']);
  }
}

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;
  }
}

一些基础架构

view和viewmodel如何实现初始化和相互作用:

Flutter业务架构抽离

如果是统一系列的产品业务形态,还可以抽离出一套核心的架构,复用在同样的生产产品线上,例如当前产品线以教育为主,利用flutter的一码多端性质,则可以把题版生产工厂、渲染题版引擎、 适配框架、 以及跨端接口的框架都抽离出来,迅速地形成可以推广复用的模板,可以事半功倍地解决掉业务上的试错成本问题,当然,其他产品性质的业务线均可如此。

3.Flutter适配

任何框架中的UI适配都是特别繁重的工作,跨端上的适配更是如此,因此在同一套布局里面,各个平台的换算过程显得尤为重要,起初的时候,flutter中并没有提供某种诸如 dp 或者 sp 的适配方式,而且考虑到直接更改底层matrix换算比例的话可能会让原本高清分辨率的手机显示不是那么清楚,而flutter的宽高单位都是num,最后编译的时候才会去对应到各个平台的单位尺寸。为了减轻设计师的设计负担,这里通常使用一套ios的设计稿即可,以375 x 667的通用设计稿为例,转换过来到android上是360 x 640 (对应1080 x 1920),这里flutter的单位也是和对应手机的像素密度有关的。

构造一个转换工具类:

//目前适配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;
  }
}

具体使用:

这样每次如果有分辨率变动或者适配方案变动的时候,直接修改resizeUtil即可,但是这样带来的问题就是,在编写过程中单位变得很冗长,而且不熟悉团队工程的人会容易忘写,导致查错时间变长,代码侵入性较高,于是利用dart语言的扩展函数特性,为resizeUtil做一些改进。

低侵入式的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中的一些坑

泛型上的坑

刚开始在移动端上使用泛型来做数据的自动解析时,使用了T.toString来判断类型,但是当编译成web的release版本时,在移动端正常运行的程序在web上无法正常工作:

刚开始的时候把目标一直定位在编译的方式上,因为存在dev profile release三种编译模式,只有在release上无法运行,误以为是release下编译有bug,随着和flutter团队的深入讨论后,发现其实是泛型在release模式下的坑,即在web版本的release模式下,一切都会进行压缩(包含类型的定义),所以在release下,T.toString()返回的是null,因此无法识别出泛型特征,具体的讨论链接:github.com/flutter/flu…

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;
    }
  }
}

在编译成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')
    );
  }
}

不过这种方式会带来新的底层刷新渲染问题(当鼠标移动到某个元素时,会不停地闪动刷新),目前在新的版本上已修复,有兴趣的同学可以看看:github.com/flutter/flu…

Flutter如何加载本地的html并且进行通信

内置html是很多工程的需求,很多网上的资料都是通过把本地的html做成数据流的方式然后加载进来,这种做法的兼容性很不好,而且编写过程中容易出现很多文件流过大无法读取的问题,其实这些做法都不是很舒适,我们应该通过IFrameElement来进行加载并通信,做法和前端很类似:

在ios13.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的手势问题还在不断的讨论中:github.com/flutter/flu…

5.关于布局和运算

容器widget和渲染widget

GlobalKey

通过GlobalKey获取RenderBox来获取渲染出的控件的size和position等参数:

浮点运算

在dart的浮点运算中,由于都是高精度的double运算,当运算长度过长的时候,dart会自动随机最后的一位小数,这样会导致每一次有些浮点运算每一次都是不确定的,这时需要手动进行精度转换,例如在计算两条线段是否共线时:

Matrix的平移和旋转

在矩阵的换算过程中,如果使用普通的matrix.translate,会导致rotate之后,再进行translate会在旋转的基数上面做系数叠加平移运算,这样计算后得到的不是自己想要的结果,因此如果运算当中有rotate操作时,应当使用leftTranslate来保证每次运算的独立性:

6.项目优化

避免build() 方法耗时:

重绘区域优化:

尽量避免使用Opacity

Flutter的单线程模型

优先全部执行完Microtask Queue中的Event,直到Microtask Queue为空,才会执行Event Queue中的Event

耗时方法放在isolate

7.杂谈总结

经历了对flutter长期的探索和项目验证,目前对flutter有自己的一些杂谈总结:

(1).flutter在移动端的表现还是很不错的,在运行流畅度方面也是非常棒,经过优化过后的带大量图像运算的App运行在2013年的旧android手机上面依然十分流畅,ios的流畅程度也堪比原生;

(2).对于web的应用来说,flutter还在不断地改进,其中还有很多的坑没有解决,这里包括了移动端的webview以及编程成的web应用,还不适合大面积的投入到web的生产环境中;

(3).关于和Native的混编,为了避免产生混合栈应用中的内存问题和渲染问题等,建议尽量将嵌入原生的flutter节点设计在叶子节点上,即业务栈跳转到flutter后尽量完成结束后再回到Native栈中;

(4).基于“去桥”的原生编译方式,flutter在未来各个平台上的运行应该会充满期待,目前验证的移动端应用打包成windows应用后,运行表现还是很不错的,当然一些更大型的应用需要时间去摸索和完善;

(5).语法方面,flutter中的dart正在变得越来越简单,也在借鉴一些优秀的前端框架上的语法,例如react等,kotlin中也有很多相似的地方,感觉flutter团队正在努力地促进大前端时代的发展。

总之,flutter确实带来了很多以前的跨端方案没法满足的惊喜的地方,相信不久的将来一码多端会变得越来越重要,特别是在新业务的探索成本上表现得十分抢眼。

以上是一些对flutter的一些粗浅的总结,欢迎有兴趣的小伙伴一起探讨。

网易技术热爱者队伍持续招募队友中!网易有道,与你同道,因为热爱所以选择, 期待志同道合的你加入我们,简历可发送至邮箱:bjfanyudan@corp.netease.com

附件: 链接:pan.baidu.com/s/1_JjnD1q5… 提取码:7r4i