Facebook构建高性能Android视频组件实践之路

2022-05-08 07:12:53于茜茜
导读 大家好,我是本期栏目编辑小友,现在为大家讲解Facebook构建高性能Android视频组件实践之路问题。 事实上,很难在可滚动的容器中播放视频。

大家好,我是本期栏目编辑小友,现在为大家讲解Facebook构建高性能Android视频组件实践之路问题。

事实上,很难在可滚动的容器中播放视频。——对设备资源的压力可能导致视频丢帧,滚动效果会很差。另外,我们不希望用户等待视频加载,也不希望它在播放时遇到缓冲,所以视频播放器需要快速启动,运行流畅。纵观市场上的各种设备,这些问题在安卓上还是很有挑战性的。

我们最近完成了安卓平台的新闻提要技术迁移,这是由我们的开源UI渲染框架Litho支持的。Litho支持异步布局和细粒度回收,这不仅有助于优化新闻提要以更有效地呈现内容,还使我们的代码更加健壮和易于扩展。我们希望将这些改进带到视频功能上,从而改善脸书用户的播放体验,帮助设计新用例的工程师。

构建视频UI元素比我们已经制作的其他UI组件更具挑战性。视频组件使用了一些Litho其他组件不需要的高级功能,同时鼓励Litho创建一些新功能。新的Litho视频组件引入了一些改进,从新闻提要的滚动性能到更灵活的设计,可以轻松地在不同的视频功能中重用。本文描述了我们在安卓应用中重写视频播放器时面临的挑战,以及Litho如何帮助克服这些困难。

视频新闻

新闻提要支持多种类型的视频新闻。有些类型的行为和外观与其他类型不同。视频附件是一种常见的新闻类型,包括普通视频、脸书直播视频、360度视频、gif或其他类型的视频,将它们附加到一篇普通文章中。

其他类型的视频新闻可以播放生成的视频、赞助商信息或短动画。

试图支持所有这些变体将使代码难以维护和测试。我们用来提供视频附件的主要视频视图类称为VideoAttachmentView。对于一些新闻类型,我们必须扩展这个视图,以便重用和定制它来满足设计的需要。在某些情况下,我们不能在派生类中进行所有的更改,只能创建一个单独的视图类,这意味着我们需要将通用逻辑移动到帮助类中。这使得代码更加复杂。

新闻源的滚动性能也会受到影响。RecyclerView只回收来自同一类的类型的视图,这意味着像Spotoredvideoattachmentview这样的视图不能回收用于一般的视频附件新闻类型,尽管Spotoredvideoattachmentview继承自videoattachmentview。由于这种低效率,RecyclerView需要分配更多的视图来容纳不同的新闻类型,从而导致更多的内存占用。另外,在屏幕上显示视频视图之前,会触发赋值行为,增加了不能及时完成的可能性,甚至可能导致丢帧。即使在新布局中重用相同的VideoAttachmentView来优化其性能,我们也需要创建一个新的视图类型。因为我们添加了另一层,它也使视图层次更深。视图越深,渲染时间越长。

设计视频组件。

Litho用一个叫做“组件”的单位来定义UI。它支持两种主要类型的组件:MountSpec和LayoutSpec。MountSpec定义了一个UI构建块,例如文本或图像组件。LayoutSpec定义包含一个或多个组件的布局。在Litho中,使用组件结构来构造更复杂的组件是非常常见的。布局规划可以定义包含其他布局规划和安装规范组件的布局,安装规范可以显示特定视图或绘制视图。

视频组件是一个UI构建块,这意味着我们需要一个MountSpec来定义它。一种方法是创建一个MountSpec来呈现视频附件视图,但是我们还需要为其他扩展视图创建另一个MountSpec,例如SponsoredVideoAttachmentView。虽然这是可行的,但我们最终会遇到同样的可重用性和可维护性问题。另一种方法是创建一个只显示简单视频视图而不是视频附件视图的MountSpec,并将其添加到特定新闻类型的LayoutSpec中。下图说明了新组件之间的关系:

CoreVideoComponent是一个MountSpec,任何视频新闻都需要它,具有最简单的特性。

@MountSpec

公共类CoreVideoComponentSpec {

@OnCreateMountContent

静态简单视频视图onCreateMountContent(组件上下文上下文){ 0

返回新的SimpleVideoView(上下文);

}

@OnPrepare

静态void onPrepare(

组件上下文上下文,

@ Prop video params video params){ 0

prefetchVideo(视频参数);

}

@OnMount

静态空位计数(

ComponentContext context, SimpleVideoView videoView, @Prop VideoParams videoParams) { initVideoPlayback(videoView, videoParams); } @OnUnmount static void onUnmount( ComponentContext context, SimpleVideoView videoView, @Prop VideoParams videoParams) { cleanupVideoPlayback(videoView, videoParams); } ... }

CoreVideoComponent 是 AutoplayVideoComponent 的子类,该组件是一个用于在新闻提要中注册视频的 LayoutSpec。所有新闻提要中的视频都是在自动播放管理器上注册的,但并不是所有的视频都需要自动播放功能 (例如,全屏视频播放器中的视频)。

@LayoutSpec public class AutoplayVideoComponentSpec { @OnCreateLayout static Component onCreateLayout( ComponentContext c, @Prop VideoParams videoParams) { registerVideoForAutoplay(videoParams); return CoreVideoComponent.create(c) .videoParams(videoParams) .build(); } ... }

最后, 我们将自动播放组件作为子类添加到 VideoAttachmentComponent 中。这个组件将一个视频附件数据结构转换为一个通用的视频组件都能理解的属性。

public class VideoAttachmentComponentSpec { @OnCreateLayout static Component onCreateLayout( ComponentContext c, @Prop VideoAttachmentInfo videoAttachmentInfo) { final VideoParams videoParams = createVideoParams(videoAttachmentInfo); return AutoplayVideoComponent.create(c) .videoParams(videoParams) .build(); } ... }

与 VideoAttachmentView 相比,这个设计提供了更多的灵活性。这些组件中的任何一个都可以添加到另一个 LayoutSpec 中,创建一个更复杂的组件并扩展它的功能或 UI 设计。Litho 鼓励使用嵌套组件,以及组件组合,以构建更强大的功能。Litho 以最优的渲染性能优化了布局树,构建出了扁平的视图结构。

下面是一个创建视频附件组件的示例,该组件显示底部的水印:

@LayoutSpec public class WatermarkVideoAttachmentComponentSpec { @OnCreateLayout static Component onCreateLayout( ComponentContext c, @Prop VideoAttachmentInfo videoAttachmentInfo) { return Column.create(c) .flexShrink(0) .alignContent(YogaAlign.FLEX_START) .child( VideoAttachmentComponent.create(c) .videoAttachmentInfo(videoAttachmentInfo)) .child( Text.create(c) .text("Powered by Litho") .textColorRes(android.R.color.holo_green_light) .textSizeDip(25) .paddingDip(YogaEdge.LEFT, 5) .positionType(YogaPositionType.ABSOLUTE) .positionDip(YogaEdge.BOTTOM, 0) .positionDip(YogaEdge.START, 0)) .build(); } }

新组件通过将其添加为子组件来重新使用视频附件组件的所有代码和 UI。

性能改进

除了支持更加灵活的设计之外,Litho 还提供了一些属性和特性,帮助我们优化新闻提要中的视频播放和整个应用的整体性能。

资源回收利用

Android 内置的 RecyclerView 可以基于视图的类型将其保存在不同的缓存池中,这对于创建了很多不同类型视图的用户界面来说可能会是一个问题。

相比之下,Litho 的回收系统复用了更小的用户界面构建模块,比如文本或图片,而不是整个视图。通过使用一个核心视频组件,同样的视图可以被循环使用于所有的视频新闻类型。更有效的回收利用减少了对象的分配,进而提高了滚动性能。

预分配

新闻提要的第一个视频新闻不能循环使用预先存在的视频视图,因为之前没有视图。当两个视频新闻同时出现在屏幕上时也需要注意:一个视频视图可以从以前的新闻中回收,但是第二个视图需要新建。当 RecyclerView 需要分配一个新的视图对象,特别是像视频视图那样的复杂视图时,会带来丢帧的风险。我们希望优化这种情况,因此我们在 Litho 中创建了预分配功能。

通过向 MountSpec 注解中添加一些属性,我们可以让 Litho 提前创建一些实例。当滚动浏览新闻提要中的第一个视频新闻时,预分配的视频视图可以极大地提高滚动性能。

@MountSpec(poolSize = 3, canPreallocate = true) public class CoreVideoComponentSpec { ... } 生命周期

MountSpec 有一些实用且简单的生命周期回调方法。这些足以让我们将大部分视频播放逻辑封装在组件中。在 Litho 之前,这个逻辑会被分散到不同的类中,由一个单独的控制器触发。视频组件中的主要回调方法包括:

onPrepare- 开始预取视频。在视频组件出现之前,在后台线程上触发。

onMount- 初始化视频播放器。组件首次配置其视图属性时触发。

onUnmount- 清除视频播放器,为下一次使用做准备。当视频滚动走时被触发。

LayoutSpec 有一个主要的回调:onCreateLayout()。它的主要目的是构造 LayoutSpec 的布局,但是它也可以为其子组件准备资源。例如,封面照片 LayoutSpec 可以在上面创建一个带有视频和封面照片的布局,同时还可以触发封面照片的预抓取,所有这些都是在同一个回调方法中进行的。

MountSpec 还支持另一个实用的回调:shouldUpdate()。当 RecyclerView 的适配器被更新时,它可以重新绑定所有的子视图,并获得所有可见的组件并重新加载 (触发 onUnmount 和 onMount)。对于简单的组件,这没有明显的影响,但是重新配置一个视频播放器就会是一个比较繁重的操作。这个回调是在 Litho 重新加载组件之前调用的,如果你觉得它没有必要的话 (例如,加载相同的视频),我们可以选择跳过它。

@ShouldUpdate(onMount = true) static boolean shouldUpdate(@Prop Diff videoParamsDiff) { return videoParamsDiff.getNext().videoId != videoParamsDiff.getPrevious().videoId; } 可测试性

新组件的模块化有助于更轻松地测试视频播放逻辑。Litho 提供了一个测试框架,可以在单元测试中模拟组件的生命周期。通过将更多的视频播放逻辑封装在这些组件中,我们可以测试和验证我们以前无法使用的复杂场景。此外,通过使用组合而不是继承来扩展功能,会更安全、更容易维护。

结 论

通过使用传统的 Android 视图,Litho 帮助我们通过一次构建就可以提升视频实现的性能、效率、可扩展性和可维护性。该视频组件在整个 Facebook 的 Android 应用程序中提升了 20% 的滚动性能,同时也改善了我们的冷启动时间,使我们的内存崩溃率降低了 2.5%,这都是更有效的内存管理的结果。代码的改进不仅改善了 Facebook 的体验,还让工程师有了更多在 Facebook 应用中创建新的视频体验的余地。

Litho 允许我们在 Android 上编写高度优化的 UI。Litho 于 2017 年 4 月开源,此后一直在成长。在去年年底,我们启动了 Sections 项目,它是基于 Litho 构建的用于编写高度优化的列表界面的 API。转换到 Litho 和 Sections 后,通常会有大幅度的性能提升,比如滚动性能提升可能高达 42%。使用 Litho 很简单并且文档丰富。可以查看 Litho 网站和 GitHub 页面上的代码示例、教程和高级指南。

在可滚动的容器中播放视频其实是有一定难度的——对设备资源的压力可能会导致视频丢帧,这会使滚动效果变的很差。此外,我们不希望用户等待视频加载,我们也不希望它在播放中碰到缓冲,因此视频播放器需要快速启动并流畅运行。纵观市场上的各种设备,这些问题在 Android 上的挑战还是很大的。

我们最近完成了 Android 平台的新闻提要技术迁移,由我们的开源 UI 渲染框架 Litho 提供技术支持。Litho 支持异步布局和细粒度的回收利用,这不仅有助于优化新闻提要使其更有效地渲染内容,而且可以使我们的代码更健壮、更易于扩展。我们希望将这些改进带到视频功能上来,以改善 Facebook 用户的播放体验,同时也为设计新用例的工程师们提供帮助。

构建一个视频 UI 元素比我们所做的其他 UI 组件更具挑战性。视频组件使用了 Litho 的一些其他组件不需要的高级特性,同时也促使 Litho 创建了一些新的特性。新的 Litho 视频组件引入了一些改进,从新闻提要的滚动性能到更灵活的设计,这些设计可以很容易地在不同的视频功能中重用。这篇文章描述了我们在 Android 应用程序中重写视频播放器时所面临的挑战,以及 Litho 如何帮助克服这些困难的。

视频新闻

新闻提要支持多种视频新闻类型。有些类型的行为和外观与其他的有所不同。视频附件是一种常见的新闻类型,其中有常规视频、Facebook Live 视频、360 度视频、gif 或其他类型的视频,它们会附在一个普通的文章上。

其他的视频新闻类型可以播放生成的视频,赞助商的信息,或者短动画。

想要支持所有这些变种会导致代码难以维护和测试。我们提供视频附件使用的主视频视图类叫 VideoAttachmentView。对于一些新闻类型,我们必须扩展这个视图,以便重用和定制它来适应设计的需要。在某些情况下,我们不能在派生类中进行所有的更改,只能创建一个单独的视图类,这意味着需要将通用逻辑移到帮助类中。这进一步使代码复杂化了。

新闻提要的滚动性能也会受到影响。RecyclerView 仅从相同类的类型中回收视图,这意味着不能为常规视频附件新闻类型回收像 SponsoredVideoAttachmentView 这样的视图,尽管 SponsoredVideoAttachmentView 继承至 VideoAttachmentView。由于这种低效率的原因,RecyclerView 需要分配更多的视图来容纳不同的新闻类型,从而导致了更多的内存占用。另外,在视频视图展现在屏幕上之前,分配行为就要被触发,这增加了它无法及时完成的可能性,甚至可能会导致丢帧。即使在一个新的布局中重用相同的 VideoAttachmentView 来优化它的表现,我们也需要创建一个新的视图类型。因为我们另外添加了一层,这也使得视图层次结构变的更深了。视图层次越深,渲染的时间就越长。

设计视频组件

Litho 用一种叫做“组件”的单位来定义 UI。它支持两种主要类型的组件:MountSpec 和 LayoutSpec。MountSpec 定义了一个 UI 构建块,例如一个文本或图像组件。LayoutSpec 定义一个包含一个或多个组件的布局。在 Litho 中,使用组件结构来构造更复杂的组件是很常见的做法。LayoutSpec 可以定义一个包含其他 LayoutSpec 和 MountSpec 组件的布局,MountSpec 可以呈现一个特定的视图或绘制视图。

视频组件是一个 UI 构建块,这意味着我们需要一个 MountSpec 来定义它。一种方法是创建一个 MountSpec 来呈现 VideoAttachmentView,但我们还需要为其它扩展视图创建另一个 MountSpec, 如 SponsoredVideoAttachmentView。虽然这做也是可行的,但最终我们还会遇到相同的重用性和可维护性的问题。另一种方法是创建一个 MountSpec 只呈现简单的视频视图而不是 VideoAttachmentView,并将其添加到一个特定的新闻类型的 LayoutSpec 中。下图演示了新组件之间的关系:

CoreVideoComponent 是一个有着最简特性的任何视频新闻都需要的 MountSpec。

@MountSpec public class CoreVideoComponentSpec { @OnCreateMountContent static SimpleVideoView onCreateMountContent(ComponentContext context) { return new SimpleVideoView(context); } @OnPrepare static void onPrepare( ComponentContext context, @Prop VideoParams videoParams) { prefetchVideo(videoParams); } @OnMount static void onMount( ComponentContext context, SimpleVideoView videoView, @Prop VideoParams videoParams) { initVideoPlayback(videoView, videoParams); } @OnUnmount static void onUnmount( ComponentContext context, SimpleVideoView videoView, @Prop VideoParams videoParams) { cleanupVideoPlayback(videoView, videoParams); } ... }

CoreVideoComponent 是 AutoplayVideoComponent 的子类,该组件是一个用于在新闻提要中注册视频的 LayoutSpec。所有新闻提要中的视频都是在自动播放管理器上注册的,但并不是所有的视频都需要自动播放功能 (例如,全屏视频播放器中的视频)。

@LayoutSpec public class AutoplayVideoComponentSpec { @OnCreateLayout static Component onCreateLayout( ComponentContext c, @Prop VideoParams videoParams) { registerVideoForAutoplay(videoParams); return CoreVideoComponent.create(c) .videoParams(videoParams) .build(); } ... }

最后, 我们将自动播放组件作为子类添加到 VideoAttachmentComponent 中。这个组件将一个视频附件数据结构转换为一个通用的视频组件都能理解的属性。

public class VideoAttachmentComponentSpec { @OnCreateLayout static Component onCreateLayout( ComponentContext c, @Prop VideoAttachmentInfo videoAttachmentInfo) { final VideoParams videoParams = createVideoParams(videoAttachmentInfo); return AutoplayVideoComponent.create(c) .videoParams(videoParams) .build(); } ... }

与 VideoAttachmentView 相比,这个设计提供了更多的灵活性。这些组件中的任何一个都可以添加到另一个 LayoutSpec 中,创建一个更复杂的组件并扩展它的功能或 UI 设计。Litho 鼓励使用嵌套组件,以及组件组合,以构建更强大的功能。Litho 以最优的渲染性能优化了布局树,构建出了扁平的视图结构。

下面是一个创建视频附件组件的示例,该组件显示底部的水印:

@LayoutSpec public class WatermarkVideoAttachmentComponentSpec { @OnCreateLayout static Component onCreateLayout( ComponentContext c, @Prop VideoAttachmentInfo videoAttachmentInfo) { return Column.create(c) .flexShrink(0) .alignContent(YogaAlign.FLEX_START) .child( VideoAttachmentComponent.create(c) .videoAttachmentInfo(videoAttachmentInfo)) .child( Text.create(c) .text("Powered by Litho") .textColorRes(android.R.color.holo_green_light) .textSizeDip(25) .paddingDip(YogaEdge.LEFT, 5) .positionType(YogaPositionType.ABSOLUTE) .positionDip(YogaEdge.BOTTOM, 0) .positionDip(YogaEdge.START, 0)) .build(); } }

新组件通过将其添加为子组件来重新使用视频附件组件的所有代码和 UI。

性能改进

除了支持更加灵活的设计之外,Litho 还提供了一些属性和特性,帮助我们优化新闻提要中的视频播放和整个应用的整体性能。

资源回收利用

Android 内置的 RecyclerView 可以基于视图的类型将其保存在不同的缓存池中,这对于创建了很多不同类型视图的用户界面来说可能会是一个问题。

相比之下,Litho 的回收系统复用了更小的用户界面构建模块,比如文本或图片,而不是整个视图。通过使用一个核心视频组件,同样的视图可以被循环使用于所有的视频新闻类型。更有效的回收利用减少了对象的分配,进而提高了滚动性能。

预分配

新闻提要的第一个视频新闻不能循环使用预先存在的视频视图,因为之前没有视图。当两个视频新闻同时出现在屏幕上时也需要注意:一个视频视图可以从以前的新闻中回收,但是第二个视图需要新建。当 RecyclerView 需要分配一个新的视图对象,特别是像视频视图那样的复杂视图时,会带来丢帧的风险。我们希望优化这种情况,因此我们在 Litho 中创建了预分配功能。

通过向 MountSpec 注解中添加一些属性,我们可以让 Litho 提前创建一些实例。当滚动浏览新闻提要中的第一个视频新闻时,预分配的视频视图可以极大地提高滚动性能。

@MountSpec(poolSize = 3, canPreallocate = true) public class CoreVideoComponentSpec { ... } 生命周期

MountSpec 有一些实用且简单的生命周期回调方法。这些足以让我们将大部分视频播放逻辑封装在组件中。在 Litho 之前,这个逻辑会被分散到不同的类中,由一个单独的控制器触发。视频组件中的主要回调方法包括:

onPrepare- 开始预取视频。在视频组件出现之前,在后台线程上触发。

onMount- 初始化视频播放器。组件首次配置其视图属性时触发。

onUnmount- 清除视频播放器,为下一次使用做准备。当视频滚动走时被触发。

LayoutSpec 有一个主要的回调:onCreateLayout()。它的主要目的是构造 LayoutSpec 的布局,但是它也可以为其子组件准备资源。例如,封面照片 LayoutSpec 可以在上面创建一个带有视频和封面照片的布局,同时还可以触发封面照片的预抓取,所有这些都是在同一个回调方法中进行的。

MountSpec 还支持另一个实用的回调:shouldUpdate()。当 RecyclerView 的适配器被更新时,它可以重新绑定所有的子视图,并获得所有可见的组件并重新加载 (触发 onUnmount 和 onMount)。对于简单的组件,这没有明显的影响,但是重新配置一个视频播放器就会是一个比较繁重的操作。这个回调是在 Litho 重新加载组件之前调用的,如果你觉得它没有必要的话 (例如,加载相同的视频),我们可以选择跳过它。

@ShouldUpdate(onMount = true) static boolean shouldUpdate(@Prop Diff videoParamsDiff) { return videoParamsDiff.getNext().videoId != videoParamsDiff.getPrevious().videoId; } 可测试性

新组件的模块化有助于更轻松地测试视频播放逻辑。Litho 提供了一个测试框架,可以在单元测试中模拟组件的生命周期。通过将更多的视频播放逻辑封装在这些组件中,我们可以测试和验证我们以前无法使用的复杂场景。此外,通过使用组合而不是继承来扩展功能,会更安全、更容易维护。

结 论

通过使用传统的 Android 视图,Litho 帮助我们通过一次构建就可以提升视频实现的性能、效率、可扩展性和可维护性。该视频组件在整个 Facebook 的 Android 应用程序中提升了 20% 的滚动性能,同时也改善了我们的冷启动时间,使我们的内存崩溃率降低了 2.5%,这都是更有效的内存管理的结果。代码的改进不仅改善了 Facebook 的体验,还让工程师有了更多在 Facebook 应用中创建新的视频体验的余地。

Litho 允许我们在 Android 上编写高度优化的 UI。Litho 于 2017 年 4 月开源,此后一直在成长。在去年年底,我们启动了 Sections 项目,它是基于 Litho 构建的用于编写高度优化的列表界面的 API。转换到 Litho 和 Sections 后,通常会有大幅度的性能提升,比如滚动性能提升可能高达 42%。使用 Litho 很简单并且文档丰富。可以查看 Litho 网站和 GitHub 页面上的代码示例、教程和高级指南。

.dfma { position: relative; width: 1000px; margin: 0 auto; } .dfma a::after { position: absolute; left: 0; bottom: 0; width: 30px; line-height: 1.4; text-align: center; background-color: rgba(0, 0, 0, .5); color: #fff; font-size: 12px; content: "广告"; } .dfma img { display: block; }
免责声明:本文由用户上传,如有侵权请联系删除!