开发者控制台

优化Fire TV应用性能

Giovanni Laquidara Dec 05, 2024
Share:
Fire TV App performance Best practices
Blog_Header_Post_Img

下班之后坐下来看电视时,可能您最不想遇到的情况就是等待应用加载,注视着旋转的圆圈。客户也和您一样。您的客户不会去了解您的应用性能到底有多么好。客户只希望获得流畅的体验,不会遭遇让自己等待的缓冲圆圈,或者是遭遇延迟。为了提供流畅、响应迅速的流媒体体验,本文将探讨三个常见的Fire TV应用性能问题以及这些问题的解决方法。

启动速度缓慢

应用启动时间相当于您的用户与应用的第一次交互,为其整个体验设定了基调。如果启动缓慢或反应迟钝,可能会导致用户感到沮丧,甚至在接触您的内容之前就放弃使用应用。了解和优化应用的启动性能对于给用户留下深刻的第一印象和留住用户至关重要。

 

了解TTID和TTFD

要有效地衡量和提高启动性能,了解两个关键指标很重要:

  1. 初始显示用时 (TTID):此项是您的应用在启动后显示第一帧所需的时间。它代表用户在屏幕上看到内容的速度,由此表明应用的响应能力。
  2. 全面显示用时 (TTFD):此项衡量您的应用在加载所有关键用户界面元素和初始内容后,可全面与用户交互所需的时间。

 

检索TTID

要查找应用的TTID,请在Logcat中查找“Displayed”的值:

Retrieving TTID

衡量TTFD

要衡量TTFD,请使用reportFullyDrawn()方法:

Copied to clipboard
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            HelloWorldTVTheme {
                // Your app content
            }
        }
        // Call this when your app is fully loaded and interactive
        reportFullyDrawn()
    }
}
Measuring TTFD

 

使用Jetpack Compose衡量TTID和TTFD

如果您对电视应用使用Jetpack Compose,则可以使用特定的API来帮助您准确衡量上述指标。

  • ReportDrawn():此API可用来指示您的组合内容已经准备好,可立即进行交互。通常用此项来衡量TTID。

Copied to clipboard
@Composable
fun HomeScreen() {
    ReportDrawn()
    // Your UI content
}

  • ReportDrawnWhen():此API接受谓词,并在满足条件时报告绘制状态。当您的用户界面就绪程度取决于所加载的某些数据时,它很有用。

 

Copied to clipboard
@Composable
fun ContentList(items: List<Item>) {
    ReportDrawnWhen(items.size > 0)
    LazyColumn {
        items(items) { item ->
            ItemRow(item)
        }
    }
}

  • ReportDrawnAfter():此API采用一个挂起函数,并在函数完成运行后报告绘制状态。它对于可能需要耗费一些时间的操作特别有用,比如加载初始数据。

 

Copied to clipboard
@Composable
fun DataDependentScreen(viewModel: MyViewModel) {
    ReportDrawnAfter {
        viewModel.loadInitialData()
    }
    // UI that depends on the loaded data
}

要进一步探索这些和其他衡量方法,您还可以利用开源的FireOS性能测试工具,在连接到计算机的实体设备上分析您的应用,为您的优化过程生成全面的日志。

如何缩短TTID和TTFD

如果您发现TTID和TTFD数值过大(例如超出此处所述的值),您的应用的启动性能可能会受到以下常见问题之一的影响:

  1. 布局复杂度:可能膨胀得过大或深度嵌套的布局。
  2. I/O操作:在启动主活动之前或在应用程序类中,在主线程上执行磁盘或网络操作。
  3. 图像处理:在加载和解码大型图像的初始阶段效率低下。

视频播放管理效率低下

An animated GIF showing a spinning loading indicator, indicating that the video player is initializing

视频播放是所有流媒体应用的核心。性能问题和不佳的用户体验往往由播放器实例管理不当造成

以下代码示例存在一些常见的视频播放管理错误。来看看您是否能发现这些错误。

Copied to clipboard
class VideoPlayerActivity : ComponentActivity() {
    private lateinit var player: ExoPlayer

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        player = ExoPlayer.Builder(this).build()
        val mediaItem = MediaItem.fromUri("https://example.com/video.mp4")
        player.setMediaItem(mediaItem)
        player.prepare()

        setContent {
            VideoPlayerComposable(player)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        player.release()
    }
}

@Composable
fun VideoPlayerComposable(player: ExoPlayer) {
    AndroidView({ context ->
        PlayerView(context).apply {
            this.player = player
        }
    })
}

找到错误了吗?下面是当中存在的问题:

  1. 播放器的创建和准备是在onCreate中完成,这可能会造成其他用户界面初始化延迟。
  2. 没有对配置更改进行处理,这可能会导致播放器不必要地重新初始化。
  3. 当播放器在进行准备时,用户不能获得视觉反馈。
  4. 缺少错误处理,这可能会导致崩溃或用户界面无响应。

 

如何解决问题

Copied to clipboard
class VideoPlayerViewModel : ViewModel() {
    private val _playerState = MutableStateFlow<PlayerState>(PlayerState.Initializing)
    val playerState: StateFlow<PlayerState> = _playerState

    private var player: ExoPlayer? = null

    fun initializePlayer(context: Context) {
        // Ensure this is called on the main thread
        player = ExoPlayer.Builder(context).build().apply {
            val mediaItem = MediaItem.fromUri("https://example.com/video.mp4")
            setMediaItem(mediaItem)
            // Prepare asynchronously
            prepare()
            addListener(object : Player.Listener {
                override fun onPlaybackStateChanged(state: Int) {
                    when (state) {
                        Player.STATE_READY -> _playerState.value = PlayerState.Ready(this@apply)
                        Player.STATE_BUFFERING -> _playerState.value = PlayerState.Buffering
                        Player.STATE_ENDED -> _playerState.value = PlayerState.Ended
                        Player.STATE_IDLE -> _playerState.value = PlayerState.Idle
                    }
                }

                override fun onPlayerError(error: PlaybackException) {
                    _playerState.value = PlayerState.Error(error.message ?: "Unknown error")
                }
            })
        }
    }

    fun releasePlayer() {
        player?.release()
        player = null
        _playerState.value = PlayerState.Initializing
    }

    override fun onCleared() {
        releasePlayer()
        super.onCleared()
    }
}

sealed class PlayerState {
    object Initializing : PlayerState()
    object Buffering : PlayerState()
    class Ready(val player: ExoPlayer) : PlayerState()
    object Ended : PlayerState()
    object Idle : PlayerState()
    class Error(val message: String) : PlayerState()
}

@Composable
fun VideoPlayerScreen(viewModel: VideoPlayerViewModel = viewModel()) {
    val playerState by viewModel.playerState.collectAsState()
    val context = LocalContext.current

    LaunchedEffect(Unit) {
        viewModel.initializePlayer(context)
    }

    DisposableEffect(Unit) {
        onDispose {
            viewModel.releasePlayer()
        }
    }

    when (val state = playerState) {
        is PlayerState.Initializing, PlayerState.Buffering -> LoadingIndicator()
        is PlayerState.Ready -> VideoPlayer(state.player)
        is PlayerState.Error -> ErrorMessage(state.message)
        PlayerState.Ended -> PlaybackEndedUI()
        PlayerState.Idle -> IdleStateUI()
    }
}

@Composable
fun VideoPlayer(player: ExoPlayer) {
    AndroidView(
        factory = { context ->
            PlayerView(context).apply {
                this.player = player
            }
        },
        modifier = Modifier.fillMaxSize()
    )
}

我们使用ViewModel和Compose的LaunchedEffectDisposableEffect来处理视频播放器的正确初始化和清理。

 

Copied to clipboard
    LaunchedEffect(Unit) {
        viewModel.initializePlayer(context)
    }
    DisposableEffect(Unit) {
        onDispose {
            viewModel.releasePlayer()
        }
    }

 

此外,我们还添加了加载指示器和错误消息,为用户提供更好的反馈,并涵盖了播放错误,防止应用崩溃。

 

Copied to clipboard
 when (val state = playerState) {
        is PlayerState.Initializing, PlayerState.Buffering -> LoadingIndicator()
        is PlayerState.Ready -> VideoPlayer(state.player)
        is PlayerState.Error -> ErrorMessage(state.message)
        PlayerState.Ended -> PlaybackEndedUI()
        PlayerState.Idle -> IdleStateUI()
    }

未优化的图像加载

A screenshot of a mobile app UI displaying a gallery of movie poster thumbnails. The thumbnails are arranged in a grid layout, showcasing various movie titles and cover art

您的Fire TV应用可能会在同一屏幕上显示许多图像,如电影海报、节目缩略图、演员头像等。加载这些图像的方式会显著影响应用的性能。您可能会想使用如下所示的组件在屏幕上绘制上述图像组合内容之一:

Copied to clipboard
@Composable
fun UnoptimizedThumbnail(url: String) {
    AsyncImage(
        model = url,
        contentDescription = null,
        modifier = Modifier.size(200.dp)
    )
}

 

这种组件主要可能导致三种问题:

  1. 过度消耗内存:对缩略图加载全尺寸图像,导致内存浪费。
  2. 用户界面加载速度缓慢:大型图像的下载和处理时间较长,导致界面速度较慢。
  3. 滚动不顺畅:在内容列表中滚动时,效率低下的图像加载可能会导致滚动不畅。

出现这些问题是因为代码暗藏了几个性能缺陷:

  • 代码从URL加载全尺寸图像,而不考虑显示屏大小。
  • 在网络层面上没有进行尺寸调整,这意味着我们可能下载了不必要的数据。
  • 装置会消耗更多的CPU功率来最终调整图像的大小以供显示,这可能会导致用户界面出现卡顿。

 

解决办法

为了解决这些问题,您可以使用图像加载库(如Coil)。

Copied to clipboard
@Composable
fun OptimizedThumbnail(url: String) {
    AsyncImage(
        model = ImageRequest.Builder(LocalContext.current)
            .data(url)
            .size(width = 200, height = 200)
            .crossfade(true)
            .build(),
        contentDescription = null,
        contentScale = ContentScale.Crop,
        modifier = Modifier.size(200.dp)
    )
}

 

我们来分析一下改进途径。

  1. 尺寸规范:
    • .size(width = 200, height = 200) 告诉Coil将图像调整为200x200像素,最好是在服务器上或下载期间进行该调整。这样可以大大减少传输的数据量和用于存储图像的内存。
  2. 淡入淡出效果:
    • crossfade(true) 可在加载图像时添加平滑过渡效果。这样可提高用户感知的性能,让图像加载更加流畅。
  3. 内容缩放:
    • contentScale = ContentScale.Crop确保图像填充整个空间而不会失真。这为不同宽高比的图像提供了一致的外观。

总结

这些优化措施可以为高性能Fire TV应用奠定基础。通过实施这些技术,您可以富有成效地创建流媒体体验,在竞争激烈的电视应用领域脱颖而出,让您的用户满意。

然而,实现最佳性能的旅程并未就此结束。在未来的文章中,我们将探索更多优化方法!

敬请关注并不断优化!

相关文章

最新文章

 

查看有关亚马逊应用商店、应用开发与盈利、亚马逊服务以及更多主题的最新消息。