百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

前车之鉴:聊聊钉钉 Flutter 落地桌面端踩过的“坑”| Dutter

haoteby 2025-03-26 12:21 35 浏览

《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 退出之后内存泄漏问题

1问题背景

Mac 端 FlutterViewController 在销毁之后,其开辟的内存并未并实际释放,会出现内存泄漏问题。此问题在 Flutter issue 中有一些讨论,但一直未有明确定位。在钉钉 Mac 端 Flutter 业务灰度过程中也遇到此问题,如无法处理将直接影响 Dutter 在 Mac 端落地的可行性:

2定位分析

一句话原因:

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 方法并未被调用。

3处理方案

问题定位之后处理方式就很简单了,可以在 FlutterViewController dealloc 的时候手动触发 FlutterEngine shutDownEngine 方法。并且通过在上层通过 OC 动态特性 hook 实现、或者直接修改重新编译 FlutterEngine 都可以。

但此处修改一定要谨慎,注意完整还原 FlutterEngine 中的 shutdown 流程,否则可能导致我们遇到的第二个问题:死锁。

1.2 FlutterEngine shutdown 阶段死锁问题

1问题背景

钉钉最初在处理上述「FlutterEngine 泄漏」问题时,采用了一种相对比较简单的方案:在 FlutterViewController dealloc 方法中,手动调用 FlutterEngine 提供的 shutDownEngine 方法,手动触发相关资源释放。

通过此方案,FlutterViewController 退出之后内存确实出现了下降,但是在灰度时发现偶尔会有整个页面卡死的情况。通过对出现问题的链路进行简单分析以及配合暴力测试,我们在 debug 环境对问题做了还原。最终初确认 UI 线程与 Raster 线程出现死锁,死锁之后的线程状态大致如下。

UI 线程状态:

Raster 线程:

2定位分析

一句话原因:

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

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

3处理方案

在上层补全 FlutterEngine 释放流程,在调用 FlutterEngine shutDownEngine 之前首先调用 FlutterView shutdown 停止 Raster 线程。

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

1问题背景

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

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

经过一系列处理之后,测试发现内存泄漏和死锁问题基本得以根治。但是在内部灰度过程中发现低版本 macOS 上会出现 Crash,堆栈大致如下:

2定位分析

一句话原因:

与问题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:

3处理方案

由于出现问题的部分是由钉钉上层代码触发,处理相对比较简单。最终我们在所有使用 OpenGL 渲染的 Mac 设备上(macOS 10.14 之前的版本)移除 FlutterView 置空动作。即最终 FlutterViewController 释放阶段只执行以下两个动作:

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

FlutterEngine Windows 端问题

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

1问题背景

此问题背景略微有些复杂,如果细分来看的话,此问题应该可以拆分为两个子问题。

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

由于迟迟无法定位导致此问题的具体原因、且 Flutter 官方表示他们对 Win7 设备的覆盖度并不完善「参考」(https://github.com/flutter/flutter/issues/92650#issuecomment-961341821)。因此我们决定对 FlutterEngine 稍加定制,在 Win7 等陈旧设备上强制通过「软解模式」来渲染 Flutter 页面。

本以为通过此方式可以绕过此问题,但很不幸运的是此方案暴露了 FlutterEngine 里另外一个 Bug:通过「软解模式」来渲染页面时,FlutterViewController 关闭只有有一定概率会导致 Windows 桌面出现残影。

2定位分析

一句话原因:

此问题主要是因为 FlutterEngine 内部 shutdown 流程中,未及时修改 FlutterWindowsEngine 指向 FlutterWindowsView 对象的指针,导致多线程场景下出现野指针;因为野指针导致raster 线程在 FlutterWindowsView 已经销毁情况下仍向其输出绘制帧,进而导致异常。

在定位时,我们通过增加辅助 log 的方式来加快问题定位过程。通过对关键节点补充日志,我们很快发现了可疑点:

上图是出现问题之后关键节点输出的日志。我们通过日志可以得到以下关键信息:

  1. OnBitmapSurfaceUpdated 是 FlutterWindowsView 的成员函数。但是在输出最后两行 OnBitmapSurfaceUpdated 方法时,FlutterWindowsView 的析构函数已被执行(野指针);
  2. 最后一次执行 OnBitmapSurfaceUpdated 时,渲染使用的 Window 句柄为 nullptr,即可供渲染的窗口(与 FlutterWindowsView 绑定)以被释放。

因为最后渲染所使用 Window 句柄为 nullptr,进而导致出现残影问题。

补充说明:在调用 C++ 成员函数时,即使调用时 this 已经为野指针,但只要成员函数中并未访问到 this 对象,则不会出现内存访问异常(Crash)。

3处理方案

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

通过此方式,可以保证在 FlutterWindowsView 销毁之后 raster 线程中的任务不会再回调渲染接口:

2.2 FlutterPlugin 注册阶段野指针 Crash

1问题背景

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

通过简单分析,还原 Crash 堆栈大致如下:

从堆栈可以达到两个比较重要的信息:

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

2定位分析

一句话原因:

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 地址不会被清除,再次使用时即会导致问题。

3处理方案

修改 FlutterEngine wrapper 层 PluginRegistrarManager 实现,优化「单例」实现方案。将单例生命周期周期管理下层到底层,wrapper 层仅负责提供相关服务。

具体可参考:

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

1问题背景

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

  • 先通过 ShowWindow(flutter_wnd, SW_HIDE) 隐藏;
  • 再通过 ShowWindow(flutter_wnd, SW_SHOWNORMAL) 显示出来。

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

2定位分析

此问题相对比较明确,Flutter Windows 端实现存在 bug,在 Window 可见性发生变化之后,应重新出发 flush 将最新视图绘制到对应窗口,但是目前此流程并未实现,导致出现以上问题。

3处理方案

此问题已经提交issue,暂时钉钉侧是通过上层补偿的方式来绕过此此问题。我们在 Native Window 可视性变化之后,手动通知 Flutter 侧刷新当前可见页面,以此触发重绘、规避问题。

总结

以上即为钉钉 Flutter 落地过程中桌面端处理的几大主要问题。从我们实际体验来看,虽然在 Flutter v2.10 版本已经正式发布对 Windows 的支持。但仅从稳定性角度来看,Flutter 在 Mac 端的表现无疑要优于 WIndows。如果有其它团队希望在使用 Flutter 在桌面单端做一下尝试,我们优先推荐选择 Mac 端,其无论是上手门槛还是性能稳定性表现,相比 Windows 端要更有优势。

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

相关推荐

Python爬虫进阶教程(二):线程、协程

简介线程线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能...

A320-V2500发动机系统FADEC介绍(2)

目的全权数字发动机控制(FADEC)系统在所有飞行和运行阶段提供全范围发动机控制。...

三国志战棋版:玩家“二叔”用这套群DOT在比武中拿下31胜5负

声明:本文首发于今日头条,而后发布于“鼎叔闯三棋”的微信公众号、抖音、哔哩哔哩和小红书平台,如果在其他平台就是抄袭。...

真正的独一无二:Dot One 推出 DNA 定制系列 139英镑起

相信很多人在挑选衣物时有着这样的困扰,综合了性价比、面料等因素后好不容易找到了心仪的款式,还要担心是否会撞衫,不管是擦肩而过的陌生人还是身边的熟人,都令人尴尬。小部分人为此热衷于购买少量的古着或者限量...

崩铁:周年庆福利再升级,老角色加强时间确定,3.xdot体系反转

#埃安UT大一圈高级很多#...

Dotgo推出RBMHub,扩大了CPaaS提供商的覆盖范围和功能

据telecompaper网7月15日报道,用于商业消息传递的RichCommunicationServices(RCS)解决方案的领先提供商Dotgo宣布推出RBMHub。RBMHub的推出扩大了C...

深度解析:快照取消Dot职业的将何去何从

写在前面曾几何时,术士的出现便被冠以dot大师的名头,从远古时期的献祭腐蚀虹吸不如暗牧一个痛,到TBC上满dot=荣誉击杀+1,到wlk接近全暴击的冰晶腐蚀,再到CTM就算了吧MOP的各种变态吸x放...

星穹铁道:抽卡芙卡之前,你必须了解什么是dot!

卡妈终于上线了,可还是有很多人不明白什么是dot伤害,抽了卡妈直接玩起了直伤流,把一个持续伤害的引爆器玩成了打手,卡妈打dot伤害是远高于直伤的,有了卡妈的玩家一直了解dot,不然这卡妈就真被玩成四不...

游戏界的闪耀星辰陨落:悼念知名游戏博主″dotα牛娃″

无尽哀思!在数字时代浪潮中,游戏不仅是消遣娱乐的代名词,更是连接心灵的桥梁,构筑了无数人的青春回忆。在这片浩瀚无垠的游戏宇宙中,有这样一位博主,他以独特的风采、深邃的洞察力和无尽的热情,成为了玩家心中...

直击2017新加坡同性恋聚会Pink Dot,自由爱!

今年的“粉红点”又来啦~这个支持LGBT群体(男女同志、双性恋、跨性别等)群体的活动,从2009年起,已经在新加坡举办8年了!”这个非营利的同性恋权益活动,主要是希望大家了解到,不管一个人的性倾向或...

python-dotenv,一款超级实用处理环境变量python库

python-dotenv,一款超级实用处理环境变量python库python-dotenv概述:...

亚马逊语音助手毫无征兆发笑 诡异至极吓坏用户

来源:新华网美国电商亚马逊7日承诺,将更改名下“亚历克萨”语音系统设置,令它不会莫名发笑,免得吓坏用户。“亚历克萨”是亚马逊开发的语音助手软件,可服从用户语音指令完成对话、播放音乐等任务。依照原来设计...

2022最火英文网名男女生

精选好听英文昵称带翻译1.moveon(离开)2.Monster(怪物)3.Solo吉他手4.Finish.(散场)...

智能家具 RecycleDot 的出现给传统家具厂商带来新的挑战

从可穿戴手环、手表到智能衣服,智能硬件逐步渗透到每一个领域。最近有一对父子MikeSandru和JohnSandru在自家的车库中设计了一款智能家具RecycleDot,给日渐萧条的家具行...

欧洲通信卫星公司 OneWeb 敦促印度DoT尽早批准提供卫星宽带服务

据telecomtalk2月17日报道,欧洲通信卫星公司EutelsatOneWeb近日敦促印度电信部(DoT)尽快批准其在印度部署双地球站网关的计划,以便连接其近地轨道(LEO)全球卫星星座,并...