从渲染原理出发探究Flutter内存泄漏(超详细)( 三 )


从渲染原理出发探究Flutter内存泄漏(超详细)
本文插图
例如 , 存在两个A , B界面 , A界面通过Navigator.push的方式添加B界面 , B界面通过Navigator.pop回退到A 。 如果B界面因为某些写法的缘故导致B的渲染树虽然被从主渲染树解开后依然无法被释放 , 这会导致整个原来B的子树都无法释放 。
通过检测渲染树节点来检测内存泄漏
基于上面的这一个情况 , 我们其实可以通过对比当前帧使用到的渲染节点个数 , 对比当前内存中渲染节点的个数来判断前一个界面释放存在内存泄漏的情况 。
Dart代码中都是通过往ui.SceneBuilder添加EngineLayer的方式去构建渲染树 , 那么我们只要检测c++中内存中EngineLayer的个数 , 对比当前帧使用的EngineLayer个数 , 如果内存中的EngineLayer个数长时间大于使用的个数 , 那么我们可以判断存在有内存泄漏
从渲染原理出发探究Flutter内存泄漏(超详细)
本文插图
依然以上次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内存泄漏(超详细)
本文插图
正确与错误的写法的区别在于 , 错误的仅是在调用Navigator.pop之前 , 使用异步方法Future引用了BuildContext , 便会导致B界面内存泄漏 。
怎么发现泄漏点?
目前flutter内存泄漏检测工具的设计思路是 , 对比界面进入前后的对象 , 寻找出未被释放的对象 , 进而查看未释放的引用关系(Retaining path或Inbound references) , 再结合源码进行分析 , 最后找到错误代码 。
使用Flutter自带的Observatory纵然可以一个一个查看每个泄漏对象的引用关系 , 但是对于一个稍微复杂一点的界面而言 , 最终生成的layer个数是非常庞杂的 , 想要在Observatory所有的泄漏对象中找到有问题的代码是一项非常庞杂的任务 。
为此我们将这些繁杂的定位工作都进行了可视化 。
我们这里将每一帧提交到engine的所有EngineLayer进行了一个记录 , 并且以折线图的形式记录下来 , 如果上文说的内存中的layer个数异常的大于使用中的layer个数 , 那么就可判断前一个页面存在有内存泄漏 。
从渲染原理出发探究Flutter内存泄漏(超详细)
本文插图
进而 , 还可以抓取当前页面的layer树的结构 , 用以辅助定位具体由哪个RenderObject树生成的Layer树 , 进而继续分析由哪个Element节点生成的RenderObject节点
从渲染原理出发探究Flutter内存泄漏(超详细)
本文插图
或者也可以打印出WeakPersitentHandle的引用链辅助分析

从渲染原理出发探究Flutter内存泄漏(超详细)
本文插图
但如今的痛点依然存在 , 依然需要通过查看Handle的引用链 , 结合源码的分析才能最终比较快捷的定位问题 。


推荐阅读