内容简介:辅导团队接入Flutter,上线核心页面已有半年有余。目前积累的主要经验包括架构设计、页面栈管理、跨平台接入层MJFlutter SDK、减包、首屏和内存优化、行为流跟踪等等。这篇文章分享下辅导团队在Flutter内存泄漏上的处理。大家应该都注意到,目前业界接入Flutter,尤其在iOS端,都是使用单例的方式,也就是让FlutterViewController独立出来。那本文以iOS为例,分享下辅导团队的单例方式。我们在接入的时候,当时业界并没有很多案例分享自己的接入方式。而且我们的应用场景包括多个独立入
辅导团队接入Flutter,上线核心页面已有半年有余。目前积累的主要经验包括架构设计、页面栈管理、跨平台接入层MJFlutter SDK、减包、首屏和内存优化、行为流跟踪等等。这篇文章分享下辅导团队在Flutter内存泄漏上的处理。
一. 起因
大家应该都注意到,目前业界接入Flutter,尤其在iOS端,都是使用单例的方式,也就是让FlutterViewController独立出来。那本文以iOS为例,分享下辅导团队的单例方式。我们在接入的时候,当时业界并没有很多案例分享自己的接入方式。而且我们的应用场景包括多个独立入口、以及多个二级和三级页面。那简单的处理,当然就是每进入一个入口,或者每次进入二级页面,分别开启一个FlutterVC。
开启一个FlutterVC,set initial route,哇,这种方式很香,好使。结果,当然是很致命,内存真的是暴增,低端机已经卡的不行了。老大们要是看到这个效果,肯定劈了我们。平复下心情,分下下原因:
1.每次启动一个FlutterVC,引擎就会帮我们开启一个dart vm,gpu、ui和io线程。显然,我们之前的处理方式太消耗资源了
if (shell::IsIosEmbeddedViewsPreviewEnabled()) { blink::TaskRunners task_runners(threadLabel.UTF8String, // label fml::MessageLoop::GetCurrent().GetTaskRunner(), // platform fml::MessageLoop::GetCurrent().GetTaskRunner(), // gpu fml::MessageLoop::GetCurrent().GetTaskRunner(), // ui fml::MessageLoop::GetCurrent().GetTaskRunner() // io ); // Create the shell. This is a blocking operation. _shell = shell::Shell::Create(std::move(task_runners), // task runners std::move(settings), // settings on_create_platform_view, // platform view creation on_create_rasterizer // rasterzier creation ); }
2. 从Flutter页面退出后,即退回入口 处的Native页面,Flutter占有的内存并不会释放掉。 我们在FlutterViewController的dealloc处加上断点,发现明明有被断住,这就很奇怪了
经过分析,以上两个问题是导致内存暴增的主要原因。有这样的严重问题,那我们就需要优先处理。对于第一问题,单例会是一个很好的方式,后面我们也发现业界普遍也都是这种方式。我们在AppDelegate中声明了Flutter,并进行相关的初始化。
二.Flutter Engine
为了解决第二个问题,我们注意到了Flutter Engine这个关键人物。
Flutter从1.0版本后引入了FlutterEngine,目的是解决早先版本中FlutterVC循环引用的问题。但最终的结果是FlutterVC可以正常释放(dealloc函数可以被断点断住),但FlutterEngine却hold住了。
看下官方给的定义,这个类主要用来管理Shell(TaskRunner),开启DartVM,基本的Channel(settng、lifecircle、platform)等等。可以看得出来,这是一个非常重要的对外接口。
为什么我们会注意到FlutterEngine这个关键人物呢,因为官方的issue也表示他们暂时也无能为力。
Google的技术讲道理不应该会有这样的问题,而且Flutter Stable已经发出几个版本了,一直没有得到合适的解决。那我们的理解主要包括两个原因:
-
引擎仍然使用MRC的方式实现,有Retain的地方就需要有Release,这么复杂的功能内存管理相对会非常困难
-
Flutter从设计之初就希望整个App使用它的外壳,全局一个FlutterVC实现所有UI。这样也就不存在内存泄不泄露的问题了,所以官方能够这个问题没有得到解决的前提下,继续发布新版本
可能Google也没有想到,确实很多科技公司都在尝试Flutter,但大多数都是采用混合模式。所以Google暂时没有着重解决这个问题,那我们就需要自力更生了。
三. 动手实践
仔细阅读源码,我们发现整个链路,从FlutterEngine开始,内存引用就是一个大循环。
FlutterVC和FlutterEngine是一种弱引用的关系,所以FlutterVC可以正常释放。所以这两个之前我们不用太在意循环引用的问题,正常释放FlutterVC就可以了。
那么如何正确使用FlutterVC呢,FlutterEngine是从1.0后引入的。之前的版本,我们初始化一个FlutterVC,可以直接使用alloc,init的方式。如果升级到1.0以后的版本,这种方式仍然是ok的,FlutterVC内部会自动帮我们创建一个FlutterEngine来管理内部的Channel,dartVM等。
FlutterViewController *FlutterVC = [[FlutterViewController alloc] init]; //源码 - (instancetype)init { return [self initWithProject:nil nibName:nil bundle:nil]; } - (instancetype)initWithProject:(FlutterDartProject*)projectOrNil nibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { _viewOpaque = YES; _weakFactory = std::make_unique<fml::WeakPtrFactory<FlutterViewController>>(self); _engine.reset([[FlutterEngine alloc] initWithName:@"io.flutter" project:projectOrNil]); _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]); [_engine.get() createShell:nil libraryURI:nil]; _engineNeedsLaunch = YES; [self loadDefaultSplashScreenView]; [self performCommonViewControllerInitialization]; } return self; }
既然引入了FlutterEngine,官方推荐的方式或者说正确的方式,应该是自己初始化一个FlutterEngine,用它来手动管理内部的channel等等。好处就是我们可以手动释放FlutterEngine,解决内存引用。这里有两个注意点:
1. [engine runWithEntrypoint:nil]: 这段代码表示FlutterEngine跑在主线程,不加会crash。Flutter是一个ui层面上的操作,所有的指定都需要在主线程
2. [flutterViewController setInitialRoute:@"route"]: 设置初始化路由,你会发现可能会不生效,真的很难用(Google自己说的)
- (void)initFlutter { FlutterDartProject * dart = [[FlutterDartProject alloc] init]; if (!self.engine) { FlutterEngine * engine = [[FlutterEngine alloc] initWithName:@"k12" project:dart]; [engine runWithEntrypoint:nil]; //必需 self.engine = engine; } FlutterViewController* flutterViewController = [[FlutterViewController alloc] initWithEngine:self.engine nibName:nil bundle:nil]; [GeneratedPluginRegistrant registerWithRegistry:flutterViewController]; [flutterViewController setInitialRoute:@"route"]; } //FlutterViewController源码 - (instancetype)initWithEngine:(FlutterEngine*)engine nibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil { NSAssert(engine != nil, @"Engine is required"); self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { _viewOpaque = YES; _engine.reset([engine retain]); _engineNeedsLaunch = NO; _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]); _weakFactory = std::make_unique<fml::WeakPtrFactory<FlutterViewController>>(self); [self performCommonViewControllerInitialization]; [engine setViewController:self]; } return self; }
内存泄漏的主要原因在于FlutterEngine中定义的一些methodChannel、basicMethodChannel、plugin,这些channel是一种retain的关系。我们看下面的源码FlutterEngine持有methodChannel,methodChannel中又持有FlutterEngine。关键点在于methodChannel中真正对FlutterEngine释放放到了dealloc,但实际上methodChannel因为一直持有FlutterEngine,所以一直无法走到dealloc中,因此造成了循环引用。
那为什么官方一直不修改这样的引用方式呢,我们的猜想是这里整体工程量大,MRC实现起来改动会很多,影响整体稳定性。而且如果使用方没有处理好FlutterEngine,提前释放,后面再调用methodChannel,很有可能会crash。
另外还有FlutterEngine、FlutterChannel、FlutterDartProject中block的使用,需要认真修复。我们看下面的代码,问题很明显。
大体上我们找到了FlutterEngine内存泄漏的主要原因。修改完后,我们需要一个入口,释放FlutterEngine中持有的channel们,这里建议放到FlutterViewController dealloc中调用,释放Flutter占有的内存。
四. 使用
以上方式,释放内存后。我们发现其实并没有完全释放干净,这里留存下的内存实际上是Dart VM占有的内存。官方这里也给出了解释,如果释放了Dart VM,就无法再重新启动了,因为它也是单例的方式实现。不过相比与之前的内存暴增而无法释放已经好太多了。
修改完源码,我们需要创建自己的Engine,替换Flutter自身的引擎。网上讲述构建自己的Engine的文章很多,大家可以自己尝试下。
以上内容,不当之处欢迎指正、交流。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 企鹅电竞weex实践——UI开发篇
- 南极沙漠,沙漠企鹅:微软云计算的罪与罚
- 跨越适配 & 性能那道坎,企鹅电竞 Android weex 优化
- 工具 | 企鹅电竞开发的,用于播放特效动画的实现方案
- Android 系统开发_内存泄漏篇 -- "内存泄漏"的前世今生
- 内存泄漏(增长)火焰图
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
ECMAScript6入门
阮一峰 / 电子工业出版社 / 2014-8 / 49.00元
《ECMAScript6入门》全面介绍了ECMAScript6新引入的语法特性,覆盖了ECMAScript6与ECMAScript5的所有不同之处,对涉及的语法知识给予了详细介绍,并给出了大量简洁易懂的示例代码。 《ECMAScript6入门》为中级难度,适合已有一定JavaScript语言基础的读者,用来了解这门语言的最新发展;也可当作参考手册,查寻新增的语法点。一起来看看 《ECMAScript6入门》 这本书的介绍吧!