优化内存使用

内存优化对于确保流畅的性能、防止应用崩溃以及维护系统稳定性和平台健康至关重要。虽然每个应用都应该监控和优化内存使用情况,但 TV 设备上的内容应用面临着与典型手持设备 Android 应用不同的特定挑战。

高内存消耗可能导致应用和系统行为出现问题,包括:

  • 应用本身可能变得缓慢或卡顿,最坏情况下甚至被杀死。
  • 用户可见的系统服务(音量控制、图片设置面板、语音助手等)变得非常卡顿或根本无法工作。
  • 低内存杀手 (LMK) 守护进程可能会通过终止最不重要的进程来响应高内存压力;然后这些组件可能会在短时间内重新启动,从而引发进一步的资源争用高峰,这会直接影响前台应用。
  • 切换到启动器可能会显著延迟,并使前台应用在切换完成之前看起来没有响应。
  • 系统可能开始使用直接回收,在等待内存分配时暂时暂停线程执行。这可能发生在任何线程上,例如主线程或与编解码器相关的线程,从而可能导致音频和视频帧丢失以及 UI 故障。

TV 设备上的内存考量

TV 设备通常比手机或平板电脑的内存少得多。例如,TV 上常见配置为 1 GB RAM 和 1080p 视频分辨率。同时,大多数 TV 应用具有相似的功能;因此,实现方式和常见挑战也相似。这两种情况带来了其他设备类型和应用中未见的问题:

  • 媒体 TV 应用通常由网格图像视图全屏背景图像组成,这需要在短时间内将大量图像加载到内存中
  • TV 应用播放多媒体流,这需要分配一定量的内存来播放视频和音频,并且需要大量的媒体缓冲区以确保流畅播放。
  • 如果实现不当,额外的媒体功能(查找、剧集更改、音轨更改等)可能会增加额外的内存压力。

了解 TV 设备

本指南主要侧重于应用的内存使用情况和低 RAM 设备的内存目标。

在 TV 设备上,请考虑以下特性:

  • 设备内存:设备已安装的随机存取存储器 (RAM) 的大小。
  • 设备 UI 分辨率:设备用于渲染操作系统和应用 UI 的分辨率;这通常低于设备视频分辨率。
  • 视频分辨率:设备可以播放视频的最高分辨率。

这导致了不同设备类型的分类以及它们应如何使用内存。

TV 设备摘要

设备内存 设备视频分辨率 设备 UI 分辨率 isLowRAMDevice()
1 GB 1080p 720p
1.5 GB 2160p 1080p
≥1.5 GB 1080p 720p 或 1080p 否*
≥2 GB 2160p 1080p 否*

低 RAM TV 设备

这些设备处于内存受限情况,并将报告 ActivityManager.isLowRAMDevice() 为 true。在低 RAM TV 设备上运行的应用需要实施额外的内存控制措施

我们认为具有以下特征的设备属于此类别:

  • 1 GB 设备:1 GB RAM,720p/HD (1280x720) UI 分辨率,1080p/FullHD (1920x1080) 视频分辨率
  • 1.5 GB 设备:1.5 GB RAM,1080p/FullHD (1920x1080) UI 分辨率,2160p/UltraHD/4K (3840x2160) 视频分辨率
  • 原始设备制造商 (OEM) 由于额外的内存限制而定义 ActivityManager.isLowRAMDevice() 标志的其他情况。

常规 TV 设备

这些设备不会面临如此显著的内存压力情况。我们认为这些设备具有以下特征:

  • ≥1.5 GB RAM,720p 或 1080p UI 以及 1080p 视频分辨率
  • ≥2 GB RAM,1080p UI 以及 1080p 或 2160p 视频分辨率

这并不意味着应用不应该关心这些设备上的内存使用情况,因为一些特定的内存滥用仍然可能耗尽可用内存并导致性能不佳。

低 RAM TV 设备上的内存目标

测量这些设备上的内存时,我们强烈建议使用 Android Studio 内存分析器监控内存的每个部分。TV 应用应分析其内存使用情况,并努力使其类别低于本节中定义的阈值。

memory profiler

内存计数方式部分,您将找到所报告内存数据的详细说明。对于 TV 应用的阈值定义,我们将重点关注三个内存类别:

  • 匿名 + 交换:由 Android Studio 中的 Java + Native + Stack 分配内存组成。
  • 图形:直接在分析器工具中报告。通常由图形纹理组成。
  • 文件:在 Android Studio 中报告为“代码”+“其他”类别。

根据这些定义,下表指出了每种内存组应使用的最大值:

内存类型 用途 使用目标 (1 GB)
匿名 + 交换(Java + Native + Stack) 用于分配、媒体缓冲区、变量和其他内存密集型任务。 < 160 MB
图形 由 GPU 用于纹理和显示相关缓冲区 30-40 MB
文件 用于内存中的代码页和文件。 60-80 MB

最大总内存(匿名+交换 + 图形 + 文件)不得超过以下值:

  • 1 GB 低 RAM 设备的总内存使用量(匿名+交换 + 图形 + 文件不得超过 280 MB

强烈建议不要超过:

  • 匿名+交换 + 图形)的内存使用量为 200 MB

文件内存

作为文件支持内存的一般指导,请注意:

  • 通常,文件内存由操作系统内存管理妥善处理。
  • 我们目前尚未发现它是造成内存压力的主要原因。

但是,在处理一般文件内存时:

  • 不要在构建中包含未使用的库,并尽可能使用库的小子集而不是完整的库。
  • 不要将大文件保持打开状态在内存中,并在使用完后立即释放它们。
  • 最小化 Java 和 Kotlin 类的编译代码大小,请参阅缩减、混淆和优化您的应用指南。

TV 专用建议

本节提供了优化 TV 设备内存使用的具体建议。

图形内存

使用适当的图像格式和分辨率。

  • 不要加载分辨率高于设备 UI 分辨率的图像。例如,在 720p UI 设备上,1080p 图像应缩小到 720p。
  • 尽可能使用硬件支持的位图
    • 在 Glide 等库中,启用默认禁用的 Downsampler.ALLOW_HARDWARE_CONFIG 功能。启用此功能可避免位图重复,否则位图将同时存在于图形内存和匿名内存中。
  • 避免中间渲染和重复渲染
    • 这些可以使用 Android GPU Inspector 识别
    • 在“纹理”部分查找那些是最终渲染步骤而非仅构成其元素的图像,这通常被称为“中间渲染”
    • 对于 Android SDK 应用,您通常可以通过使用布局标志 forceHasOverlappedRendering:false 来禁用此布局的中间渲染,从而移除这些渲染。
    • 请参阅关于重叠渲染的 避免重叠渲染,这是一个很棒的资源。
  • 尽可能避免加载占位符图像,对于占位符纹理,请使用 @android:color/@color
  • 当可以在离线执行图像合成时,避免在设备上合成多个图像。优先加载独立图像,而不是从下载的图像进行图像合成。
  • 遵循处理位图指南,更好地处理位图。

匿名+交换内存

匿名+交换由 Android Studio 内存分析器中的 Native + Java + Stack 分配组成。使用 ActivityManager.isLowRAMDevice() 检查设备是否受内存限制,并根据这些指南适应这种情况。

  • 媒体
    • 根据设备 RAM 和视频播放分辨率为媒体缓冲区指定可变大小。这应考虑 1 分钟的视频播放:
      1. 1 GB / 1080p 为 40-60 MB
      2. 1.5 GB / 1080p 为 60-80 MB
      3. 1.5 GB / 2160p 为 80-100 MB
      4. 2 GB / 2160p 为 100-120 MB
    • 在更改剧集时释放媒体内存分配,以防止匿名内存总量增加。
    • 当您的应用停止时,立即释放并停止媒体资源:使用 activity 生命周期回调来处理音频和视频资源。如果您不是音频应用,请在活动上发生 onStop()停止播放,保存您正在执行的所有工作,并设置要释放的资源。要安排您稍后可能需要的工作,请参阅作业和警报部分。
    • 视频查找时注意缓冲区的内存:开发者通常在查找时分配额外的 15-60 秒的未来内容,以便视频为用户做好准备,但这会产生额外的内存开销。通常,在用户选择新的视频位置之前,不要占用超过 5 秒的未来缓冲区。如果您确实需要在查找时预缓冲额外时间,请确保:
      • 提前分配查找缓冲区并重复使用。
      • 缓冲区大小不应超过 15-25 MB(取决于设备内存)。
  • 分配
    • 使用图形内存指南,确保您不会在匿名内存中复制图像
      • 图像通常是最大的内存使用者,因此复制图像可能会给设备带来很大压力。在大量导航图像网格视图时尤其如此。
    • 通过在屏幕切换时丢弃对它们的引用来释放分配:确保没有位图和对象的引用遗留。
    • 添加新库时分析库的内存分配,因为它们也可能加载额外的库,这可能还会进行分配并创建绑定
  • 网络
    • 不要在应用启动期间执行阻塞网络调用,它们会减慢应用启动时间,并在启动时产生额外的内存开销,此时内存尤其受应用加载的限制。首先显示加载或启动画面,然后在 UI 就位后进行网络请求。

绑定

绑定会引入额外的内存开销,因为它们会将其他应用带入内存增加已绑定应用的内存消耗(如果它已在内存中)以促进 API 调用。因此,这会减少前台应用的可用内存。绑定服务时,请注意何时以及使用绑定多长时间。请务必在不需要时立即释放绑定

典型绑定和最佳实践

  • Play integrity API:用于检查设备完整性
    • 在加载画面之后和媒体播放之前检查设备完整性
    • 在播放内容之前,释放对 PlayIntegrity StandardIntegrityManager 的引用。
  • Play Billing Library:用于使用 Google Play 管理订阅和购买
  • GMS FontsProvider
    • 在低 RAM 设备上,优先使用独立字体而不是字体提供商,因为下载字体成本较高,并且 FontsProvider 会绑定服务来完成此操作。
  • Google Assistant 库:有时用于搜索和应用内搜索,如果可能,请替换此库。
    • 对于 Leanback 应用:使用 Gboard 文本转语音或 androidx.leanback 库。
      • 遵循搜索指南来实现搜索。
      • 注意:Leanback 已弃用,应用应迁移到 TV Compose。
    • 对于 Compose 应用:
      • 使用 Gboard 文本转语音来实现语音搜索。
    • 实现下一观看,使您的应用中的媒体内容可被发现。

前台服务

前台服务是一种特殊类型的服务,与通知相关联。此通知显示在手机和平板电脑的通知托盘中,但 TV 设备没有与这些设备相同的通知托盘。即使前台服务很有用,因为它们可以在应用程序在后台运行时保持运行,但 TV 应用必须遵循以下准则:

在 Android TV 和 Google TV 中,前台服务只允许在用户离开应用后继续运行。

  • 对于音频应用前台服务只允许在用户离开应用后继续运行以保持音频播放。音频播放结束后,服务必须立即停止。
  • 对于任何其他应用:一旦用户离开您的应用,所有前台服务都必须停止,因为没有通知可以告知用户应用仍在运行并消耗资源。
  • 对于更新推荐或下一观看后台作业,请使用 WorkManager

作业和警报

WorkManager 是用于调度后台循环作业的最先进的 Android API。WorkManager 将在可用时(SDK 23+)使用新的 JobScheduler,否则使用旧的 AlarmManager。为了在 TV 上执行计划作业时获得最佳实践,请遵循以下建议:

  • 在 SDK 23+ 上,避免使用 AlarmManager API,尤其是 AlarmManager.set()AlarmManager.setExact() 和类似方法,因为它们不允许系统决定运行作业的适当时间(例如,当设备处于空闲状态时)。
  • 在低 RAM 设备上,除非严格必要,否则避免运行作业。如果需要,WorkManager WorkRequest 仅用于在播放后更新推荐,并尝试在应用仍打开时进行。
  • 定义 WorkManager Constraints,让系统在适当的时候运行您的作业

Kotlin

Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .setRequiresStorageNotLow(true)
    .setRequiresDeviceIdle(true)
    .build()

Java

Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .setRequiresStorageNotLow(true)
    .setRequiresDeviceIdle(true)
    .build()
  • 如果您必须定期运行作业(例如,根据用户在另一个设备上应用中的内容观看活动来更新下一观看),请将内存使用量控制在作业内存消耗低于 30 MB

一般内存考量

以下指南提供了 Android 应用开发的一般信息:

  • 最小化对象分配,优化对象重用,并及时释放所有未使用的对象。
    • 不要持有对对象的引用,尤其是位图。
    • 避免使用 System.gc() 和直接释放内存调用,因为它们会干扰系统的内存处理过程:例如,在使用 zRAM 的设备中,强制调用 gc() 可能由于内存的压缩和解压缩而暂时增加内存使用量。
    • 使用 LazyList(如 Compose 中的目录浏览器中所示)或已弃用的 Leanback UI 工具包中的 RecyclerView 来重用视图,而不是重新创建列表元素。
    • 本地缓存从外部内容提供商读取且不太可能更改的元素,并定义更新间隔以防止分配额外的外部内存。
  • 检查可能的内存泄漏。
    • 注意典型的内存泄漏情况,例如匿名线程内部的引用、永不释放的视频缓冲区重新分配以及其他类似情况。
    • 使用堆转储来调试内存泄漏。
  • 生成基准配置文件,以在冷启动时执行应用时最大程度地减少即时编译的需求。

了解直接内存回收

当 Android TV 应用程序请求内存且系统承受压力时,作为 Android 基础的 Linux 内核可能需要使用直接内存回收

此过程涉及完全暂停任何分配线程,以等待释放的内存页。当后台回收无法主动维护足够的内存池时,就会发生这种情况。

这可能导致用户体验中出现明显的暂停或卡顿,因为系统会暂停分配线程,直到有足够的内存可用。在此意义上,分配线程不限于诸如 malloc() 之类的应用程序代码调用;例如,需要分配内存来分页代码页。

工具摘要