一行程式碼教你解決FlutterPlatformViews記憶體洩露(memory leak)

AShawn發表於2019-02-21

背景

Flutter正式釋出1.0release版本,其中有Platform Views可以支援iOS,Android原生View嵌入Flutter中進行展示,如 developers.googleblog.com/2018/12/flu… 這篇文章所述。

對應issue:github.com/flutter/flu… Google已經merge:github.com/flutter/eng…

業務需求需要Youtube播放器,考慮到有Platform Views支援,採用了原生實現Youtube播放功能,將View嵌入到Flutter中使用。iOS端在實現了視訊列表以後,發現看了幾個視訊以後記憶體就會爆掉,問題很嚴重。

業務程式碼方向調查

初步懷疑還是原生UI建立後的迴圈引用問題,參考 github.com/flutter/plu… 實現了一個簡單的UIView,核心程式碼如下:

  int i = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView example'),
        actions: <Widget>[
        ],
      ),
      body: (i%2 == 0)? Container(color: Colors.red,) : WebView(
        initialUrl: 'https://flutter.io',
        javascriptMode: JavascriptMode.unrestricted,
        onWebViewCreated: null,
      ),
      floatingActionButton: favoriteButton(),
    );
  }
複製程式碼

通過不斷的點選按鈕setState重新整理UI達到PlatformView的建立銷燬,發現最簡單的UIView在經過重複的建立銷燬後依然會存在記憶體洩露的問題,而不使用PlatformView,同樣的程式碼建立銷燬Flutter的Widget不會出現問題,這樣可以斷定業務實現上沒有問題,程式碼出在Flutter engine的底層實現上。

底層engine程式碼方向調查

關於如何使用Flutter engine可以參考 juejin.im/post/5c24ac… 引入engine進行調查,非常方便。

UIKit方向調查

同時看到flutter engine的issue中有 github.com/flutter/flu… ,其實是有人遇到同樣的問題了,官方沒有解決問題把問題關了,只能靠我們自己進行調查了。

因為有之前解決Flutter engine記憶體洩露的經驗,相關內容可以參考 juejin.im/post/5c24ad…

開始調查engine的程式碼想當然的以為會是Google在實現PlatformView時有類似的迴圈引用問題導致建立的原生UI無法被釋放。Google在實現PlatformView時會建立FlutterPlatformView和FlutterOverlayView兩個View,通過在dealloc新增log方式可以發現建立的View都得到了釋放,基本可以判斷UIKit這部分沒有問題。

這時沒有什麼好辦法才使用了Apple的查記憶體洩露的工具,調查結果如下圖所示:

memory leak

可以看到主要的記憶體洩露來源都是IOSurface,檢視堆疊可以看到是 renderbufferStorage:fromDrawable:方法造成的,可以檢視蘋果官方文件 developer.apple.com/documentati… 知道這個方法其實是將framebuffer與CAEAGLLayer進行繫結。

熟悉OpenGL的同學應該可以知道問題基本上可以定位到是Flutter engine使用CAEAGLLayer渲染時,申請的記憶體沒有得到釋放導致的。

OpenGL方向調查

找到呼叫 renderbufferStorage:fromDrawable:方法的地方,是在IOSGLRenderTarget中,大概看了一下是基本的OpenGL渲染模組,通過向上查詢,在FlutterOverlayView中找到了引用的地方與UIKit方向調查的結果吻合,在實現PlatformView時會一層層傳下來建立了IOSGLRenderTarget,層級關係如下所示:

render

通過調查相關節點的記憶體釋放情況,發現路徑中建立的東西都得到了釋放,這時候就很困惑了,好像整個調查卡主了。再返回去看IOSGLRenderTarget的渲染流程,最終發現在解構函式中釋放建立的OpenGL Framebuffer程式碼如下:

IOSGLRenderTarget::~IOSGLRenderTarget() {
  FML_DCHECK(glGetError() == GL_NO_ERROR);

  // Deletes on GL_NONEs are ignored
  glDeleteFramebuffers(1, &framebuffer_);
  glDeleteRenderbuffers(1, &colorbuffer_);

  FML_DCHECK(glGetError() == GL_NO_ERROR);
}
複製程式碼

終於發現了問題所在,在DeleteFramebuffers時,Google沒有先設定上下文,在Flutter這種使用多個context進行渲染的的結構中,進行gl操作時最好都提前設定一下當前對應的上下文,避免其他程式碼更改了上下文,這邊再進行gl操作時操作無效。所以最後我們只需要加一行程式碼就可以解決這個大問題,解決程式碼如下:

IOSGLRenderTarget::~IOSGLRenderTarget() {
  [EAGLContext setCurrentContext:context_];
  FML_DCHECK(glGetError() == GL_NO_ERROR);

  // Deletes on GL_NONEs are ignored
  glDeleteFramebuffers(1, &framebuffer_);
  glDeleteRenderbuffers(1, &colorbuffer_);

  FML_DCHECK(glGetError() == GL_NO_ERROR);
}
複製程式碼

編譯後,工程中使用我們編譯出來的framework,記憶體問題就得到了解決!以後可以方便的在Flutter工程中使用原生View了!

flutter技術積累相關連結

flutter通用基礎庫flutter_luakit_plugin

flutter_luakit_plugin使用例子

《手把手教你編譯Flutter engine》

《手把手教你解決 Flutter engine 記憶體洩漏》

修復記憶體洩漏後的flutter engine(可直接使用)

修復記憶體洩漏後的flutter engine使用例子

持續更新中...

相關文章