张龑(网易有道技术团队)
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 extends StatefulWidget {
final T viewModel;
final Widget child;
ViewModelProvider({
@required this.viewModel,
@required this.child,
});
static T of(BuildContext context) {
final type = _typeOf<_ViewModelProviderInherited>();
_ViewModelProviderInherited provider =
// 查询Element树中缓存的InheritedElement
context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
return provider?.viewModel;
}
static Type _typeOf() => T;
@override
_ViewModelProviderState createState() => _ViewModelProviderState();
}
class _ViewModelProviderState
extends State> {
@override
Widget build(BuildContext context) {
return _ViewModelProviderInherited(
child: widget.child,
viewModel: widget.viewModel,
);
}
@override
void dispose() {
widget.viewModel.dispose();
super.dispose();
}
}
// InheritedWidget可以被Element树缓存
class _ViewModelProviderInherited
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 {
ChallengeHistoryRankingListResponse.fromJson(Map 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 ranks; //先给10天
//二维码
String get qrcode =>
TextUtils.isEmpty(props) ? '' : json.decode(props)['qrcode'] ?? '';
ChallengeHistoryRankingData.fromJson(Map 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 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 _challengeObservable =
BehaviorSubject();
Stream 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 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 syncLoadHistoryListData(
int pageNum,
int pageSize,
) async {
Map 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(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
留言与评论(共有 0 条评论) “” |