張龑(網易有道技術團隊)html
Flutter的性能分析、工程架構、以及一些細節處理前端
跨端技術衆多,爲什麼選擇(Flutter),它能帶來哪些優點,有哪些缺點。react
web端的連接android
flutter工程效果ios
不管如何,原生的運行效率毋庸置疑是最高的,可是從工程工做量的角度來對比的話,特別是快速試錯和業務擴展階段,flutter是目前爲止比較推薦的利器。git
任何跨端的技術都是基於一碼多端的思惟,解決工程效率的問題,以前不少的跨端技術,例如React Native等都是基於web的跨端性解決方案,可是你們都知道,web在移動端上的運行效率和PC上有巨大差距的,這就致使RN不能頗有效地在移動端完成各類複雜的交互式運算(例如複雜的動畫運算,交互的執行性能等),即使是引入了Airbnb的Lottie引擎依然會在低端的手機上面顯得很卡頓(固然也可使用一些自研的引擎技術來針對各端來解決,不過這樣就失去了跨端的意義)。 github
lutter的編譯方式和產物是決定其高效運行效率的前提,不一樣於web的跨端編譯同樣(web的跨端編譯大可能是選擇了使用 "橋" 的概念來調用編譯產物,一般是使用了原生端的入口 + web端的橋來實現),Flutter幾乎是把dart的源碼經過不一樣平臺的編譯原理生成各平臺的產物,這種「去橋」的產物正式咱們所但願獲得的、貼近原生運行性能的編譯產物(固然,在dart最初設計的時候,是參考了不少前端的結構來完成的,特別從語法上面可以很明顯地感覺到前端的痕跡,並且最初的dart2js的原理也是一樣「橋」的概念)。web
例如 9月23號 google發佈的新flutter版本中,在支持的windows編譯產物上,就是經過相似visual studio的編譯工具(若是要將你的flutter工程編譯成windows產物,須要提早安裝一些VS相關的編譯插件),生成了windows下的工程解決方案.sln,最終生成dll的調用方式,運行起來很流暢,能夠下載附件中的Release.zip來嘗試運行:編程
(PS:這裏全部編譯工程都是經過同一套代碼完成,包括上文中的web地址、移動端案例還有這裏的windows案例)json
以上是一樣功能模塊下,Flutter和RN的一些數據上的對比,是從衆多的數據中抽取出來比較有表明性的一組
![]](img-blog.csdnimg.cn/20201022165…)
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();
}
複製代碼
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;
}
複製代碼
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']);
}
}
複製代碼
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;
}
}
複製代碼
若是是統一系列的產品業務形態,還能夠抽離出一套核心的架構,複用在一樣的生產產品線上,例如當前產品線以教育爲主,利用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作一些改進。
經過擴展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());
}
複製代碼
而後在佈局中直接書寫單位便可:
剛開始在移動端上使用泛型來作數據的自動解析時,使用了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;
}
}
}
複製代碼
對於移動端來講,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…
內置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的手勢問題還在不斷的討論中:github.com/flutter/flu…
經過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
附件: 連接:pan.baidu.com/s/1_JjnD1q5… 提取碼:7r4i