乐趣区

关于flutter:前车之鉴聊聊钉钉-Flutter-落地桌面端踩过的坑-Dutter

作者:刘太举(驽良)

《Dutter 系列文章》将论述钉钉基于 Flutter 构建的跨四端利用框架(代号 Dutter)的技术实际与踩坑教训,共分为上、下两篇,上篇内容可点击 Dutter | 钉钉 Flutter 跨四端方案设计与技术实际,本文为下篇,感激浏览。

本文次要介绍一下钉钉 Flutter 业务灰度过程中,在桌面端遇到并解决过的几个 FlutterEngine 层面的 Bug。具体蕴含:

  • Mac 端:
  1. FlutterEngine 退出之后内存透露问题;
  2. FlutterEngine shutdown 阶段死锁问题;
  3. 低版本 macOS OpenGL 析构阶段 Crash 问题;
  • Windows 端:
  1. Win7 设施渲染模块「Crash + 残影」问题;
  2. FlutterPlugin 注册阶段野指针 Crash;
  3. Flutter Window 可见性变动之后页面白屏。

上面来为大家别离介绍一下。

FlutterEngine Mac 端问题

1.1 FlutterEngine 退出之后内存透露问题

问题背景

Mac 端 FlutterViewController 在销毁之后,其开拓的内存并未并理论开释,会呈现内存透露问题。此问题在 Flutter issue 中有一些探讨,但始终未有明确定位。在钉钉 Mac 端 Flutter 业务灰度过程中也遇到此问题,如无奈解决将间接影响 Dutter 在 Mac 端落地的可行性:

定位剖析

一句话起因:

Mac 端 FlutterEngine 实现中对 weak property 应用不合理导致。FlutterViewController 强持有 FlutterEngine,后者持有一个指向 FlutterViewController 的 weak property。FlutterViewController 在 dealloc 流程中尝试开释 FlutterEngine,然而此时 FlutterEngine 中持有的 weak property 曾经无奈正确拜访(nil),导致开释流程未能失常执行,呈现透露。

上面联合具体实现来为大家做一个简略阐明。

因为设计到 OC 和 C++ 对象生命周期治理问题,FlutterEngine 外部对象持有关系稍微非凡一些,大抵如下图所示:

  • FlutterViewController 作为对外裸露的次要 Class,负责创立并持有 FlutterEngine 以及 FlutterView;
  • FluterEngine 在初始化阶段会本人强持有本人,并在 shutdown 时自我 Release;
  • FlutterEngine 会创立并持有 FlutterRenderer,FlutterRenderer 会强持有 FlutterView;
  • FlutterEngine 间接强持有 FlutterView;
  • FlutterEngine 有一个指向 FlutterViewController 的弱援用指针。

失常状况下,FlutterViewController 退出之后,会通过调用 FlutterEngine 的 setViewController 传入 nil 的形式,来触发 FlutterEngine shudown 动作。参考实现如下:

即失常状况下,FlutterViewController dealloc 之后应该触发 369 行代码运行,进而开释 FlutterEngine 资源。然而理论运行状况缺不是这样,在代码运行到 359 行时,尝试判断 if (_viewController != controller) 时并未成立。通过上述代码咱们晓得,controller 是内部传入的对象此时为 nil;_viewController 作为一个 weak proptry,在 FlutterViewController 进入 dealloc 流程之后也变为 nil。因此在此流程下,咱们心愿中的 shutDownEngine 办法并未被调用。

解决计划

问题定位之后解决形式就很简略了,能够在 FlutterViewController dealloc 的时候手动触发 FlutterEngine shutDownEngine 办法。并且通过在下层通过 OC 动静个性 hook 实现、或者间接批改从新编译 FlutterEngine 都能够。

但此处批改肯定要审慎,留神残缺还原 FlutterEngine 中的 shutdown 流程,否则可能导致咱们遇到的第二个问题:死锁。

1.2 FlutterEngine shutdown 阶段死锁问题

问题背景

钉钉最后在解决上述「FlutterEngine 透露」问题时,采纳了一种绝对比较简单的计划:在 FlutterViewController dealloc 办法中,手动调用 FlutterEngine 提供的 shutDownEngine 办法,手动触发相干资源开释。

通过此计划,FlutterViewController 退出之后内存的确呈现了降落,然而在灰度时发现偶然会有整个页面卡死的状况。通过对呈现问题的链路进行简略剖析以及配合暴力测试,咱们在 debug 环境对问题做了还原。最终初确认 UI 线程与 Raster 线程呈现死锁,死锁之后的线程状态大抵如下。

UI 线程状态:

Raster 线程:

定位剖析

一句话起因:

钉钉侧调用 FlutterEngine shutDownEngine 办法不合理导致。shutDownEngine 之前,必须先调用 FlutterView 的 shutdown 办法来进行渲染流程。待渲染流程失常进行之后,才可进入 FlutterEngine 资源开释流程,否则即有可能呈现上述死锁问题。

因为此问题为钉钉调用不合理导致,具体异样起因不再深入分析,感兴趣的同学能够根据上述线索自行查阅。

解决计划

在下层补全 FlutterEngine 开释流程,在调用 FlutterEngine shutDownEngine 之前首先调用 FlutterView shutdown 进行 Raster 线程。

1.3 低版本 macOS OpenGL 析构阶段 Crash 问题

问题背景

此问题还是接两个问题,在解决完问题 1 和问题 2 之后,参考 FlutterEngine shutdown 流程,钉钉会在 FlutterViewController 析构之后做 3 件事件:

  1. 将 FlutterRenderer 中绑定的 FlutterView 置为 nil;
  2. 调用 FlutterView shutdown 办法;
  3. 调用 FlutterEngine shutDownEngine 办法。

通过一系列解决之后,测试发现内存透露和死锁问题根本得以根治。然而在外部灰度过程中发现低版本 macOS 上会呈现 Crash,堆栈大抵如下:

定位剖析

一句话起因:

与问题 2 相似,此问题也是因为钉钉解决透露问题而引入。其大抵由两方面因素迭代导致。一方面因为重置 FlutterOpenGLRenderer 绑定的 FlutterView,导致在 embedder 层创立的 OpenGL 对象被提前开释;另外一方面因为低版本 macOS OpenGL 实现不欠缺析构流程中未能对要害链路做爱护,进而导致异样。

上面对异样相干代码做一下简答剖析,防止其他同学再遇到相似问题。

1、在 FlutterEngine setViewController 办法中,如果处于开释流程,会调用 FlutterOpenGLRenderer setFlutterView 办法,并传入 nil:

2、FlutterOpenGLRenderer setFlutterView 办法在入参为 nil 时,会开释其外部保护的 NSOpenGLContext 对象:

3、FlutterEngine 底层实现会在 GrDirectContext 对象析构时执行 flush,如果此时 OpenGL 相干对象曾经开释,在低版本 macOS(10.11, 10.12)会呈现 Crash:

解决计划

因为呈现问题的局部是由钉钉下层代码触发,解决绝对比较简单。最终咱们在所有应用 OpenGL 渲染的 Mac 设施上 (macOS 10.14 之前的版本) 移除 FlutterView 置空动作。即最终 FlutterViewController 开释阶段只执行以下两个动作:

  1. 调用 FlutterView shutdown 办法;
  2. 调用 FlutterEngine shutDownEngine 办法。

FlutterEngine Windows 端问题

2.1 Win7 设施渲染模块「Crash + 残影」问题

问题背景

此问题背景稍微有些简单,如果细分来看的话,此问题应该能够拆分为两个子问题。

第一个问题是,在局部 Win7 设施上(x86 + x64)呈现 d3d11 导致的 Crash,堆栈大抵如下:

因为迟迟无奈定位导致此问题的具体起因、且 Flutter 官网示意他们对 Win7 设施的覆盖度并不欠缺「参考」。因而咱们决定对 FlutterEngine 稍加定制,在 Win7 等古老设施上强制通过「软解模式」来渲染 Flutter 页面。

本认为通过此形式能够绕过此问题,但很不侥幸的是此计划裸露了 FlutterEngine 里另外一个 Bug:通过「软解模式」来渲染页面时,FlutterViewController 敞开只有有肯定概率会导致 Windows 桌面呈现残影。

定位剖析

一句话起因:

此问题次要是因为 FlutterEngine 外部 shutdown 流程中,未及时批改 FlutterWindowsEngine 指向 FlutterWindowsView 对象的指针,导致多线程场景下呈现野指针;因为野指针导致 raster 线程在 FlutterWindowsView 曾经销毁状况下仍向其输入绘制帧,进而导致异样。

在定位时,咱们通过减少辅助 log 的形式来放慢问题定位过程。通过对要害节点补充日志,咱们很快发现了可疑点:

上图是呈现问题之后要害节点输入的日志。咱们通过日志能够失去以下要害信息:

  1. OnBitmapSurfaceUpdated 是 FlutterWindowsView 的成员函数。然而在输入最初两行 OnBitmapSurfaceUpdated 办法时,FlutterWindowsView 的析构函数已被执行(野指针);
  2. 最初一次执行 OnBitmapSurfaceUpdated 时,渲染应用的 Window 句柄为 nullptr,即可供渲染的窗口(与 FlutterWindowsView 绑定)以被开释。

因为最初渲染所应用 Window 句柄为 nullptr,进而导致呈现残影问题。

补充阐明:在调用 C++ 成员函数时,即便调用时 this 曾经为野指针,但只有成员函数中并未拜访到 this 对象,则不会呈现内存拜访异样(Crash)。

解决计划

批改 FlutterEngine 外部实现,在 SoftwareRenderer 模式下 FlutterWindowsView 析构时,置空 FlutterWindowsEngine 指向其的指针(因 GPU 模式会有异样输入,暂未修改):

通过此形式,能够保障在 FlutterWindowsView 销毁之后 raster 线程中的工作不会再回调渲染接口:

2.2 FlutterPlugin 注册阶段野指针 Crash

问题背景

在钉钉 Flutter 版本「+ 面板」业务 Windows 端一灰、二灰阶段呈现较多例 Crash,客户端整体 Crash 率高达 x%:

通过简略剖析,还原 Crash 堆栈大抵如下:

从堆栈能够达到两个比拟重要的信息:

  1. Crash 呈现在 FlutterEngine 初始化阶段,具体是在 Plugin 注册时出现异常;
  2. 导致 Crash 起因是野指针问题。

定位剖析

一句话起因:

Flutter 为 Windows 平台提供 wrapper 层代码中,蕴含一个设计上为单例的对象 PluginRegistrarManager。PluginRegistrarManager 次要服务于 FlutterPlugin 注册、设计上为一个单例,其外部通过 map 维持了一个 FlutterEngine 指针与 Registrar 的映射关系,保障 Registrar 与 FlutterEngine 生命周期保持一致。然而因为 wrapper 层的代码在构建时被编入了 pulgin.dll,导致每一个 plugin.dll 中都蕴含一份 PluginRegistrarManager 实现正本,即「单例机制」生效。带来的问题是 FlutterEngine 析构时无奈正确革除 PluginRegistrarManager 中的绑定关系,导致其外部保护一个生效的指针地址,再次拜访时呈现 Crash。

上面简略介绍一下剖析过程。通过暴力测试,咱们能够复现问题:

依据上图能够确认,呈现 Crash 是因为 FlutterEngine 对象野指针导致。进一步定位插件注册时 Engine 指针起源,最终可定位到 flutter::PluginRegistrarManager::GetInstance()->GetRegistrar() 办法中:

进一步剖析 PluginRegistrarManager 中的实现,可知 GetRegistrar 外部须要 map + emplace 办法来维系 FlutterEngine 地址与 Registrar 关系:

其外部会通过 FlutterDesktopPluginRegistrarSetDestructionHandler 将办法注册到底层 Engine 对象中,其会在 FlutterEngine 析构时被调用,进而解除绑定关系:

问题即呈现在此流程中,如果 PluginRegistrarManager 并非真正的单例,且 FlutterEngine 只能保护一份无效的 OnRegistrarDestroyed 回调,那么在 FlutterEngine 析构时,有局部 PluginRegistrarManager 对象中保留的 FlutterEngine 地址不会被革除,再次应用时即会导致问题。

解决计划

批改 FlutterEngine wrapper 层 PluginRegistrarManager 实现,优化「单例」实现计划。将单例生命周期周期治理上层到底层,wrapper 层仅负责提供相干服务。

具体可参考:

2.3 Flutter Window 可见性变动之后页面白屏

问题背景

在 Windows 端 Flutter 页面中,如果将 Flutter Window:

  • 先通过 ShowWindow(flutter_wnd, SW_HIDE) 暗藏;
  • 再通过 ShowWindow(flutter_wnd, SW_SHOWNORMAL) 显示进去。

会发现 Flutter 页面内容无奈失常展现,画布上为空白一片。如果在白屏之后通过 setState 或者 拖拽窗口等形式触发 Flutter 页面刷新,则内容可被失常渲染。

定位剖析

此问题绝对比拟明确,Flutter Windows 端实现存在 bug,在 Window 可见性发生变化之后,应从新登程 flush 将最新视图绘制到对应窗口,然而目前此流程并未实现,导致呈现以上问题。

解决计划

此问题曾经提交 issue,临时钉钉侧是通过下层弥补的形式来绕过此此问题。咱们在 Native Window 可视性变动之后,手动告诉 Flutter 侧刷新以后可见页面,以此触发重绘、躲避问题。

总结

以上即为钉钉 Flutter 落地过程中桌面端解决的几大次要问题。从咱们理论体验来看,尽管在 Flutter v2.10 版本曾经正式公布对 Windows 的反对。但仅从稳定性角度来看,Flutter 在 Mac 端的体现无疑要优于 WIndows。如果有其它团队心愿在应用 Flutter 在桌面单端做一下尝试,咱们优先举荐抉择 Mac 端,其无论是上手门槛还是性能稳定性体现,相比 Windows 端要更有劣势。

关注【阿里巴巴挪动技术】,阿里前沿挪动干货 & 实际给你思考!

退出移动版