发布网友 发布时间: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