看腾讯企鹅辅导如何解决Flutter内存泄漏

栏目: IOS · 发布时间: 5年前

内容简介:辅导团队接入Flutter,上线核心页面已有半年有余。目前积累的主要经验包括架构设计、页面栈管理、跨平台接入层MJFlutter SDK、减包、首屏和内存优化、行为流跟踪等等。这篇文章分享下辅导团队在Flutter内存泄漏上的处理。大家应该都注意到,目前业界接入Flutter,尤其在iOS端,都是使用单例的方式,也就是让FlutterViewController独立出来。那本文以iOS为例,分享下辅导团队的单例方式。我们在接入的时候,当时业界并没有很多案例分享自己的接入方式。而且我们的应用场景包括多个独立入

看腾讯企鹅辅导如何解决Flutter内存泄漏

辅导团队接入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也表示他们暂时也无能为力。

看腾讯企鹅辅导如何解决Flutter内存泄漏

看腾讯企鹅辅导如何解决Flutter内存泄漏

Google的技术讲道理不应该会有这样的问题,而且Flutter Stable已经发出几个版本了,一直没有得到合适的解决。那我们的理解主要包括两个原因:

  • 引擎仍然使用MRC的方式实现,有Retain的地方就需要有Release,这么复杂的功能内存管理相对会非常困难

  • Flutter从设计之初就希望整个App使用它的外壳,全局一个FlutterVC实现所有UI。这样也就不存在内存泄不泄露的问题了,所以官方能够这个问题没有得到解决的前提下,继续发布新版本

可能Google也没有想到,确实很多科技公司都在尝试Flutter,但大多数都是采用混合模式。所以Google暂时没有着重解决这个问题,那我们就需要自力更生了。

三. 动手实践

仔细阅读源码,我们发现整个链路,从FlutterEngine开始,内存引用就是一个大循环。

看腾讯企鹅辅导如何解决Flutter内存泄漏

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;
}

看腾讯企鹅辅导如何解决Flutter内存泄漏

内存泄漏的主要原因在于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,就无法再重新启动了,因为它也是单例的方式实现。不过相比与之前的内存暴增而无法释放已经好太多了。

看腾讯企鹅辅导如何解决Flutter内存泄漏

修改完源码,我们需要创建自己的Engine,替换Flutter自身的引擎。网上讲述构建自己的Engine的文章很多,大家可以自己尝试下。

以上内容,不当之处欢迎指正、交流。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

游戏编程算法与技巧

游戏编程算法与技巧

【美】Sanjay Madhav / 刘瀚阳 / 电子工业出版社 / 2016-10 / 89

《游戏编程算法与技巧》介绍了大量今天在游戏行业中用到的算法与技术。《游戏编程算法与技巧》是为广大熟悉面向对象编程以及基础数据结构的游戏开发者所设计的。作者采用了一种独立于平台框架的方法来展示开发,包括2D 和3D 图形学、物理、人工智能、摄像机等多个方面的技术。《游戏编程算法与技巧》中内容几乎兼容所有游戏,无论这些游戏采用何种风格、开发语言和框架。 《游戏编程算法与技巧》的每个概念都是用C#......一起来看看 《游戏编程算法与技巧》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

html转js在线工具
html转js在线工具

html转js在线工具