栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Flutter系列(二):Flutter在开眼快创的工程演进实践

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Flutter系列(二):Flutter在开眼快创的工程演进实践

上篇文章分享了开眼快创在Flutter跨端实现上的一些实践,本篇文章继续介绍一下Flutter在音视频领域的实践和探索,以及在工程提效方面的一些工作。

音视频实践

开眼快创的业务场景有很多的音视频功能,包括视频播放、直播、模板视频、视频编辑等,后续可能还会增加拍摄功能。其中最复杂的是视频编辑模块,主要有以下几个特点:

  1. 功能复杂,包含导入、导出,剪辑:分割、删除、变速、复制、排序,音乐:原声、音乐、音效、录音,字幕,滤镜、特效,多轨道,撤销,草稿等等功能;

  2. 状态复杂,太多视频编辑的状态,和当前页面用户操作的中间状态等等;

  3. 技术复杂,涉及多个技术领域,需要跨多部门合作开发;

  4. 逻辑复杂,这里主要涉及到一个变速,变速的需求将时间对齐这件简单事情的复杂度提升了一个次元;

  5. UI复杂,这里主要复杂在时间轴部分,视频轴,音乐轴,音效轴等等;

  6. 性能要求高,频繁的数据通信和视频预览对性能影响较大,后续的优化其实主要也是针对这一点。

对于这块功能使用原生开发还是使用纯Flutter开发,团队内部有过一些讨论,最终选择了用纯Flutter进行开发。因为使用Flutter可以在功能复杂的提前下实现一套业务代码运行在双端,可以提升研发效率并保持双端复杂逻辑的统一。同时,跨端渲染也可以实现双端UI的一致性,在后期功能维护阶段,减轻业务维护成本。

在人力少且任务重的情况下,我们通过四个版本两个月的迭代,完成了视频编辑主流功能的实现并双端上线。对比原生开发,Flutter的业务开发效率基本是原生开发的两倍。

在使用Flutter开发视频编辑模块过程中,我们也碰到了几个问题:

  1. 如何实现视频播放

  2. 复杂UI和复杂状态如何管理

  3. 如何跨多端实现高效的数据通信

  4. 如何保证性能

接下来介绍下在针对这几个问题做的一些工作。

外接纹理

由于Flutter是跨端渲染,那么在音视频场景下该如何实现Flutter侧的视频播放?在视频播放、编辑预览等场景可以使用外接纹理的方案来实现,其基本流程有3个步骤(如下图):

  1. Flutter通知Native创建Texture,并生成TextureId,返回给Flutter。

  2. Flutter声明Texture Widget,通过TextureId将Native Texture绑定到到Texture Widget。

  3. Texture Widget对应TextureLayer,Native通过Texture将Native侧的纹理数据映射给Flutter侧,TextureLayer将具体绘制内容提交给Flutter Engine,最后交给Skia合成上屏。

Native最左边表示原始图像流的生成方式:Video(本地/网络视频流)、Camera(摄像头拍摄的视频流)、Software/Hardware Render(使用Skia/GL绘制的图像流)。Native Texture在Android平台上是Surface Texture,在iOS平台上对应FlutterTexture。Android中的Surface Texture是比较核心的渲染组件,用于提供输出到OpenGL ES纹理的Surface。

Surface Texture是典型的生产者-消费者模型,其中维护了一个Buffer Queue队列,Surface是它的生产者,GLConsumer是它的消费者。GLConsumer拿到了Surface的原始图像流,最终转化GL Texture纹理,Texture纹理可以提供给Surface View、Texture View等使用,也可以贡献给类型为GL_TEXTURE_EXTERNAL_OES的纹理使用。

Surface Texture内部主要是通过EGLImageKHR来实现的,通过EGLImageKHR将GraphicBuffer图像数据绑定到GL_TEXTURE_EXTERNAL_OES型纹理上。EGLImageKHR的设计目的就是为了共享2D纹理数据,在CPU和GPU对同一资源进行访问时,它可以做到无需拷贝数据共享,这实际上就是通过共享内存的方式,来实现共享纹理。

iOS中的Texture组件为Flutter Texture,其核心是实现了copyPixelBuffer()方法。FlutterEngine调用copyPixelBuffer()方法拿到纹理数据CVPixelBuffer,然后使用CVOpenGLESTextureCacheCreateTextureFromImage()从CVPixelBuffer对象构建OpenGL Texture纹理。这就相当于将CVPixelBuffer对象与Texture纹理做了绑定,在数据发生变化时,Texture会触发绘制。

CVPixelBuffer本身也是可以同时被CPU和GPU访问的,并且绑定CVPixelBuffer和Texture纹理的过程可以是双向的,所以iOS也可以实现通过共享内存的方式,从而达到共享纹理的目的。即将Native GL环境绑定到CVPixelBuffer内存上,而CVPixelBuffer本身又是和Flutter GL纹理绑定的,这就相当于Native GL和Flutter GL通过CVPixelBuffer内存做到了数据共享。

我们和平台组合作,在视频编辑缩略图显示优化过程中用到过类似的共享纹理方案ks_image来提升性能。

数据通信

对于复杂的UI和状态,我们使用Redux来进行管理。关于状态管理的方案选型,我们在上一篇文章中做了阐述,在接下来推送的文章中,会详细介绍视频编辑页中的Redux应用,这里对状态管理不做过多的说明。在数据通信这块,模板视频和视频编辑模块等较重的音视频模块会涉及大量的Flutter业务层和编辑SDK服务层之间的数据交互。

在较早的模板视频功能实现过程中,开眼参考了快影的实现方式,Flutter提供Action和原始数据,比如更换素材、调整画幅等,然后通过Channel通知Android/iOS实现具体的方法,组装VideoEditorProject最后调用编辑SDK。这种方案在复杂度不高的场景下使用没有太大问题,但在复杂度高的业务情况下就会暴露明显弊端,每个功能点(比如替换素材)都需要三端实现(Android、iOS、Flutter),开发成本很高,每次新加功能都需要原生深度参与开发。

由于视频编辑中的功能点多,业务逻辑复杂,如果在视频编辑中也按照这种方案来处理,那么开发成本会比原生开发更高,这是不可接受的。

所以,针对此问题,我们提出了更优化的通信方案,即所有逻辑操作都放在Flutter端实现,在Flutter端直接组装VideoEditorProject,通过Channel传递给编辑SDK,Native只做为通道,不做实际的业务开发和逻辑开发。如下图:

上图中,当导入素材如图片、视频、音频等,会生成TrackAsset、AudioAsset,这些Assets在Flutter端拼接成VideoProject,updateProject()方法通过Channel统一给Native刷新到编辑SDK中,所有的业务组装逻辑全部在Flutter端完成,Native只作为通道传输数据。

当然,新方案也面临许多问题,其中一个就是如何才能保证Model在传输过程中保持统一的问题。因为音视频编辑的很多数据都是二进制的,所以上文提到的ProtoBuf(PB)可以很好地解决这个问题。

首先,编辑SDK的数据类型都是PB定义的,那么我们只需要根据编辑SDK定义的PB文件,再加上业务需要定义的PB文件,生成三端代码,Flutter以二进制的形式进行发送请求,Native端根据二进制数据反序列化为Model定义,Native拿到VideoEditorProject直接刷新给编辑SDK。整个过程都在PB的基础上传输。更进一步的,我们可以通过Dart FFI省略掉Native中间的传输过程,由Flutter直接对接C++的编辑SDK,原本由Channel来传输的缩略图也切换到FFI实现,在下文中会详细介绍。

这样所有的UI全部可以在Flutter实现,除了预览和导出功能外,所有的业务逻辑也全部在Flutter实现。采用新方案后,可以做到UI统一、逻辑统一,对于写具体业务的同学而言,写一套Flutter逻辑和视图双端即可运行,相对于原生业务开发必须每端投入人力开发同样的功能,且原生开发需要功能对齐、逻辑对齐、UI对齐等沟通成本,所以基于Flutter业务开发投入人力可以比原生业务开发少一半人力,基本相当于客户端原生开发双倍的开发效率。在后期功能维护上,由于Flutter开发的UI、逻辑、功能等都是统一的,即修改Flutter代码即可在双端生效,所以投入的维护成本也远远小于原生开发。

优化实践

为了提升用户体验,开眼快创做了大量的性能优化工作,Flutter确实也存在一些性能方面的问题,例如页面跳转明显的卡顿问题,但是随着版本的持续迭代,性能也在稳步提升。特别是在Flutter v1.17版本,iOS A7处理器以及iOS 10及以上设备默认使用metal替代OpenGL进行渲染,整体APP性能肉眼可见的提升,在升级之后常见的问题基本得到解决。下面介绍一下开眼在Flutter上做的一些优化工作,包括FFI、缩略图获取优化以及帧率优化。

FFI

音视频的底层能力都是由C层的音视频SDK提供的,在Android和iOS平台上各自包装了一层Wrapper供业务方使用,在上面提到的音视频通信方案中,Flutter组装好了数据发送给原生端,原生端再转发给音视频SDK,有没有一种类似JNI的方式,可以绕过平台层,由Dart直接调用C层的音视频服务?

Flutter官方提供了dart:ffi[1]用于Dart调用C/C++的库代码,在Flutter Engine和Flutter framework的交互过程中,大量使用了FFI来进行通信。在Flutter 2之后,FFI发布了稳定版本,并且提供了一套类型绑定生成工具ffigen[2],可以自动生成Dart Wrapper。我们和音视频同学合作对流程做了改造,整体改造后如下图:

组装数据的过程不需要原生参与,由Flutter和C层的音视频服务通过FFI直接通信,省略掉了中间原生部分参与的过程。四端(音视频SDK、Flutter、Android、iOS)使用同一份PB定义,生成四端代码。

整个数据传输过程基于PB进行序列化和反序列化,同步的方法调用基本和本地调用是一样的。之前很多通过Channel写的await方法,在改造后也可以变为同步调用,对于Flutter业务开发来说开发体验更佳。改造后性能提升数据如下,性能提升较为明显:

缩略图获取优化

最初,我们在视频编辑模块获取了缩略图,随后和音视频同学合作引入验证FFI方案的可行性。由于Dart是单线程语言,异步调用需要在Isolate之间通信,在Flutter v1.17以前,FFI只提供了同步调用,异步调用接口没有暴露。在Flutter v1.17版本之后,官方放开了异步调用关键函数:

DART_EXPORT bool Dart_PostCObject(Dart_Port port_id, Dart_CObject* message);

DART_EXPORT Dart_Port Dart_NewNativePort(const char* name,
                                         Dart_NativeMessageHandler handler,
                                         bool handle_concurrently);

DART_EXPORT bool Dart_CloseNativePort(Dart_Port native_port_id);

C层异步获取完数据,将C层的数据通过Dart_PostCObject()发送给中间Service Isolate进行数据转发,由Service Isolate和主Isolate进行通信,主Isolate收到数据后发送给C层反解,最终调用Dart层注册的回调函数完成异步调用过程。

有了FFI的异步支持,从Flutter v1.17版本开始,我们尝试接入FFI方案,Android平台接入后性能数据也有所提升。首次加载缩略图速度提升2% ~ 16%,在涉及大量图片传输场景下数据提升明显,数据传输耗时占比较高,FFI替换Channel后传输耗时降低。在需要多次解码的场景下,大部分的耗时出现在解码阶段,而数据传输并不是瓶颈,所以数据提升不明显。整体来说该方案比较符合大量小图传输的使用场景。

对于iOS来说,VideoToolBox解码器解码的时间都比系统接口获取到缩略图要长,因为视频编辑的业务场景是素材的原缩略图,并不是经过添加了特效、字幕等等之后的缩略图,在这种纯净的场景下,iOS系统自身的缩略图获取性能极高,编辑SDK暂时无法超越,所以没有切换FFI方案。在缩略图这块我们也尝试了外接纹理的ks_image方案,该方案在涉及大图显示时提升较为明显。另外,我们还针对缩略图从业务角度进行了优化,性能数据收益不错。后续会有相关文章推送,敬请期待。

FFI和缩略图一起优化后,视频编辑页在iOS上视频预览状态下帧率从50提升到58左右,在快速滑动状态下,帧率从30提升到45左右,整体页面在线上平均帧率在50+,Android平台视频编辑页的线上平均帧率也在45+。

帧率优化

Flutter是单线程模型,整个渲染流程由渲染管线调度管理。

Graphics Pipeline:

UIThread负责生产flutter::Layer Tree,RasterThread负责消费flutter::Layer Tree。这种调度机制可以确保RasterThread不至于过载(2个任务),同时也可以避免UIThread不必要的资源消耗。

所以不论在UIThread还是在RasterThread耗时太久,都可能会导致Flutter应用卡顿,因为会导致延迟接受VSync信号,导致掉帧。

所以整体上业务代码优化的两个方向,一个是避免UIThread耗时太久,另一个是避免RasterThread耗时太久。其中,UIThread是重点的优化方向,是因为对于刚接触Flutter开发的同学来说,很容易触发频繁绘制和过度绘制,导致UIThread耗时过久。优化的主要手段也是避免过度绘制,控制刷新范围等,主要包括:

  • 避免在Build方法中调用耗时方法或视图无关的方法

  • 控制Redux的刷新范围和频率,拆分更细粒度的Model和UI之间的关系

  • 降低刷新频率,避免重复Rebuild

  • 控制刷新范围,下沉setState避免大范围Rebuild

  • 注意InheritedWidget的影响范围,避免不必要的Context上下文依赖

  • 重用Key和Type,在需要的场景复用Element

  • 使用const修饰不需要变更的Widget

  • 列表控件中控制cacheExtend数量,找到首次加载和缓存复用的平衡

  • 合理组织控件层次,减少不必要的嵌套层次

  • 提高组件内聚,尽可能少的与外部组件联动

RasterThread卡顿的问题不太常见,但是需要着重注意下面几个:

  • 尽量避免使用需要SaveLayer离屏渲染的API,如Opacity、ShaderMask、ColorFilter等;

  • 使用RepaintBoundary包裹需要频繁刷新的控件,创建单独的Layer渲染。

总体上对于业务来说,Flutter确实可以提高开发效率,但是对于新手或写代码不注意就会造成性能问题。一般的问题都可以通过优化业务代码来优化性能,但整体上业务代码能进行干预的其实并不多,当碰到一些framework或Engine的性能问题时,只能想办法绕过,相比原生开发,Flutter业务层对系统的掌控力度更小,但是如果想要更深层次的优化,就只能定制Flutter Engine。

早期Flutter版本更新速度较快,一些性能问题官方可以快速进行修复,甚至会在更底层进行性能优化。定制Flutter Engine和官方Engine之间的合并成本较高,如果不能快速升级,就享受不到官方Engine升级带来的提升。这也是目前我们没有选择定制Engine的主要原因。但是,目前来看Flutter版本更新频率放缓,整体趋于稳定,后续要不要使用定制Engine在深层次提升App性能,是我们需要考虑的事情。

研发支撑

我们在工程研发流程和工程提效方面也做了一些工作。Flutter整个研发流程依托于Keep平台,包括持续集成、应用发布、渠道管理、线上监控等。针对Flutter开发,我们和平台组同学合作,在研发阶段落地了Pub私服管理、fvm Flutter版本管理、内存泄露检测等功能;在线上落地了APM性能监控、Crash监控等功能。

除此之外,我们也自研了调试工具KDebugTools[3]KDebugTools[3]是一套适用于Flutter平台的移动端应用研发辅助工具,包括App端和Web端。除了在App内使用外,通过内置的Web服务,也可以在电脑浏览器实现以下功能:

  • App和设备信息查询

  • 设备文件管理、传输和预览

  • SharedPreferences、SQLite直接查询和修改

  • Flutter网络抓包拦截及限流配置

  • Flutter日志查看

  • Flutter Widget属性检查

  • Flutter路由跳转

  • 设备剪切板同步

  • 设备投屏及录制(Android)

整套功能全部使用Dart实现。App端使用Flutter App实现,Web端使用Flutter Web开发,App端使用Dart开发的后台服务,通过WebSocket和Web端建立连接。所有功能无需ROOT,无需USB连接(关于Web投屏实现有另外一篇文章详细介绍)。KDebugTools[3] 也已开源回馈社区,欢迎大家使用。

总结

本文主要介绍了Flutter在开眼快创的工程演化实践并开源了KDebugTools[3],希望可以对读者提供一些参考价值。文中讲解到了复杂性较高的音视频功能,从技术上来说Flutter可以胜任,开发效率上基本是原生开发的两倍,性能也达到了一般标准。

当然,目前的Flutter还存在一些问题,比如图片加载问题、列表卡顿问题、字体对齐问题等等,但我们认为Flutter已经完全可以胜任中小型新项目,新项目没有历史包袱,可以不用考虑内嵌Flutter,直接使用纯Flutter进行开发即可。Flutter的性能虽然不能达到原生一样流畅,很多大的App已经弃用Flutter。但在需要快速迭代的项目中,Flutter可以在保持一定性能的情况下帮助项目快速迭代。在Flutter 2发布之后,我们相信Flutter在跨平台领域会越走越好。

最后,感谢平台组和音视频同学的大力支持和帮助,感谢快影团队在Flutter音视频实践中奠定的基础。

相关链接

[1]https://flutter.dev/docs/development/platform-integration/c-interop

[2]https://pub.dev/packages/ffigen

[3]https://pub.dev/packages/k_debug_tools

加入我们

快手主站技术部客户端团队由业界资深的移动端技术专家组成,通过领先的移动技术深耕工程架构、研发工具、动态化、数据治理等多个垂直领域,积极探索创新技术,为亿万用户打造极致体验。团队自2011年成立以来全面赋能快手生态,已经建立起业内领先的大前端技术体系,支撑快手在国内外的亿万用户。

在这里你可以获得:

  • 提升架构设计能力和代码质量

  • 通过大数据解决用户痛点的能力 

  • 持续优化业务架构、挑战高效研发效能

  • 和行业大牛并肩作战

我们期待你的加入!请发简历到:

app-eng-hr@kuaishou.com

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/276516.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号