自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

攜程酒店Flutter性能優(yōu)化實踐

開發(fā)
攜程酒店業(yè)務使用Flutter技術開發(fā)的時間快接近兩年,這期間有列表頁、詳情頁、相冊頁等頁面使用了Flutter技術棧進行了跨平臺整合,大大提高了研發(fā)效率。

作者簡介 | Qifan,攜程高級工程師,專注移動端開發(fā);Yinuo,攜程高級工程師,專注移動端開發(fā);popeye,攜程軟件技術專家,關注移動端跨端技術,致力于快速,高性能地支撐業(yè)務開發(fā)。

一 、前言

攜程酒店業(yè)務使用Flutter技術開發(fā)的時間快接近兩年,這期間有列表頁、詳情頁、相冊頁等頁面使用了Flutter技術棧進行了跨平臺整合,大大提高了研發(fā)效率。在開發(fā)過程中,也遇到了一些性能相關問題和用戶反饋,比如長列表滾動卡頓、頁面打開時間較長、頁面打開后部分數(shù)據(jù)加載時間較長等問題。為解決這些問題,我們選用了多個性能指標監(jiān)控業(yè)務運行狀態(tài),借助性能檢測工具定位問題,并查閱源碼、文檔等資源解決問題,形成了這篇文章。

同時在不斷的需求迭代和代碼更新過程中,APP的性能穩(wěn)定性持續(xù)受到挑戰(zhàn),為此我們建立了線上性能監(jiān)控系統(tǒng),通過量化,治理,監(jiān)控三方面手段,持續(xù)改善APP性能和用戶體驗。目前頁面的各種性能指標諸如FPS、TTI、內(nèi)存等都達到了不錯的效果,本文將介紹我們在優(yōu)化過程中所遇到的問題和采取的主要優(yōu)化方案。

二、FPS&TTI提升性能優(yōu)化

2.1 常用性能指標和卡頓定義

對于客戶端應用來說,流暢度是影響用戶使用體驗的關鍵因素。流暢度低主要有:低FPS、高TTI、卡頓。這些現(xiàn)象出現(xiàn)時,頁面會出現(xiàn)不連續(xù)的動畫,頁面刷新會短暫停頓,打開新頁面速度較慢,新頁面出現(xiàn)白屏或者較長時間的加載動畫,用戶做點擊滑動等交互時頁面不響應。

用戶操作 FPS 的定義是每秒傳輸幀數(shù) (Frames Per Second),是圖像領域的概念。對于手機客戶端來說,主流顯示屏的刷新率為60Hz,高端手機顯示屏刷新率可以達到120Hz及以上。理想情況下,頁面繪制的FPS和屏幕刷新率一致。屏幕畫面刷新次數(shù)越多,屏幕可以展示的動態(tài)細節(jié)越多,所以數(shù)值越高越好。TTI的定義是從頁面加載開始到頁面處于完全可交互狀態(tài) (Time To Interactive),完全可交互狀態(tài)指的是頁面有內(nèi)容呈現(xiàn)并且用戶可以進行操作。

2.2 FPS優(yōu)化的工具介紹

Flutter官方提供了三種應用編譯選項,debug模式、release模式和profile模式。當我們需要做性能分析的時候,需要打包profile模式的應用,這個模式的性能接近release模式,并且有性能相關的信息分析。我們使用的工具是官方提供開發(fā)者工具中的Performance View,并選擇了Enhance tracing模式。

圖片

圖1 幀渲染時間柱狀圖

上圖是幀渲染時間,橫坐標是幀號,縱坐標是繪制時間,藍色代表該幀滿足60fps,橙色代表不滿足60fps。從這張圖可以快速定位到繪制時間較長的幀,而下圖是選中某幀之后,UI繪制和光柵化時間,如果選擇了Enhance tracing模式,可以看到耗時較長的方法、widget build。

前文已經(jīng)介紹過FPS的定義,對于flutter繪制而言,每幀繪制耗時前三的是UI繪制時間、光柵化時間、vsync ahead。UI繪制時間主要是widget build、layout、paint,簡單認為是CPU時間;光柵化時間可以簡單認為是GPU時間;vsync ahead是vsync信號與widget build之間的延時。

圖片

圖2 Widget build耗時與對應執(zhí)行的方法

2.3 具體實踐方案

a) 控制setState次數(shù),使用Provider機制減小刷新范圍

我們的業(yè)務開發(fā)是MVVM結構的,數(shù)據(jù)驅(qū)動UI更新。UI的繪制占了性能開銷的很大部分,減少不必要的UI繪制、控制UI繪制的范圍這兩種方法能顯著改善性能。

減少不必要的UI繪制是通過控制build次數(shù)實現(xiàn)的。widget build是通過setState方法或者builder方法觸發(fā)的,在業(yè)務中,盡量減少非必要的setState,只有真正頁面數(shù)據(jù)發(fā)生變化,頁面狀態(tài)變化時才調(diào)用setState方法。對于builder方法,可以實現(xiàn)shouldRebuild等接口,增加觸發(fā)builder方法的限制。

控制UI繪制的范圍是通過改變widget樹層級實現(xiàn)的。MVVM中數(shù)據(jù)觸發(fā)UI更新的方式有很多,我們的業(yè)務主要用到了Provider機制,這是一種觀察者模式設計。如下圖所示,對于左邊的widget樹,如果只需要更新Container容器配置和Icon圖標配置,那么可以將selector拆分到這兩個widget的雙親widget,實現(xiàn)了Text widget不刷新。對于widget樹較大的業(yè)務,這樣的改動能顯著提升FPS。

圖片

圖3 Widget樹結構優(yōu)化以減少build次數(shù)

b) 預構建widget (AnimatedBuilder)

圖片

圖4 酒店詳情頁頭部使用預構建減少build次數(shù)

上圖是酒店詳情頁頭部沉浸式動畫的UI,頭部展開的過程中,圖片和圖片上的蒙層需要重新繪制,圖片上部SHA logo不需要重新繪制,圖片下部tab欄不需要重新繪制,對于這個需求的做法是用AnimatedBuilder。

AnimatedBuilder提供了幾個可選參數(shù),animation是對動畫的監(jiān)聽,builder是動畫過程中需要重新繪制的部分,child是動畫過程中不需要重新繪制的部分,child作為參數(shù)會傳入builder中。下面的偽代碼是一個例子,動畫過程中Text并不會多次繪制。

@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: Container(
width: 200.0,
height: 200.0,
color: Colors.green,
child: const Center(
child: Text('Text!'),
),
),
builder: (BuildContext context, Widget child) {
return Transform.rotate(
angle: _controller.value * 2.0 * math.pi,
child: child,
);
},
);
}

對于詳情頁頭部沉浸式動畫的例子,可以把widget樹進行拆分,只有圖片和圖片蒙層放入builder方法中,其余的widget作為child傳入builder,同時用Stack widget實現(xiàn)兩部分UI的組合,這樣改進之后,F(xiàn)PS在動畫過程中有較大提升。

c) const widget

對于dart語法,需要分清楚final和const關鍵字的區(qū)別。關鍵字final的意思是一次賦值,不能改變;而關鍵字const的意思是常量,確定的值。這兩者的區(qū)別是final變量在第一次使用時被初始化,而const 變量是一個編譯時替換為常量值。同樣的,對于const widget,這個widget在編譯階段就已經(jīng)確定,不會有狀態(tài)的變化和成員變量更新。const widget特別適合于標簽、特殊Icon等可以復用的UI,性能開銷較小。

d) 減少耗時計算,放到Isolate

Flutter應用中的Dart代碼執(zhí)行在UI Runner中,而Dart是單線程的,我們平時使用的異步任務Future都是在這個單線程的Event Queue之中,通過Event Loop來按順序執(zhí)行。需要避免將一些耗時計算放在UI線程,可以把耗時計算放到Isolate去執(zhí)行。

e) 懶加載

能夠?qū)崿F(xiàn)懶加載的有ListView.builder、PageView.builder和GridView.builder,這些widget可以用戶長列表或重復容器結構的UI,通過判斷單個item是否在屏幕內(nèi)或者將要進入屏幕位置而進行繪制。與之對應的是Column、Row等一次性繪制widget,對于重復結構的數(shù)據(jù),盡量避免使用這些組件。

如下圖中,酒店周邊景點美食購物列表和附近同類型酒店列表都實現(xiàn)了按需加載。酒店周邊景點美食購物列表的卡片數(shù)量超過20個,最初使用Row 組件構建時,第一次構建時間超過25ms,達不到60FPS的16ms繪制時間要求。當然,按需加載也有性能開銷,出現(xiàn)在列表的滑動過程中。如果一次性全部構建了列表,滑動過程中不會觸發(fā)新的構建,滑動流暢度體驗更好,但是第一次構建時的卡頓感明顯。

圖片

圖5 酒店詳情頁周邊內(nèi)容運用懶加載減少構建次數(shù)

f) 分幀渲染

錯峰加載方案使用分幀渲染,分幀渲染的原理是將一棵Widget樹中的部分繪制時間較長的節(jié)點在第一幀時只占位不繪制,等到下一幀開始時,節(jié)點替換占位UI,單獨使用一幀時間繪制。

在酒店詳情頭部信息繪制中運用了分幀渲染技術,下左圖未使用分幀渲染,下右圖對圖片tab欄、酒店設施標簽、點評模塊、地址欄使用分幀渲染。從結果看,減少了3次卡頓和1次輕微卡頓,流暢幀占比超過90%。

圖片

圖6 分幀渲染在詳情頁頭部運用的效果

布局與繪制的基本單位是一棵widget樹,分幀渲染的原理是將布局與繪制時間較長的子widget先用Container占位,再等下一幀開始時單獨渲染。使用占位widget的偽代碼如下,build方法返回占位widget,并在widget構建幀結束時替換占位widget并觸發(fā)繪制。

@override
void initState() {
super.initState();
result = widget.placeHolder;
replaceWidget ();
}
@override
Widget build(BuildContext context) {
return result;
}
void replaceWidget() {
SchedulerBinding.instance.addPostFrameCallback((t) {
TaskQueue.instance.scheduleTask(() {
if (mounted)
setState(() {
result = widget.child;
});
}, Priority.animation, id: widget.index);
});
}

幀的繪制狀態(tài)可以從SchedulerBinding獲得,同時建立隊列保證一幀執(zhí)行一個子widget繪制。

// 等待當前幀結束時替換占位widget并觸發(fā)繪制
await SchedulerBinding.instance.endOfFrame;
// 執(zhí)行任務隊列中的繪制任務
final TaskEntry<dynamic> entry = _taskQueue.first;
entry.run();

2.4  UI GPU 問題定位與優(yōu)化

GPU 問題主要集中在底層渲染耗時上。有時候 Widget 樹雖然構造起來容易,但在 GPU 線程下的渲染卻很耗時。涉及 Widget 裁剪、蒙層這類多視圖疊加渲染,或是由于缺少緩存導致靜態(tài)圖像的反復繪制,都會明顯拖慢 GPU 的渲染速度??梢允褂眯阅軋D層提供的兩項參數(shù),負責檢查多視圖疊加的視圖渲染開關checkerboardOffscreenLayers和負責檢查緩存的圖像開關checkerboardRasterCacheImages來檢查這種模塊的存在。

a) checkerboardOffscreenLayers

多視圖疊加通常會用到 Canvas 里的savaLayer 方法,這個方法在實現(xiàn)一些特定的效果(比如半透明)時非常有用,但由于其底層實現(xiàn)會在 GPU 渲染上涉及多圖層的反復繪制,因此會帶來較大的性能問題。對于 saveLayer 方法使用情況的檢查,我們只要在 MaterialApp 的初始化方法中,將 checkerboardOffscreenLayers 開關設置為 true,分析工具就會自動幫我們檢測多視圖疊加的情況了,使用了 saveLayer 的 Widget 會自動顯示為棋盤格式,并隨著頁面刷新而閃爍。

不過,saveLayer 是一個較為底層的繪制方法,因此我們一般不會直接使用它,而是會通過一些功能性 Widget,在涉及需要剪切或半透明蒙層的場景中間接地使用。所以一旦遇到這種情況,我們需要思考一下是否一定要這么做,能不能通過其他方式來實現(xiàn)。如下圖所示,因為詳情頭部bar用到高斯模糊,同時使用ClipRRect裁切圓角,ClipRRect會調(diào)到savelayer接口,所以該部分產(chǎn)生閃爍。

圖片

圖7 詳情頁頭部圖片標題欄中裁切樣式應用

b) checkerboardRasterCacheImages

從資源的角度看,另一類非常消耗性能的操作是,渲染圖像。這是因為圖像的渲染涉及 I/O、GPU 存儲,以及不同通道的數(shù)據(jù)格式轉換,因此渲染過程的構建需要消耗大量資源。

為了緩解GPU 的壓力,F(xiàn)lutter 提供了多層次的緩存快照,這樣Widget 重建時就無需重新繪制靜態(tài)圖像了。與檢查多視圖疊加渲染的checkerboardOffscreenLayers 參數(shù)類似,F(xiàn)lutter 也提供了檢查緩存圖像的開關 checkerboardRasterCacheImages,來檢測在界面重繪時頻繁閃爍的圖像(即沒有靜態(tài)緩存)。

我們可以把需要靜態(tài)緩存的圖像加到 RepaintBoundary 中,RepaintBoundary 可以確定 Widget 樹的重繪邊界,如果圖像足夠復雜,F(xiàn)lutter 引擎會自動將其緩存,避免重復刷新。當然,因為緩存資源有限,如果引擎認為圖像不夠復雜,也可能會忽RepaintBoundary。

2.5 頁面預加載提升TTI

網(wǎng)頁應用的主要流程有三步,通過鏈接打開頁面,發(fā)送服務請求獲得頁面數(shù)據(jù),將頁面數(shù)據(jù)展示在頁面上。對客戶端應用來說,頁面之間跳轉是相對確定的,數(shù)據(jù)在頁面之間存在共享的可能,預加載的工作是在打開頁面之間預先獲得頁面的數(shù)據(jù),從而減少打開頁面到頁面展示的時間。

預加載數(shù)據(jù)有三種常見方法,第二個頁面的數(shù)據(jù)在第一個頁面的服務結果中獲得;第二個頁面的數(shù)據(jù)在客戶端其它頁面中預先獲得并緩存;第二個頁面的服務請求在打開頁面之前發(fā)送。

a) 預加載頁面數(shù)據(jù)

頁面數(shù)據(jù)預獲取的方案,實現(xiàn)方法是在上一個頁面提前獲取服務數(shù)據(jù),在用戶跳轉到當前頁面時,直接從緩存獲取,節(jié)省了數(shù)據(jù)的網(wǎng)絡傳輸時間,達到快速展示當前頁面內(nèi)容的效果。目前在酒店核心預訂流程,都運用了數(shù)據(jù)預加載技術,如下圖所示。

圖片

圖8 酒店業(yè)務預加載頁面數(shù)據(jù)的應用

結合酒店業(yè)務特點,數(shù)據(jù)預加載需要考慮幾個方面問題,第一,酒店預訂流程頁面PV量都很高,酒店列表和詳情頁PV都是千萬級別,所以需要考慮數(shù)據(jù)預加載的時機,避免服務的資源浪費。第二,酒店列表,詳情,填單頁都有價格信息,價格信息對用戶來說是動態(tài)信息,實時都有變價可能,所以需要考慮數(shù)據(jù)預加載的緩存策略,避免因為價格的前后不一致造成用戶誤解。

在實現(xiàn)全流程預加載方案之后,我們酒店預訂流程頁面的慢加載率從初始值的42.90%降低至現(xiàn)階段的8.05%。

b) 預加載ViewModel

與數(shù)據(jù)預獲取的方案相比,預加載ViewModel更進一步,將預獲取的數(shù)據(jù)處理成ViewModel形式,在打開頁面時直接用ViewModel進行展示。這種方案減少了業(yè)務對數(shù)據(jù)處理的時間。

圖片

圖9 酒店詳情頁預加載ViewModel技術的應用

上圖是杭州綠城尊藍錢江豪華精選酒店在酒店列表頁和酒店詳情頁頭部的UI對比??梢钥闯?,酒店詳情頁頭部的信息主要是酒店名稱、星級、榜單、特色設施、點評、開業(yè)裝修時間等信息,這些信息和列表頁酒店卡片信息存在重合。如果用戶瀏覽的軌跡為從酒店列表頁到酒店詳情頁,那么可以直接將列表頁的數(shù)據(jù)帶入酒店詳情頁作為頭部展示。

圖片

圖10 酒店詳情頁預加載ViewModel的數(shù)據(jù)流

上圖為詳情頁頭部預加載的主要流程。我們的flutter業(yè)務代碼采用MVVM的結構,將服務請求的結果處理完的數(shù)據(jù)放入ViewModel中,ViewModel的數(shù)據(jù)更新通過Provider機制觸發(fā)頁面UI更新。

圖中可以開到,詳情頁頭部ViewModel的數(shù)據(jù)有兩個來源,分別是列表頁服務請求的結果和詳情頁服務請求的結果。這兩個服務請求結果到ViewModel的業(yè)務流程不一樣,列表頁的服務結果數(shù)據(jù)通過URL參數(shù)的方式傳入詳情頁,而詳情頁服務結果可以直接生成詳情頁頭部的ViewModel。

圖中還有一個重要模塊是列表頁服務結果和詳情頁服務結果之間的通用緩存DataCache,它的功能是實現(xiàn)頁面之間數(shù)據(jù)的一致性。頁面上的數(shù)據(jù)可以由服務更新,也可以由用戶交互更新。業(yè)務的ViewModel依賴這個通用緩存,數(shù)據(jù)更新會觸發(fā)頁面UI更新。

三、Flutter服務通道優(yōu)化

3.1 背景

因為我們APP采用的私有服務協(xié)議,目前發(fā)服務的動作還是在Native代碼上,而酒店的核心頁面已經(jīng)轉到了Flutter上。通過Flutter框架提供的通道技術Native到Flutter的數(shù)據(jù)傳輸通道需要對數(shù)據(jù)做一次額外的序列化及反序列化的傳輸,同時傳輸?shù)倪^程比較耗時,會阻塞UI的渲染主線程,對頁面的加載會造成明顯的影響。我們檢測到這個環(huán)節(jié)之后和框架一起對Flutter的底層框架進行了改造,可以實現(xiàn)數(shù)據(jù)流直接的透傳,同時不阻塞UI主線程,性能得到了極大的提升。

優(yōu)化前,通過服務返回的數(shù)據(jù)流傳遞到flutter使用,整個過程要經(jīng)歷以下4步:

  • PB反序列化
  • Response到JsonString的編碼
  • JsonString到MethodChannel(使用JsonMethodCodec編解碼)
  • 傳輸JsonString到Reponse的解碼

整個過程鏈路長,數(shù)據(jù)傳輸量大,效率低,影響到頁面加載性能,如下圖所示

圖片

圖11 優(yōu)化前的業(yè)務服務請求數(shù)據(jù)流

改造后,通過服務返回的數(shù)據(jù)流,直接傳輸?shù)紽lutter側,在Flutter直接進行PB的反序列化,傳輸性能得到極大提升。

  • ?PB的數(shù)據(jù)流到MethodChannel(使用StandardMethodCodec編解碼)傳輸
  • PB反序列化到Response

整個過程鏈路短,數(shù)據(jù)傳輸量小,效率高,如下圖所示:

圖片

圖12 優(yōu)化后的業(yè)務服務請求數(shù)據(jù)流

其中MethodChannel的編解碼器由JsonMethodCodec換成了StandardMethodCodec。因為StandardMethodCodec可以避免轉換JsonString的操作,能節(jié)省傳輸時間。

3.2 Flutter中使用Protobuf

在flutter中使用Protobuf,首先需要將proto契約文件轉化成dart文件,可以借助官方編譯工具protoc進行編譯。

a) 獲取protoc工具

安裝C+

+sudo apt-get install autoconf automake libtool curl make g++ unzip

安裝Protobuf發(fā)行版

https://github.com/protocolbuffers/protobuf/releases

下載完成之后,解壓,進到目錄中執(zhí)行下面命令編譯安裝

./configure
make
make check
sudo make install
sudo ldconfig # refresh shared library cache.
安裝protoc-gen-dart插件
dart pub global activate protoc_plugin
在Terminal中執(zhí)行protoc命令生成dart文件
protoc --dart_out=. <文件名>.proto

圖片

圖13 生成的契約文件結構

b)  使用生成的dart契約文件

執(zhí)行flutter pub add protobuf命令,修改項目的pubspec.yaml,在dependencies中加上: protobuf: ^2.0.1

編寫如下測試代碼:

圖片

圖14 使用契約的樣例代碼

執(zhí)行后可以得到如下結果:

圖片

圖15 執(zhí)行結果

其中,生成Person的類繼承了Protobuf包里的GeneratedMessage類,序列化和反序列化由基類實現(xiàn)。但是這種方式不能根據(jù)需要定制化生成契約文件。因此,為了更好的兼容Json格式的數(shù)據(jù),可以使用FreeMarker模板引擎定制化生成契約文件。

圖片

圖16 使用FreeMarker生成契約的文件結構

3.3 使用FreeMarker定制化生成dart契約文件

FreeMarker是一款模板引擎:即一種基于模板和要改變的數(shù)據(jù),并用來生成輸出文本(HTML網(wǎng)頁、電子郵件、配置文件、源代碼等)的通用工具。它不是面向最終用戶的,而是一個Java類庫,是一款程序員可以嵌入他們所開發(fā)產(chǎn)品的組件。

下面介紹如何使用FreeMarker和protoc命令生成任意編程語言的契約文件

1)下載FreeMarker最新版jar包

https://freemarker.apache.org/freemarkerdownload.html

2)下載Protobuf對應版本的jar包

https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java

3)在Java項目中導入對應jar包

圖片

圖17 項目中導入工具方法

4)編寫Java程序

圖片

圖18 程序流程圖

程序的流程如上圖所示。首先使用protoc命令生成對應的描述文件,其次將描述文件轉換成對應java對象,最后使用FreeMarker模板引擎生成任意語言的契約文件。

圖片

圖19 程序的實現(xiàn)

由上圖可知,模板引擎的輸入是一個classModel對象。如下圖實現(xiàn)了將描述文件轉化成classModel對象的功能。

圖片

圖片

圖20 程序的實現(xiàn)(續(xù))

FTL模板文件如下圖所示:

圖片

圖21 模版文件

5)執(zhí)行代碼輸出契約文件

圖片

圖22 輸出的契約文件

這樣就可以實現(xiàn)了根據(jù)proto文件自定義生成任意編程語言的契約文件。

3.4 Json與Protobuf的性能對比

我們對比了相同報文情況下Json和Protobuf在序列化和反序列化上所花費的時間。從下圖可知,Protobuf在序列化和反序列化相同大小報文時比Json花費的時間大大減少了,也大大提高了我們獲取數(shù)據(jù)的速度。

圖片

圖23 序列化、反序列化時間

四、內(nèi)存泄漏治理

4.1 內(nèi)存泄漏的常用監(jiān)控手段

內(nèi)存泄漏是一個比較嚴重的問題,如果出現(xiàn),對App的穩(wěn)定性和用戶體驗都有非常大影響。因此對這塊的監(jiān)控和治理也是我們非常關注的一塊。

在監(jiān)控方面Flutter現(xiàn)在比較通用的方法就是利用Expando中的弱引用去監(jiān)控我們要檢查是否有泄漏的對象,如果出現(xiàn)則從VM中獲取其引用鏈接,從而分析其泄漏原因。我們的框架也利用此方法監(jiān)控了我們app中的每個頁面是否在退出時還存在泄漏。

另外通過Flutter的Dev tool中的內(nèi)存監(jiān)控工具也能實現(xiàn)對泄漏對象的發(fā)現(xiàn)。比如對于酒店詳情頁面,反復進入和退出此頁面,如果有泄漏會發(fā)現(xiàn),在內(nèi)存監(jiān)控工具中出現(xiàn)此頁面多個的對象存活,此時基本可以判斷出此頁面出現(xiàn)了泄漏了。下圖的第一列是類名,第二、三列是實例數(shù)量,第四、五列是對應分配的字節(jié)數(shù)。

圖片

圖24 酒店詳情的內(nèi)存泄漏監(jiān)控

4.2 內(nèi)存泄漏的治理

下面介紹一下,我們在我們頁面的內(nèi)存泄漏治理中發(fā)現(xiàn)的一些導致泄漏的原因和解決的辦法。

a) 調(diào)用Native的Plugin時,對Future的Then設置的閉包沒有關閉

在調(diào)用Native的Plugin接口時,有時會設置一個Then的閉包,期望在這個閉包里去處理這個Plugin的返回結果。這個閉包會注冊到引擎的全局變量里面,如果Native調(diào)用了result的listener,這個Then的閉包會走到,然后會被清除掉。如果某些case,Native沒有調(diào)用,則這個閉包會泄露,如果這個閉包所屬的Model能引用到頁面對象的話,則會造成整個頁面的泄露。

比如下面這個例子,我們進入flutter頁面時會調(diào)這個plugin,但是native對應的result則必須在某些case情況下才會回調(diào)。而大部分情況下,是不會回調(diào)的,從而造成整個頁面的泄露。解決方法是把future轉換成stream,然后我們在頁面退出時cancel掉,就能避免閉包的泄漏。

例子:調(diào)用Native的Plugin時出現(xiàn)泄漏的情況

Flutter側的調(diào)用:

void callNative() {
FlutterBridge.callNative("method", map).then((value) {
do some thing;});
}

Native的響應:

override fun flutterPluginAction ( result: MethodChannel.Result){
if (condition) {
result.success(ret)
} else {
do something;
}
}

可以看到Native在接受到這個plugin調(diào)用時,對于result的調(diào)用返回不是一直都會做的,它需要等到滿足條件才會做這件事情,而如果它不做這件事情,對應的flutter那邊的閉包就會一直被保存在引擎中,這個引用鏈也會一直存在,從而造成這個引用鏈上的對象都泄漏了。

解決的方法:

void callNative () {
Future future = FlutterBridge. callNative ("method");
_streamSubscription?.cancel();
_streamSubscription = future?.asStream()?.listen((value)
{
do something;
});
}

我們的解決方式,就是對這種異步但不能確定回調(diào)是否一定完成的情況,換成用StreamSubscription去監(jiān)聽。然后當頁面退出時做一下cancel的動作,這樣就能避免泄漏的發(fā)生。

void onPageDestroy() {
_ streamSubscription?.cancel();
}

這種等待對異步調(diào)用的回調(diào)監(jiān)聽其實都可能存在類似問題,只不過如果是單純在Dart中的異步調(diào)用一般不會存在這種不回調(diào)的情況。但是對于plugin這種跟native的交互的地方,我們在初期接觸flutter時沒有關注到這塊,有可能會造成遺漏。

b) 一些觀察者模式中的訂閱者在頁面退出時沒有取消訂閱

這種是大家比較熟悉的一種情況。常見的例子有例如像Timer,EventBusCenter.defaultBus和LifeCycleObserver等。這些訂閱者如果在頁面退出時不需要了,需要記得取消掉。否則也會造成內(nèi)存泄漏,這種情況我們也應該避免。

五、小結

性能優(yōu)化是一件不斷持續(xù),不斷深入的事情。我們通過本文中所介紹的改進措施對頁面性能實現(xiàn)了很大的優(yōu)化,達到了不錯的效果。后續(xù)也會在此基礎之上對還可提高的地方繼續(xù)加深,同時也會對已經(jīng)驗證實行有效的方案去做一些抽象,封裝工作,后續(xù)提供通用的解決方案。

責任編輯:未麗燕 來源: 攜程技術
相關推薦

2022-07-15 09:20:17

性能優(yōu)化方案

2024-09-10 16:09:58

2023-11-24 09:44:07

數(shù)據(jù)攜程

2023-07-07 12:26:39

攜程開發(fā)

2024-04-18 09:41:53

2024-03-22 15:09:32

2022-04-14 17:53:50

攜程AWS上云

2022-10-21 10:40:08

攜程酒店MySQL慢查詢

2021-09-17 12:54:05

AI 數(shù)據(jù)人工智能

2022-06-03 08:58:24

APP攜程流暢度

2023-04-24 15:10:23

優(yōu)化方案

2024-09-25 15:37:46

2022-04-07 17:30:31

Flutter攜程火車票渲染

2022-03-30 18:39:51

TiDBHTAPCDP

2023-07-07 14:18:57

攜程實踐

2016-09-01 09:39:20

攜程無線

2023-03-14 14:01:00

內(nèi)存優(yōu)化

2015-05-28 14:05:02

2022-07-15 12:58:02

鴻蒙攜程華為

2022-05-13 09:27:55

Widget機票業(yè)務App
點贊
收藏

51CTO技術棧公眾號