问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

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

发布网友 发布时间:2024-09-17 01:30

我来回答

1个回答

热心网友 时间:2024-10-30 18:18

作者:刘太举(驽良)

《Dutter系列文章》将阐述钉钉基于Flutter构建的跨四端应用框架(代号Dutter)的技术实践与踩坑经验,共分为上、下两篇,上篇内容可点击Dutter|钉钉Flutter跨四端方案设计与技术实践,本文为下篇,感谢阅读。

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

Mac端:

FlutterEngine退出之后内存泄漏问题;

FlutterEngineshutdown阶段死锁问题;

低版本macOSOpenGL析构阶段Crash问题;

Windows端:

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

FlutterPlugin注册阶段野指针Crash;

FlutterWindow可见性变化之后页面白屏。

下面来为大家分别介绍一下。

FlutterEngineMac端问题1.1FlutterEngine退出之后内存泄漏问题问题背景

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

定位分析

一句话原因:

Mac端FlutterEngine实现中对weakproperty使用不合理导致。FlutterViewController强持有FlutterEngine,后者持有一个指向FlutterViewController的weakproperty。FlutterViewController在dealloc流程中尝试释放FlutterEngine,但是此时FlutterEngine中持有的weakproperty已经无法正确访问(nil),导致释放流程未能正常执行,出现泄漏。

下面结合具体实现来为大家做一个简单说明。

由于设计到OC和C++对象生命周期管理问题,FlutterEngine内部对象持有关系略微特殊一些,大致如下图所示:

FlutterViewController作为对外暴露的主要Class,负责创建并持有FlutterEngine以及FlutterView;

FluterEngine在初始化阶段会自己强持有自己,并在shutdown时自我Release;

FlutterEngine会创建并持有FlutterRenderer,FlutterRenderer会强持有FlutterView;

FlutterEngine间接强持有FlutterView;

FlutterEngine有一个指向FlutterViewController的弱引用指针。

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

即正常情况下,FlutterViewControllerdealloc之后应该触发369行代码运行,进而释放FlutterEngine资源。但是实际运行情况缺不是这样,在代码运行到359行时,尝试判断if(_viewController!=controller)时并未成立。通过上述代码我们知道,controller是外部传入的对象此时为nil;_viewController作为一个weakproptry,在FlutterViewController进入dealloc流程之后也变为nil。因而在此流程下,我们希望中的shutDownEngine方法并未被调用。

处理方案

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

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

1.2FlutterEngineshutdown阶段死锁问题问题背景

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

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

UI线程状态:

Raster线程:

定位分析

一句话原因:

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

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

处理方案

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

1.3低版本macOSOpenGL析构阶段Crash问题问题背景

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

将FlutterRenderer中绑定的FlutterView置为nil;

调用FlutterViewshutdown方法;

调用FlutterEngineshutDownEngine方法。

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

定位分析

一句话原因:

与问题2类似,此问题也是因为钉钉处理泄漏问题而引入。其大致由两方面因素迭代导致。一方面因为重置FlutterOpenGLRenderer绑定的FlutterView,导致在embedder层创建的OpenGL对象被提前释放;另外一方面因为低版本macOSOpenGL实现不完善析构流程中未能对关键链路做保护,进而导致异常。

下面对异常相关代码做一下简答分析,避免其他同学再遇到类似问题。

1、在FlutterEnginesetViewController方法中,如果处于释放流程,会调用FlutterOpenGLRenderersetFlutterView方法,并传入nil:

2、FlutterOpenGLRenderersetFlutterView方法在入参为nil时,会释放其内部维护的NSOpenGLContext对象:

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

处理方案

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

调用FlutterViewshutdown方法;

调用FlutterEngineshutDownEngine方法。

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

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

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

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

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

定位分析

一句话原因:

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

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

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

OnBitmapSurfaceUpdated是FlutterWindowsView的成员函数。但是在输出最后两行OnBitmapSurfaceUpdated方法时,FlutterWindowsView的析构函数已被执行(野指针);

最后一次执行OnBitmapSurfaceUpdated时,渲染使用的Window句柄为nullptr,即可供渲染的窗口(与FlutterWindowsView绑定)以被释放。

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

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

处理方案

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

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

2.2FlutterPlugin注册阶段野指针Crash

问题背景

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

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

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

Crash出现在FlutterEngine初始化阶段,具体是在Plugin注册时出现异常;

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

处理方案

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

具体可参考:

2.3FlutterWindow可见性变化之后页面白屏问题背景

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

先通过ShowWindow(flutter_wnd,SW_HIDE)隐藏;

再通过ShowWindow(flutter_wnd,SW_SHOWNORMAL)显示出来。

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

定位分析

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

处理方案

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

总结

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

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

原文:https://juejin.cn/post/7096737604795629599

声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
靓仔怎么在班里受欢迎啊? 男的叫哥,女的叫姐 2024年nba总决赛几号开始总决赛赛程时间表出炉了-今日头条 无限连接的打印机不能打印公务员准考证 全国流量什么意思 新笑傲江湖手游恒山不能复活吗 有没有复活技能解答 新笑傲江湖手游恒山派怎么样_新笑傲江湖手游恒山派技能详解 新笑傲江湖恒山菩提武学怎么玩_恒山菩提武学玩法介绍 c语言argc参数在哪里设置? 这个C语言程序为何运行不出 ...come to my window to sing and fly away. 怎么读 flutterstirl怎么读 怎么正确的取票呢 明清四大长篇小说 打印机指示灯都是什么颜色的? 酒在古代社会礼仪中是否不可或缺? 中国酒桌上的礼仪大全 喝白酒的时候有哪些礼仪啊(喝白酒礼仪常识) 喝白酒的时候有哪些礼仪啊(喝中国白酒礼仪) 喝白酒的时候有哪些礼仪啊 为什么手机正常别人打电话提示关机呢? 手机无法接打电话,显示关机是什么回事? 打电话时出现关机提示,是怎么回事啊? 别人给我打电话显示关机是什么情况! “春风知别苦,不遣柳条青”是什么意思_出处及原文翻译_学习力_百度... 对李白 <<劳劳亭>>"不遣"作分析 微信怎么拉黑别人,对方会知道吗? 如何在微信上将某人拉入黑名单? 住院没刷医保能报销么 鱼钩鱼钩颜色 上海没有医保结算单 玛卡真能补肾壮阳吗? 黑玛卡是一时的作用还是永久的 买了一只猫咪六个月了,花了不少银子,店家告诉我是布偶猫还有电子血统... 猫咪六个月相当于多大 猫咪六个月相当于人类几岁 有哪些特点 环保工程二级资质可承接工程范围有哪些 环保工程二级资质的承包范围是什么 坐或站的时间稍长,就会大腿外侧酸疼的厉害!胯骨疼!尾椎疼!是坐骨神经炎... 女生27岁,工作经常站立。最近感到腿疼,右腿很沉,很涨挺,酸胀酸胀的... 有几个月了。久坐,久站后左侧大腿根部臀部后侧感觉是骨头疼,无_百度知 ... ...为什么我坐久了站起来 大腿后侧一根筋像扯着我疼 然后脚发麻 是我的... 大腿后侧筋疼怎么回事 入驻抖音mcn能干嘛 入驻抖音mcn有什么好处优势 天干干支纪法 地支是怎么来的 天干与地支之间有什么关系 江门市江海区蓬生装饰工程有限公司怎么样? 江门市极致装饰工程有限公司怎么样? 江门市康和装饰有限公司怎么样? 怎么去除WORD中的白底?