Flutter iOS engine销毁实践

flutter从1.12开始支持add to app,相信有不少人使用该模式将Flutter集成到自己的项目里。打开一个Flutter页面很简单,如,Android中使用startActivity(FlutterActivity.createDefaultIntent(this))就可以打开Flutter页面,为了减少FlutterEngine初始化时间,一般会选择pre-warm FlutterEngine的方式。但是,当用户进入APP,至始至终都没有打开Flutter页面,FlutterEngine会一直存在内存中(如果对pre-warming FlutterEngine需要多少内存可以查看官方的测试数据),造成内存浪费。更糟糕的情况,FlutternEngine一直存在内存中得不到回收,内存不足的时候甚至会发生OOM。

之前在接入旅行首页的时候,我们使用的是延迟加载flutter engine的方式,但是engine一直无法释放,原因是pigeon代码在传入binaryMessenger的时候内部类会强引用,导致viewController一直无法deinit


image2021-1-26_14-47-39.png

这里我写了一个弱代理模式,打破之间的强引用:


image2021-1-26_15-12-38.png

延迟创建FlutterEngine
本次探索实践了新方法,为了解决pre-warm FlutterEngine可能会造成内存浪费的问题,可以在用户第一次打开Flutter页面时才创建FlutterEngine,将其缓存起来,减少用户再次打开Flutter页面时FlutterEngine的初始化时间。但是,延迟创建FlutterEngine会出现在第一个Flutter帧渲染出来前出现白屏的情况,为了优化用户体验可以为页面加上Splash Screen。下面看看在iOS中实现(android同理)。

由于FlutterViewController没有提供类似Android中provideFlutterEngine的方法,所以需要实现自己Container View Controller,先显示Splash Screen,然后创建并缓存FlutterEngine,再创建FlutterViewController。

class BaseRootFlutterViewController: UIViewController {
    override func viewDidLoad() {
    super.viewDidLoad()
    let splashViewController = SplashViewControler()
    splashViewController!.willMove(toParent: self)
    addChild(splashViewController!)
    splashViewController!.view.frame = self.view.bounds
    view.addSubview(splashViewController!.view)
    splashViewController!.didMove(toParent: self)
let engine = (UIApplication.shared.delegate as! AppDelegate)
    .getFlutterEngine()

let flutterViewController = FlutterViewController(
  engine: engine,
  nibName: nil,
  bundle: nil)
flutterViewController.setFlutterViewDidRenderCallback {
  [unowned self, splashViewController] in
  self.flutterViewDidRenderCallback?()

  if (splashViewController != nil) {
    UIView.animate(withDuration: 0.5, animations: {
      splashViewController!.view.alpha = 0
    }, completion: { (_) -> Void in
      splashViewController!.willMove(toParent: nil)
      splashViewController!.view.removeFromSuperview()
      splashViewController!.removeFromParent()
    })
  }
}
flutterViewController.willMove(toParent: self)
addChild(flutterViewController)
flutterViewController.view.frame = self.view.bounds
view.addSubview(flutterViewController.view)
flutterViewController.didMove(toParent: self)

  }
}

管理FlutterEngine

上面展示了如何在Android和iOS中实现延迟创建FlutterEngine。但在实际开发中需要处理从Flutter页面跳转Native页面,再从Native页面跳转Flutter页面的场景(Flutter -> Native -> Flutter),这是目前使用单个FlutterEngine(不使用第三方库)无法解决的。这种情况不能每个Flutter页面都创建并缓存FlutterEngine,因为如果用户打开多个Flutter页面,然后将Flutter页面都关闭后,之后用户只重新打开一个Flutter页面(例如,用户打开了3个Flutter页面,然后将3个页面都关闭,之后只打开1个Flutter页面),其他被缓存的FlutterEngine就造成浪费,而且内存也得不到释。但为了更好的用户体验,不能每次都创建“一次性”FlutterEngine(随着Flutter页面创建,随着Flutter页面销毁)。这便需要我们管理好FlutterEngine,允许设置可缓存FlutterEngine的数量,超过这个数量的Flutter页面都使用“一次性”FlutterEngine,以解决缓存太多FlutterEngine的问题。同时要允许内存紧张的时候将FlutterEngine回收掉。下面我们来实现自己的FlutterEngine管理类。

实现Flutter Engine Cache

在实现FlutterEngine管理类之前,我们需要先解决允许内存紧张的时候将FlutterEngine回收掉的问题。这里需要实现自己的Flutter Engine Cache。

在iOS中,主要使用NSCache

class AddonFlutterEngineCache {
  ... 

  private let cachedEngines = NSCache<NSString, FlutterEngine>()

  func contains(engineId: String) -> Bool {
    return cachedEngines.object(forKey: NSString(string: engineId)) != nil
  }

  func get(engineId: String) -> FlutterEngine? {
    return cachedEngines.object(forKey: NSString(string: engineId))
  }
}

实现FlutterEngine管理类

如前面所说,需要允许设置可缓存FlutterEngine的数量,如果超过这个数量就创建“一次性”的FlutterEngine。因此需要以栈(这里使用列表来模拟栈)的方式记录FlutterEngine的使用情况,创建新FlutterEngine的时候为其分配一个Id,并将该Id进栈,页面销毁的时将Id移出栈顶。

class FlutterEngineManager {

  // 可缓存FlutterEngine数量
  var cacheFlutterEngineThreshold = 2

  private var activeEngines = [String]()

  func getFlutterEngine() -> FlutterEngine {
    let cachedEngineIds = FlutterEngineCache.shared.getCachedEngineIds()
    let cachedEngineIdsSize = cachedEngineIds.count
    let activeEngineSize = activeEngines.count
    if (!cachedEngineIds.isEmpty && activeEngineSize < cachedEngineIdsSize) {
      let existEngineId = cachedEngineIds.first(where: { (key) -> Bool in
        return !activeEngines.contains {
          return $0 == key as String
        }
      })!
  var engine = FlutterEngineCache.shared.get(engineId: existEngineId)
  if engine == nil {
    engine = createFlutterEngine(name: existEngineId)
  }
  activeEngines.append(existEngineId)

  return engine!
}

let flutterEngine: FlutterEngine
let cacheEngineId: String
if cachedEngineIdsSize < cacheFlutterEngineThreshold {
  cacheEngineId = "cache_engine_\(cachedEngineIdsSize + 1)"
  flutterEngine = createFlutterEngine(name: cacheEngineId)
  FlutterEngineCache.shared.put(engineId: cacheEngineId, engine: flutterEngine)
} else {
  cacheEngineId = "new_engine_\(activeEngineSize - cachedEngineIdsSize + 1)"
  flutterEngine = createFlutterEngine(name: cacheEngineId)
}

activeEngines.append(cacheEngineId)

return flutterEngine

  }
private func createFlutterEngine(name: String) -> FlutterEngine {
    let engine = FlutterEngine(name: name)
    engine.navigationChannel.invokeMethod("setInitialRoute", arguments:"/")
    engine.run()
    return engine
  }
// 页面关闭时将栈顶FlutterEngine Id移除
  func inactiveEngine() {
    if !activeEngines.isEmpty {
      let cachedEngineIds = FlutterEngineCache.shared.getCachedEngineIds()
      let key = activeEngines.last!
      let removeEventChannelIndex = eventChannels.lastIndex { (k, _) -> Bool in
        !cachedEngineIds.contains(key) && k == key
      } ?? -1
      if removeEventChannelIndex != -1 {
        eventChannels.remove(at: removeEventChannelIndex)
      }
      

  activeEngines.remove(at: activeEngines.count - 1)
}

  }
}
}

先来看看实现之前的内存:
进出4-5次旅行频道之后,engine无法释放,内存暴增


image2021-1-26_15-33-27.png

实现后的效果如下:(iPhone 8 plus,3G内存)
1.刚启动时的内存:


image2021-1-26_15-14-56.png

3.打开首页时的内存:
image2021-1-26_15-15-17.png

image2021-1-26_15-15-33.png

image2021-1-26_15-6-30.png

所有VC均释放
以上

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容