张龑(网易有道技术团队)
Flutter的性能分析、工程架构、以及一些细节处理
跨端技术众多,为何选择(Flutter),它能带来哪些优势,有哪些缺点。
无论如何,原生的运行效率毋庸置疑是最高的,但是从工程工作量的角度来对比的话,特别是快速试错和业务扩展阶段,flutter是目前为止比较推荐的利器。
任何跨端的技术都是基于一码多端的思维,解决工程效率的问题,之前很多的跨端技术,例如React Native等都是基于web的跨端性解决方案,但是大家都知道,web在移动端上的运行效率和PC上有巨大差距的,这就导致RN不能很有效地在移动端完成各种复杂的交互式运算(例如复杂的动画运算,交互的执行性能等),即便是引入了Airbnb的Lottie引擎依然会在低端的手机上面显得很卡顿(当然也可以使用一些自研的引擎技术来针对各端来解决,不过这样就失去了跨端的意义)。
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案例)
以上是同样功能模块下,Flutter和RN的一些数据上的对比,是从众多的数据中抽取出来比较有代表性的一组
Flare-Flutter是一款十分优秀的flutter动画引擎,编译出的动画已经在windows、移动端、web上亲测验证过。
flutter生成的互动可以嵌入到任何端中使用精简的指令集进行互动,为互动场景(教学场景等带来巨大的希望),以下是直播同步互动的demo场景
flutter中目前是没有现成的mvvm框架的,但是我们可以利用Element树特性来实现mvvm
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();}123456789101112131415161718192021222324
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;}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
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))); } }}///历史战绩的itemclass 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']); }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
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; }}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
如果是统一系列的产品业务形态,还可以抽离出一套核心的架构,复用在同样的生产产品线上,例如当前产品线以教育为主,利用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; }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
这样每次如果有分辨率变动或者适配方案变动的时候,直接修改resizeUtil即可,但是这样带来的问题就是,在编写过程中单位变得很冗长,而且不熟悉团队工程的人会容易忘写,导致查错时间变长,代码侵入性较高,于是利用dart语言的扩展函数特性,为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());}12345678910
然后在布局中直接书写单位即可:
刚开始在移动端上使用泛型来做数据的自动解析时,使用了T.toString来判断类型,但是当编译成web的release版本时,在移动端正常运行的程序在web上无法正常工作:
刚开始的时候把目标一直定位在编译的方式上,因为存在dev profile release三种编译模式,只有在release上无法运行,误以为是release下编译有bug,随着和flutter团队的深入讨论后,发现其实是泛型在release模式下的坑,即在web版本的release模式下,一切都会进行压缩(包含类型的定义),所以在release下,T.toString()返回的是null,因此无法识别出泛型特征,具体的讨论链接:https://github.com/flutter/flutter/issues/47967
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; } }}123456789101112
对于移动端来说,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') ); }}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
不过这种方式会带来新的底层刷新渲染问题(当鼠标移动到某个元素时,会不停地闪动刷新),目前在新的版本上已修复,有兴趣的同学可以看看:https://github.com/flutter/flutter/issues/53253
内置html是很多工程的需求,很多网上的资料都是通过把本地的html做成数据流的方式然后加载进来,这种做法的兼容性很不好,而且编写过程中容易出现很多文件流过大无法读取的问题,其实这些做法都不是很舒适,我们应该通过IFrameElement来进行加载并通信,做法和前端很类似:
官方的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/flutter/issues/53490
通过GlobalKey获取RenderBox来获取渲染出的控件的size和position等参数:
在dart的浮点运算中,由于都是高精度的double运算,当运算长度过长的时候,dart会自动随机最后的一位小数,这样会导致每一次有些浮点运算每一次都是不确定的,这时需要手动进行精度转换,例如在计算两条线段是否共线时:
在矩阵的换算过程中,如果使用普通的matrix.translate,会导致rotate之后,再进行translate会在旋转的基数上面做系数叠加平移运算,这样计算后得到的不是自己想要的结果,因此如果运算当中有rotate操作时,应当使用leftTranslate来保证每次运算的独立性:
优先全部执行完Microtask Queue中的Event,直到Microtask Queue为空,才会执行Event Queue中的Event
经历了对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
附件:
链接:https://pan.baidu.com/s/1_JjnD1q5JXgctX04e1h8tA
提取码:7r4i