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
这里我写了一个弱代理模式,打破之间的强引用:
延迟创建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无法释放,内存暴增
实现后的效果如下:(iPhone 8 plus,3G内存)
1.刚启动时的内存:
3.打开首页时的内存:
所有VC均释放
以上