背景
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的查記憶體洩露的工具,調查結果如下圖所示:
可以看到主要的記憶體洩露來源都是IOSurface,檢視堆疊可以看到是 renderbufferStorage:fromDrawable:方法造成的,可以檢視蘋果官方文件 developer.apple.com/documentati… 知道這個方法其實是將framebuffer與CAEAGLLayer進行繫結。
熟悉OpenGL的同學應該可以知道問題基本上可以定位到是Flutter engine使用CAEAGLLayer渲染時,申請的記憶體沒有得到釋放導致的。
OpenGL方向調查
找到呼叫 renderbufferStorage:fromDrawable:方法的地方,是在IOSGLRenderTarget中,大概看了一下是基本的OpenGL渲染模組,通過向上查詢,在FlutterOverlayView中找到了引用的地方與UIKit方向調查的結果吻合,在實現PlatformView時會一層層傳下來建立了IOSGLRenderTarget,層級關係如下所示:
通過調查相關節點的記憶體釋放情況,發現路徑中建立的東西都得到了釋放,這時候就很困惑了,好像整個調查卡主了。再返回去看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 engine 記憶體洩漏》
修復記憶體洩漏後的flutter engine(可直接使用)
持續更新中...