当前位置: 首页 > 科技观察

前车之鉴:说说钉钉Flutter在桌面端踩过的“坑”

时间:2023-03-13 11:45:58 科技观察

本文主要介绍钉钉Flutter业务在灰度化过程中在桌面端遇到并解决的几个FlutterEngine级别的bug。具体包括:Mac端:Windows端:下面分别给大家介绍一下。FlutterEngineMac端问题1.1FlutterEngine退出后内存泄漏1问题背景Mac端的FlutterViewController销毁后,分配的内存并没有真正释放,出现内存泄漏。这个问题在Flutterissue中有讨论过,但是一直没有明确定位。钉钉Mac端Flutter业务灰度过程中也遇到了这个问题。如果不能解决,将直接影响Dutter在Mac端落地的可行性:2定位分析一句话原因:FlutterEngine在Mac端实现中弱属性的使用不合理导致。FlutterViewController拥有一个强大的FlutterEngine,它拥有一个指向FlutterViewController的弱属性。FlutterViewController在dealloc过程中尝试释放FlutterEngine,但是此时FlutterEngine持有的weak属性已经无法正确访问(nil),导致释放过程无法正常执行,发生泄漏。下面结合具体实现为大家简单介绍一下。由于OC和C++对象生命周期管理问题的设计,FlutterEngine内部的对象持有关系略有特殊,如下图所示:FlutterViewController作为对外暴露的主类,负责创建和持有FlutterEngine和FlutterView;FlutterEngine会在关机时强抱住自己,释放自己;FlutterEngine会创建并持有FlutterRenderer,FlutterRenderer会强持有FlutterView;FlutterEngine间接强持FlutterView;FlutterEngine有一个指向FlutterViewController的弱引用指针。一般情况下,FlutterViewController退出后,会通过调用FlutterEngine的setViewController并传入nil来触发FlutterEngine关闭动作。参考实现如下:正常情况下,FlutterViewControllerdealloc后,应该触发369行代码运行,从而释放FlutterEngine资源。但实际运行情况并非如此。当代码运行到第359行时,尝试判断if(_viewController!=controller)时不成立。通过上面的代码,我们知道controller是一个从外部传入的对象,此时为nil;_viewController作为弱属性,在FlutterViewController进入dealloc过程后也变为nil。所以在这个过程下,我们希望的shutdownDownEngine方法并没有被调用。3解决方法问题定位后,解决方法就很简单了。您可以在FlutterViewControllerdealloc时手动触发FlutterEngine的shutDownEngine方法。并且可以通过上层的OC动态特性hook实现,也可以直接修改重新编译FlutterEngine。但是这里修改的时候一定要谨慎,注意FlutterEngine中的关闭过程要完全还原,否则可能会导致我们遇到的第二个问题:死锁。1.2FlutterEngineshutdown阶段死锁问题1问题背景钉钉最初在处理上述“FlutterEngineleak”问题时采用了一个比较简单的解决方案:在FlutterViewController的dealloc方法中,手动调用FlutterEngine提供的shutdownEngine方法,手动触发相关资源释放.通过这个方案,FlutterViewController退出后内存确实下降了,但是在灰度的时候偶尔会出现整个页面卡顿的情况。通过对问题环节的简单分析,配合暴力测试,我们在debug环境下修复了问题。最后初步确定是UI线程和Raster线程发生了死锁,死锁后的线程状态大致如下。UI线程状态:光栅线程:2定位分析一句话原因:钉钉端调用FlutterEngineshutDownEngine方法不合理。在shutDownEngine之前,必须调用FlutterView的shutdown方法停止渲染进程。渲染进程正常停止后才能进入FlutterEngine资源释放流程,否则可能会出现上述死锁问题。因为这个问题是钉钉调用不合理导致的,具体异常原因就不做进一步分析了。有兴趣的同学可以根据以上线索自行查看。3解决方案在上层补充FlutterEngine的release流程,先调用FlutterViewshutdown停止Raster线程,再调用FlutterEngineshutDownEngine。1.3解构阶段低版本macOSOpenGLCrash问题1问题背景这个问题还是有两个问题。解决问题1和2后,参考FlutterEngine关闭流程。FlutterViewController销毁后,钉钉会做三件事:将FlutterRenderer中绑定的FlutterView设置为nil;调用FlutterView的关闭方法;调用FlutterEngine的shutdownEngine方法。经过一系列的处理,测试发现内存泄漏和死锁问题基本得到解决。但是在内部灰度过程中,发现macOS低版本会出现Crash,堆栈大致如下:2定位分析一句话原因:和问题2类似,引入这个问题也是由于钉钉的泄露问题。大致是两个因素的迭代造成的。一方面,由于重置了FlutterOpenGLRenderer绑定的FlutterView,导致在embedder层创建的OpenGL对象提前释放;另一方面,由于macOSOpenGL低版本实现不完善,在销毁过程中关键链接没有得到保护,导致出现异常。下面对异常相关的代码做一个简答分析,避免其他同学遇到类似问题。在FlutterEngine的setViewController方法中,如果是在release过程中,会调用FlutterOpenGLRenderer的setFlutterView方法,传入nil:当FlutterOpenGLRenderer的setFlutterView方法为nil时,会释放内部维护的NSOpenGLContext对象:FlutterEngine的底层实现会是在GrDirectContext对象中销毁此时执行flush。如果此时OpenGL相关对象已经发布,Crash:3将出现在低版本的macOS(10.11、10.12)中。解决方案由于问题部分是由钉钉上层代码触发的,所以处理起来比较简单。最后,我们在所有使用OpenGL渲染的Mac设备(macOS10.14之前)上删除了FlutterView消隐操作。即FlutterViewController最终释放阶段只执行以下两个动作:调用FlutterView关闭方法;调用FlutterEngine的shutDownEngine方法。FlutterEngineWindowsIssues2.1Win7DeviceRenderingModule“Crash+Afterimage”Issue1问题背景这个问题的背景有点复杂。如果细分来看,这个问题应该分为两个子问题。第一个问题是d3d11引起的Crash出现在一些Win7设备(x86+x64)上。“参考”不完整(https://github.com/flutter/flutter/issues/92650#issuecomment-961341821)。因此,我们决定稍微定制一下FlutterEngine,强制使用“软解模式”在Win7等老设备上渲染Flutter页面。本以为可以通过这种方式绕过这个问题,但不幸的是,这个方案暴露了FlutterEngine的另一个bug:在“软解模式”渲染页面时,只有一定概率FlutterViewController的关闭会导致Windows桌面显示为禁用。电影。2定位分析一句话原因:该问题主要是因为FlutterEngine内部关闭过程中没有及时修改FlutterWindowsEngine指向FlutterWindowsView对象的指针,导致多线程场景下出现野指针;由于存在野指针,光栅线程在FlutterWindowsView被销毁后仍在运行。为它绘制框架,这又会导致异常。在定位的时候,我们通过增加辅助日志来加快问题定位的速度。通过给关键节点添加日志,我们很快发现了可疑点:上图是关键节点出现问题后输出的日志。通过日志我们可以得到以下关键信息:OnBitmapSurfaceUpdated是FlutterWindowsView的一个成员函数。但是在最后两行OnBitmapSurfaceUpdated方法输出的时候,FlutterWindowsView的析构函数已经被执行了(野指针);最后一次执行OnBitmapSurfaceUpdated时,用于渲染的Windowhandle为nullptr,即释放的渲染窗口(绑定FlutterWindowsView)。因为最终渲染使用的Windowhandle是nullptr,导致了残影问题。补充说明:在调用C++成员函数时,即使调用时this已经是野指针,只要在成员函数中不访问this对象,就不会出现内存访问异常(Crash)。3解决方案修改FlutterEngine的内部实现。SoftwareRenderer模式下FlutterWindowsView销毁时,清空FlutterWindowsEngine指向的指针(由于GPU模式输出异常,暂未修改):这样可以保证FlutterWindowsView销毁后光栅线程中的任务程序将不再回调渲染接口:2.2FlutterPlugin注册阶段野指针Crash1问题背景在钉钉Flutter版“+面板”业务Windows端,一灰二灰阶段崩溃的案例较多,并且客户端整体crash率高达x%:通过简单分析,Crash栈的恢复大致如下:从栈中可以得到两个重要的信息:Crash发生在FlutterEngine的初始化阶段,具体来说当插件注册过程中发生异常时;Crash的原因是野指针问题。2定位分析一句话原因:Flutter提供了Windows平台的wrapper层代码,其中包含一个设计为单例的对象PluginRegistrarManager。PluginRegistrarManager主要服务于FlutterPlugin的注册,设计为单例。它内部通过一个map维护了一个FlutterEngine指针和Registrar的映射关系,保证Registrar和FlutterEngine的生命周期是一致的。但是由于wrapper层的代码在构建时被编译成pulgin.dll,每个plugin.dll都包含了一份PluginRegistrarManager的实现,即“单例机制”失效。问题是FlutterEngine析构时无法正确清除PluginRegistrarManager中的绑定关系,导致内部维护的指针地址无效,再次访问时出现Crash。下面简单介绍一下分析过程。通过暴力测试,我们可以重现问题:根据上图,可以确认崩溃是由FlutterEngine对象的野指针引起的。进一步定位插件注册时Engine指针的来源,最终定位到flutter::PluginRegistrarManager::GetInstance()->GetRegistrar()方法中:进一步分析PluginRegistrarManager中的实现可知,map+GetRegistrar内部需要emplace方法来维护FlutterEngine地址和Registrar关系:在内部,该方法会通过FlutterDesktopPluginRegistrarSetDestructionHandler注册到底层Engine对象,当FlutterEngine被析构时调用,释放绑定关系:这个过程出现问题,如果PluginRegistrarManager不是真正的单例,FlutterEngine只能维护一个有效的OnRegistrarDestroyed回调,那么当FlutterEngine被销毁时,PluginRegistrarManager对象中保存的一些FlutterEngine地址不会被清除,再次使用时会出现问题.3解决方案修改FlutterEnginewrapper层PluginRegistrarManager实现,优化“单例”实现。单例生命周期管理下放到底层,wrapper层只负责提供相关服务。详见:2.3FlutterWindow可见性改变后页面空白1.问题背景在Windows端的Flutter页面。如果先通过ShowWindow(flutter_wnd,SW_HIDE)隐藏FlutterWindow;然后通过ShowWindow(flutter_wnd,SW_SHOWNORMAL)显示。你会发现Flutter页面的内容无法正常显示,画布一片空白。如果在白屏后通过setState或拖动窗口的方式触发Flutter页面刷新,则可以正常渲染内容。2定位分析这个问题比较明确。Windows端Flutter的实现存在一个bug。窗口的可见性发生变化后,应该再次启动flush,将最新的视图绘制到对应的窗口上。但目前该工艺并未实施,导致出现上述问题。3解决方案该问题已作为问题提交,钉钉方面通过上层补偿暂时绕过该问题。NativeWindow的可见性发生变化后,我们手动通知Flutter端刷新当前可见的页面来触发重绘,避免出现问题。综上所述,以上就是钉钉Flutter落地过程中在桌面端处理的主要问题。从我们的实际体验来看,虽然Flutterv2.10版本已经正式发布了对Windows的支持。但仅从稳定性来看,Flutter在Mac端的表现无疑优于WIndows。如果其他团队想尝试在桌面端使用Flutter,我们推荐先在Mac端使用。它在入门门槛和性能稳定性方面比Windows端更有优势。