下班之后坐下来看电视时,可能您最不想遇到的情况就是等待应用加载,注视着旋转的圆圈。客户也和您一样。您的客户不会去了解您的应用性能到底有多么好。客户只希望获得流畅的体验,不会遭遇让自己等待的缓冲圆圈,或者是遭遇延迟。为了提供流畅、响应迅速的流媒体体验,本文将探讨三个常见的Fire TV应用性能问题以及这些问题的解决方法。
应用启动时间相当于您的用户与应用的第一次交互,为其整个体验设定了基调。如果启动缓慢或反应迟钝,可能会导致用户感到沮丧,甚至在接触您的内容之前就放弃使用应用。了解和优化应用的启动性能对于给用户留下深刻的第一印象和留住用户至关重要。
了解TTID和TTFD
要有效地衡量和提高启动性能,了解两个关键指标很重要:
检索TTID
要查找应用的TTID,请在Logcat中查找“Displayed”的值:
衡量TTFD
要衡量TTFD,请使用reportFullyDrawn()
方法:
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()
}
}
使用Jetpack Compose衡量TTID和TTFD
如果您对电视应用使用Jetpack Compose,则可以使用特定的API来帮助您准确衡量上述指标。
@Composable
fun HomeScreen() {
ReportDrawn()
// Your UI content
}
@Composable
fun ContentList(items: List<Item>) {
ReportDrawnWhen(items.size > 0)
LazyColumn {
items(items) { item ->
ItemRow(item)
}
}
}
@Composable
fun DataDependentScreen(viewModel: MyViewModel) {
ReportDrawnAfter {
viewModel.loadInitialData()
}
// UI that depends on the loaded data
}
要进一步探索这些和其他衡量方法,您还可以利用开源的FireOS性能测试工具,在连接到计算机的实体设备上分析您的应用,为您的优化过程生成全面的日志。
如何缩短TTID和TTFD
如果您发现TTID和TTFD数值过大(例如超出此处所述的值),您的应用的启动性能可能会受到以下常见问题之一的影响:
视频播放是所有流媒体应用的核心。性能问题和不佳的用户体验往往由播放器实例管理不当造成
以下代码示例存在一些常见的视频播放管理错误。来看看您是否能发现这些错误。
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
}
})
}
找到错误了吗?下面是当中存在的问题:
onCreate
中完成,这可能会造成其他用户界面初始化延迟。
如何解决问题
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的LaunchedEffect
及DisposableEffect
来处理视频播放器的正确初始化和清理。
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()
}
您的Fire TV应用可能会在同一屏幕上显示许多图像,如电影海报、节目缩略图、演员头像等。加载这些图像的方式会显著影响应用的性能。您可能会想使用如下所示的组件在屏幕上绘制上述图像组合内容之一:
@Composable
fun UnoptimizedThumbnail(url: String) {
AsyncImage(
model = url,
contentDescription = null,
modifier = Modifier.size(200.dp)
)
}
这种组件主要可能导致三种问题:
出现这些问题是因为代码暗藏了几个性能缺陷:
解决办法
为了解决这些问题,您可以使用图像加载库(如Coil)。
@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)
)
}
我们来分析一下改进途径。
.size(width = 200, height = 200)
告诉Coil将图像调整为200x200像素,最好是在服务器上或下载期间进行该调整。这样可以大大减少传输的数据量和用于存储图像的内存。crossfade(true)
可在加载图像时添加平滑过渡效果。这样可提高用户感知的性能,让图像加载更加流畅。contentScale = ContentScale.Crop
确保图像填充整个空间而不会失真。这为不同宽高比的图像提供了一致的外观。总结
这些优化措施可以为高性能Fire TV应用奠定基础。通过实施这些技术,您可以富有成效地创建流媒体体验,在竞争激烈的电视应用领域脱颖而出,让您的用户满意。
然而,实现最佳性能的旅程并未就此结束。在未来的文章中,我们将探索更多优化方法!
敬请关注并不断优化!