天天看點

從渲染原理出發探究Flutter記憶體洩漏(超詳細)背景flutter記憶體都包含哪些Dart綁定層如何工作通過檢測渲染樹節點來檢測記憶體洩漏總結與展望

作者:閑魚技術-蕭湘

背景

衆所周知,記憶體的高低是評判一款app的性能優劣的重要的名額之一。作為開發者而言,都會盡可能的減少記憶體的使用,清除無用的記憶體塊,進而減少整個app的記憶體使用量。這也是曆來開發者是追求的目标。然而,開發者難免時常因為語言用法或者寫法的緣故,導緻該釋放而未釋放的對象遲遲未釋放,進而記憶體洩漏,消耗殆盡記憶體空間,進而導緻系統崩潰的情況。

如何更簡單的幫助開發者分析、暴露且解決記憶體洩漏問題,幾乎是每一個平台或架構、開發者亟需的一個的"标配"的feature。例如:蘋果的instruments,Linux的Kmemleak等。但是對于flutter社群,缺少一款用得順手的記憶體洩漏工具。

對于使用flutter而言,因使用dart語言,通過形成渲染樹送出到c++的skia進行渲染,從dart層到c++層擁有很長的渲染鍊路,使用者必須對整個渲染鍊路有通盤深刻的了解,才能深刻此時此刻的記憶體使用情況。

本文從flutter渲染原理出發,分析flutter的記憶體配置設定情況,理清渲染流程,提出一種基于渲染樹個數的方式尋找記憶體洩漏的解決方案

flutter記憶體都包含哪些

虛拟記憶體還是實體記憶體?

當我們談論記憶體時,通常說的是實體記憶體(Physical memory),同一個應用程式運作在不同機器或者作業系統上時,會因不同作業系統和機器的硬體條件的不同,配置設定的到實體記憶體大小會有所不同,但大緻而言,一款應用程式所使用到的虛拟記憶體(Virtual Memory)而言便會大緻一樣,本文讨論的都值的是虛拟記憶體。

我們可以直覺的了解,代碼中操作的所有對象都是能用虛拟記憶體衡量,而不太關心對象是否存在于實體記憶體與否,隻要能減少對象的應用,盡量少的持有對象,不管白貓黑貓,能減少對象的,都是“好貓”。

讨論flutter記憶體時,我們在談論什麼

flutter從使用的語言上,可以分成3大部分,

  • Framework層 由Dart編寫,開發者接觸到頂層,用于應用層開發
  • Engine 層,由C/C++編寫,主要進行圖形渲染
  • Embedder層,由植入層語言編寫,如iOS使用Objective-C/swift,Android使用java

當我們從程序角度談論flutter應用的記憶體時,指的是這個三者所有的記憶體的總和。

為簡化,這裡可以簡單的以使用者能直接接觸的代碼為邊界,将其分成DartVM和native記憶體,

DartVM指Dart虛拟機占用記憶體,而native記憶體包含Engine和平台相關的代碼運作的記憶體。

從渲染原理出發探究Flutter記憶體洩漏(超詳細)背景flutter記憶體都包含哪些Dart綁定層如何工作通過檢測渲染樹節點來檢測記憶體洩漏總結與展望

既然說Flutter的使用者能接觸到的最直接的對象都是使用Dart語言生成的對象,那麼對于Engine層的對象的建立與銷毀,使用者似乎鞭長莫及了?這就不得不說Dart虛拟機綁定層的設計了。

Dart綁定層如何工作

出于性能或者跨平台或其他原因,腳本語言或者基于虛拟機的語言都會提供c/c++或函數對象綁定到具體語言對象的接口,以便在語言中接着操控c/c++對象或函數,這層API稱為綁定層。例如: 最易嵌入應用程式中的

Lua binding

Javascript V8 引擎的binding

等等。

Dart虛拟機在初始化時,會将C++聲明的某個類或者函數和某個函數和Dart中的某個類或者綁定起來,依次注入Dart運作時的全局周遊中,當Dart代碼執行某一個函數時,便是指向具體的C++對象或者函數。

下面是幾個常見的綁定的幾個c++類和對應的Dart類

flutter::EngineLayer --> ui.EngineLayer

flutter::FrameInfo --> ui.FrameInfo

flutter::CanvasImage --> ui.Image

flutter::SceneBuilder --> ui.SceneBuilder

flutter::Scene --> ui.Scene

ui.SceneBuilder

一個例子了解下Dart是如何綁定c++對象執行個體,并且控制這個c++執行個體的析構工作。

Dart層渲染過程是配置的layer渲染樹,并且送出到c++層進行渲染的過程。

ui.SceneBuilder

便是這顆渲染樹的容器
從渲染原理出發探究Flutter記憶體洩漏(超詳細)背景flutter記憶體都包含哪些Dart綁定層如何工作通過檢測渲染樹節點來檢測記憶體洩漏總結與展望
  1. Dart代碼調用構造函數

    ui.SceneBuilder()

    時,調用c++方法

    SceneBuilder_constructor

  2. 調用

    flutter::SceneBuilder

    的構造方法并生成c++執行個體sceneBuilder
  3. flutter::SceneBuilder

    繼承自記憶體計數對象

    RefCountedDartWrappable

    ,對象生成後會記憶體計數加1
  4. 将生成c++執行個體sceneBuilder使用Dart的API生成一個

    WeakPersitentHandle

    ,注入到Dart上下中。在這裡之後,Dart便可使用這個

    builder

    對象,便可操作這個c++的

    flutter::SceneBuilder

    執行個體。
  5. 程式運作許久後,當Dart虛拟機判斷Dart 對象builder沒有被任何其他對象引用時(例如簡單的情況是被置空builder=null,也稱為無可達性),對象就會被垃圾回收器(Garbage Collection)回收釋放,記憶體計數将會減一
  6. 當記憶體計數為0時,會觸發c++的析構函數,最終c++執行個體指向的記憶體塊被回收

可以看到,Dart是通過将C/C++執行個體封裝成WeakPersitentHandle且注入到Dart上下文的方式,進而利用Dart虛拟機的GC(Garbage Collection)來控制C/C++執行個體的建立和釋放工作

更直白而言,隻要C/C++執行個體對應的Dart對象能正常被GC回收,C/C++所指向的記憶體空間便會正常釋放。

WeakPersistentHandle是什麼

因為Dart對象在VM中會因為GC整理碎片化中經常移動,是以使用對象時不會直接指向對象,而是使用句柄(handle)的方式間接指向對象,再者c/c++對象或者執行個體是介乎于Dart虛拟機之外,生命周期不受作用域限制,且一直長時間存在于整個Dart虛拟機中,是以稱為常駐(Persistent),是以WeakPersistentHandle專門指向生命周期與常在的句柄,在Dart中專門用來封裝C/C++執行個體。

在flutter官方提供的Observatory工具中,可以檢視所有的WeakPersistentHandle對象

從渲染原理出發探究Flutter記憶體洩漏(超詳細)背景flutter記憶體都包含哪些Dart綁定層如何工作通過檢測渲染樹節點來檢測記憶體洩漏總結與展望

其中Peer這欄也就是封裝c/c++對象的指針

從渲染原理出發探究Flutter記憶體洩漏(超詳細)背景flutter記憶體都包含哪些Dart綁定層如何工作通過檢測渲染樹節點來檢測記憶體洩漏總結與展望

Dart對象的可達性

Dart對象釋放會被垃圾回收器(Garbage Collection)進行釋放,是通過判定對象是否還有可達性(availability)來達到的。可達性是指通過某些根節點出發,通過對象與對象間的引用鍊去通路對象,如可通過引用鍊去通路對象,則說明對象有可達性,否則無可達性。

黃色有可達性,藍色無可達性

從渲染原理出發探究Flutter記憶體洩漏(超詳細)背景flutter記憶體都包含哪些Dart綁定層如何工作通過檢測渲染樹節點來檢測記憶體洩漏總結與展望

難以察覺的記憶體洩漏

看到這裡我們會發現一個問題,其實我們很難從Dart側難感覺C/C++對象的消亡,因為Dart對象無統一的如同C++類一樣的析構函數,一旦對象因為循環引用等的原因被長期其他對象長期引用,GC将無法将其釋放,最終導緻記憶體洩漏。

将問題放大一點,我們知道flutter是一個渲染引擎,我們通過編寫Dart語言建構出一顆Widget樹,進而經過繪制等過程簡化成Element樹,RenderObject樹,Layer樹,并将這顆Layer樹送出至C++層,進而使用Skia進行渲染。

從渲染原理出發探究Flutter記憶體洩漏(超詳細)背景flutter記憶體都包含哪些Dart綁定層如何工作通過檢測渲染樹節點來檢測記憶體洩漏總結與展望

如果某個Wigdet樹或Element樹的某個節點長期無法得到釋放,将可能造成他的子節點也牽連着無法釋放,将洩漏的記憶體空間迅速擴大。

從渲染原理出發探究Flutter記憶體洩漏(超詳細)背景flutter記憶體都包含哪些Dart綁定層如何工作通過檢測渲染樹節點來檢測記憶體洩漏總結與展望

例如,存在兩個A,B界面,A界面通過Navigator.push的方式添加B界面,B界面通過Navigator.pop回退到A。

如果B界面因為某些寫法的緣故導緻B的渲染樹雖然被從主渲染樹解開後依然無法被釋放,這會導緻整個原來B的子樹都無法釋放。

通過檢測渲染樹節點來檢測記憶體洩漏

基于上面的這一個情況,我們其實可以通過對比目前幀使用到的渲染節點個數,對比目前記憶體中渲染節點的個數來判斷前一個界面釋放存在記憶體洩漏的情況。

Dart代碼中都是通過往

ui.SceneBuilder

添加EngineLayer的方式去建構渲染樹,那麼我們隻要檢測c++中記憶體中EngineLayer的個數,對比目前幀使用的EngineLayer個數,如果記憶體中的EngineLayer個數長時間大于使用的個數,那麼我們可以判斷存在有記憶體洩漏

從渲染原理出發探究Flutter記憶體洩漏(超詳細)背景flutter記憶體都包含哪些Dart綁定層如何工作通過檢測渲染樹節點來檢測記憶體洩漏總結與展望

依然以上次A頁面pushB界面,B界面pop回退A界面為例子。

正常無記憶體洩漏的情況下,正在使用的layer個數(藍色),記憶體中的layer個數(橙色)兩條曲線的雖然有波動,但是最終都會比較貼合。

但是在B頁面存在記憶體洩漏的時候,退到A界面後,B樹完全無法釋放,記憶體中的layer個數(橙色)無法最終貼合藍色曲線(正在使用的layer個數)

也就是說,對于渲染而言,如果代碼導緻Widget樹或Element樹長時間無法被GC回收,很可能會導緻嚴重的記憶體洩漏情況。

如何導緻記憶體洩漏?

目前發現異步執行的代碼的場景(Feature, async/await,methodChan)長期持有傳入的BuildContext,導緻 element 被移除後,依然長期存在,最終導緻以及關聯的 widget, state 發生洩漏。

再繼續看B頁面洩漏的例子

從渲染原理出發探究Flutter記憶體洩漏(超詳細)背景flutter記憶體都包含哪些Dart綁定層如何工作通過檢測渲染樹節點來檢測記憶體洩漏總結與展望

正确與錯誤的寫法的差別在于,錯誤的僅是在調用Navigator.pop之前,使用異步方法Future引用了BuildContext,便會導緻B界面記憶體洩漏。

怎麼發現洩漏點?

目前flutter記憶體洩漏檢測工具的設計思路是,對比界面進入前後的對象,尋找出未被釋放的對象,進而檢視未釋放的引用關系(Retaining path或Inbound references),再結合源碼進行分析,最後找到錯誤代碼。

使用Flutter自帶的Observatory縱然可以一個一個檢視每個洩漏對象的引用關系,但是對于一個稍微複雜一點的界面而言,最終生成的layer個數是非常龐雜的,想要在Observatory所有的洩漏對象中找到有問題的代碼是一項非常龐雜的任務。

為此我們将這些繁雜的定位工作都進行了可視化。

我們這裡将每一幀送出到engine的所有EngineLayer進行了一個記錄,并且以折線圖的形式記錄下來,如果上文說的記憶體中的layer個數異常的大于使用中的layer個數,那麼就可判斷前一個頁面存在有記憶體洩漏。

從渲染原理出發探究Flutter記憶體洩漏(超詳細)背景flutter記憶體都包含哪些Dart綁定層如何工作通過檢測渲染樹節點來檢測記憶體洩漏總結與展望

進而,還可以抓取目前頁面的layer樹的結構,用以輔助定位具體由哪個RenderObject樹生成的Layer樹,進而繼續分析由哪個Element節點生成的RenderObject節點

從渲染原理出發探究Flutter記憶體洩漏(超詳細)背景flutter記憶體都包含哪些Dart綁定層如何工作通過檢測渲染樹節點來檢測記憶體洩漏總結與展望

或者也可以列印出WeakPersitentHandle的引用鍊輔助分析

從渲染原理出發探究Flutter記憶體洩漏(超詳細)背景flutter記憶體都包含哪些Dart綁定層如何工作通過檢測渲染樹節點來檢測記憶體洩漏總結與展望

但如今的痛點依然存在,依然需要通過檢視Handle的引用鍊,結合源碼的分析才能最終比較快捷的定位問題。

這也是接下來亟需解決的問題。

總結與展望

  • 我們這種從渲染樹的角度去探尋flutter記憶體洩漏的方法,可以推廣到是以其他Dart不同類型的對象。
  • 開發者在編寫代碼時,需要時刻注意異步調用,以及時刻注意操縱的Element會否被引用而導緻無法釋放

閑魚作為長期深耕flutter的團隊,也在持續在flutter工具鍊中持續發力,當然也少不了這一重要的記憶體檢測工具的深入開發,歡迎大家持續關注!