Flutter 多引擎渲染,组件支持 FlutterPlugin

718 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情

Flutter 多引擎系列: 《Flutter 多引擎渲染,在稿定 App 的实践》 等等,专栏里可查看

背景

虽然 FGUIComponentAPI 方案中采用的内置 messageChannel 的方式来达到 Flutter 多引擎互不干扰的消息通信。

但市面上很多有名的/没名的第三方库都有使用 FlutterPlugin 的方式来与 Native 通信(源生路径、设备信息等等)。

那如果“跨端 UI 组件化”没有支持 FlutterPlugin 的加载,会导致在使用第三方库的时候要变的十分谨慎,这不利于开发效率,也不利于代码复用。

举个栗子:

path_provider 是在 Flutter 中十分常用的获取源生各种文件路径插件,而且会被很多的其他第三方库依赖(吐槽下它不没有兼容 Web)。

那如果 FlutterEngine 不支持 FlutterPlugin,在使用 cached_network_image(相当于 iOS 的 SDImage)第三方插件做图片缓存时,图片就显示不出来,因为拿不到 Native 路径、无法缓存。

(Debug 下,找到了图片显示不了的原因,如下图位置)

image2022-6-2_17-5-45.png

FlutterPlugin 加起来很容易吗?

理论上很容易

Flutter 编译后会生成一个 FlutterPluginRegistrant 库,提供 registerWithRegistry: 方法可以很容易的把整个 Flutter 库中的插件都注册到 FlutterEngine 上。

比如 flutter_boost 也是在它代码里这样做的:

image2022-6-2_17-13-21.png

以上代码是运行时调用,等价于

[GeneratedPluginRegistrant registerWithRegistry:self.engine];

也是我们自定义的插件能在基于 flutter_boost 的页面上正常使用的根本所在。

But

我们项目里的自实现的插件大哥们是存在内存循环引用问题的,具体是哪一个或哪几个也很难去细究出来。在 flutter_boost 上问题并不明显,毕竟插件由单例常持有且只有一份,这在单引擎渲染上也是合理,后续页面可以更快加载,单个的内存占用体量也在可忍受范围内。

但是在 Flutter 多引擎渲染下,如果使用上面全部加载的方式,FlutterEngine 根本就释放不掉,内存问题就变的十分十分恐怖。

测试一下:

image2022-6-2_17-26-18.png

同样的使用全部加载的方式,打开后关闭再打开同一个跨端 UI 组件,结果如下:

image2022-6-2_17-24-4.png

每次加载后都不能完全释放,只能释放一点点内存,boom!关闭重开后,内存基本是 double 增长。

为了证明是我们自研发的插件大哥们导致的问题,在测试方式一致的情况下,选择部分的插件加载,如下图示:

image2022-6-2_17-35-54.png

结果如下:

image2022-6-2_17-33-48.png

明显的每次加载都完全释放掉了,这就说明是我们自身开发的插件是有内存问题的。 O.O

解决方案

解决我们 plugins 中的循环引用问题

虽然可以解决问题,但也无法控制后续 plugin 开发上不再出现内存问题,一旦出现就容易变成灾难性的,无法做到防裂化。

而且有些 plugins 是有使用 ffi 的,这导致更难判断出内存泄漏发生在哪里。

更重要的是可能也不光是我们自己开发的插件,可能有些第三方插件也会有内存循环引用导致的不释放问题。

只注册第三方库的 plugins

那我们换一种思路,不要全部注册进去,在多引擎渲染中只关心自己需要的插件。

采用手动注册的方式,在 component_foundations 中提供插件名注册的地方,好处是后期可以增加我们自己安全的 plugins。

image2022-6-2_19-18-33.png

如上图,增加插件配置,即可在所有 UI 组件的地方使用注册过的插件能力。

(当然大家别被误导了,这个 registrar_plugins 是自定义的)

这在 iOS 上会根据 FGUIComponentAPI 自动生成到代码里。

- (void)registrarPlugins {
    [NSClassFromString(@"FLTPathProviderPlugin") registerWithRegistrar:[self.engine registrarForPlugin:@"FLTPathProviderPlugin"]];
    [NSClassFromString(@"SqflitePlugin") registerWithRegistrar:[self.engine registrarForPlugin:@"SqflitePlugin"]];
}

也放一下 FGUIComponentAPI 这一部分的逻辑

Ruby:

  def OCGenerator.register_plugin
    yaml_path = File.join($project_path, "components", "component_foundations", "pubspec.yaml")
    map = YAML.load(File.open(yaml_path))
    array = map["registrar_plugins"]["iOS"]["plugins"]
    list = []
    array.each do |name|
      list << "    [NSClassFromString(@\"#{name}\") registerWithRegistrar:[self.engine registrarForPlugin:@\"#{name}\"]];"
    end

    ouput_path = File.join($ios_root_path, "FGUIComponentAPI", "FGUIComponentAPI", "Classes", "common", "FGUIComponent.m")
    content = File.read(ouput_path)
    content = content.gsub("${PLUGINS}", list.join("\n"))
    file = File.new(ouput_path, "w")
    if file
      file.syswrite(content)
    end
    file.close
  end

随便说一句,可以看到 FGUIComponentAPI 实际上跟公司项目绑定很深

后续

Android 没有遇到类似的问题,这一部分的实现是怎么样的也没有具体了解,有了解的同学可以分享一下 ~

如果对你开发学习上有丝丝作用,请点个赞[开心] ~