优化内存使用情况

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

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

  • 应用本身可能会变得缓慢或卡顿,最糟糕的情况下会被终止。
  • 用户可见的系统服务(音量控制、画面设置信息中心、语音助理等)变得非常卡顿,甚至可能无法正常运行。
  • 低内存终止守护进程 (LMK) 进程可能会通过终止最不必要的进程来应对内存压力较高这一问题;然后,这些组件可能会在不久后重新启动,从而触发进一步的资源争用高峰,这会直接影响前台应用。
  • 过渡到启动器可能会严重延迟,导致前台应用在过渡完成之前看起来无响应。
  • 系统可能会开始使用直接回收,在等待内存分配时暂时暂停线程的执行。这种情况可能会发生在任何线程(例如主线程或编解码器相关线程)中,可能会导致音频和视频丢帧以及界面故障。

电视设备上的内存注意事项

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

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

了解电视设备

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

在电视设备上,请考虑以下特征:

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

这有助于对不同类型的设备进行分类,并确定这些设备应如何使用内存。

电视设备摘要

设备内存 设备视频分辨率 设备界面分辨率 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) 界面分辨率、1080p/FullHD (1920x1080) 视频分辨率
  • 1.5 GB 设备:1.5 GB RAM、1080p/全高清 (1920x1080) 界面分辨率、2160p/超高清/4K (3840x2160) 视频分辨率
  • 由于存在额外的内存限制,OEM 定义了 ActivityManager.isLowRamDevice() 标志的其他情况。

普通电视设备

这些设备不会出现如此严重的内存压力情况。我们认为这些设备具有以下特征:

  • ≥1.5 GB RAM、720p 或 1080p 界面和 1080p 视频分辨率
  • ≥2 GB RAM、1080p 界面和 1080p 或 2160p 视频分辨率

这并不意味着应用不应关注这些设备上的内存用量,因为某些特定的内存滥用行为仍可能会耗尽可用内存,导致性能不佳。

低 RAM TV 设备上的内存目标值

在测量这些设备上的内存时,我们强烈建议使用 Android Studio 内存分析器监控内存的每个部分。电视应用应分析其内存用量,并努力将其类别控制在本部分定义的阈值以下。

内存分析器

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

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

根据这些定义,下表显示了每种类型的内存组应使用的最大值:

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

内存总量上限(匿名内存 + 交换 + 显存 + 文件)不得超过以下值:

  • 对于 1 GB 低 RAM 设备,总内存用量(匿名 + 交换空间 + 图形 + 文件)为 280 MB

强烈建议不要超过以下值:

  • 内存用量为 200 MB匿名内存 + 交换空间 + 图形内存)。

文件内存

作为文件支持的内存的一般性指导,请注意以下事项:

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

不过,在处理文件内存时,一般情况下:

  • 请勿在 build 中包含未使用的库,并尽可能使用库的小型子集,而不是完整的库。
  • 不要将大型文件一直打开并保存在内存中,而应在处理完后立即释放。
  • 如需最大限度地减小 Java 和 Kotlin 类的已编译代码大小,请参阅缩减、混淆处理和优化应用指南。

具体电视推荐

本部分针对优化电视设备上的内存用量提供了具体建议。

显存

使用合适的图片格式和分辨率。

  • 请勿加载分辨率高于设备界面分辨率的图片。 例如,在 720p 界面设备上,1080p 图片应缩小为 720p。
  • 尽可能使用由硬件支持的位图
    • 在 Glide 等库上,启用默认处于停用状态的 Downsampler.ALLOW_HARDWARE_CONFIG 功能。启用此功能可避免重复使用位图,否则位图会同时位于显存和匿名内存中。
  • 避免中间渲染和重新渲染
    • 您可以使用 Android GPU 检查器来识别这些问题:
    • 在“纹理”部分中,查找属于最终渲染步骤的图片,而不是仅包含构成最终渲染的元素,这通常称为“中间渲染”
    • 对于 Android SDK 应用,您通常可以使用布局标志 forceHasOverlappedRendering:false 停用此布局的中间渲染,从而移除这些内容。
    • 如需了解有关重叠渲染的更多信息,请参阅避免重叠渲染这篇出色的资源文章。
  • 尽可能避免加载占位图片,请使用 @android:color/@color 作为占位纹理。
  • 如果可以在离线状态下执行合成,请避免在设备上合成多张图片。优先加载独立图片,而不是从下载的图片进行图片合成
  • 请按照处理位图指南更好地处理位图。

匿名内存 + 交换内存

Anon+Swap 由 Android Studio 内存分析器中的原生内存分配、Java 内存分配和堆栈内存分配组成。使用 ActivityManager.isLowMemoryDevice() 检查设备是否受内存限制,并按照以下准则适应这种情况。

  • 媒体
    • 根据设备 RAM 和视频播放分辨率为媒体缓冲区指定可变大小。这应计为 1 分钟的视频播放时间:
      1. 40-60 MB(1 GB / 1080p)
      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(具体取决于设备内存)。
  • 分配
    • 使用显存指南确保您不会在匿名内存中复制图片
      • 图片通常是内存用量大户,因此重复使用图片可能会给设备带来很大压力。在大量浏览图片网格视图时尤其如此。
    • 在移动屏幕时,通过舍弃引用来释放分配:确保没有留下对位图和对象的引用。
    • 添加新库时,分析来自库的内存分配,因为它们也可能会加载其他库,而这些库也可能会进行分配并创建 Binding
  • 网络
    • 请勿在应用启动期间执行阻塞的网络调用。它们会延长应用启动时间,并在启动时产生额外的内存开销,而此时内存会因应用加载而受到特别的限制。先显示加载屏幕或启动画面,然后在界面就位后执行网络请求。

绑定

绑定会带来额外的内存开销,因为它们会将其他应用带入内存增加绑定应用的内存消耗(如果该应用已在内存中),以方便进行 API 调用。这会导致前台应用的可用内存减少。绑定服务时,请注意您使用绑定的时间和时长。请务必在不再需要绑定时立即释放绑定

典型绑定和最佳实践:

  • Play Integrity API:用于检查设备完整性
    • 在加载屏幕之后和媒体播放之前检查设备完整性
    • 在播放内容之前,释放对 PlayIntegrity StandardIntegrityManager 的引用。
  • Play 结算库:用于管理使用 Google Play 的订阅和购买交易
  • GMS FontsProvider
    • 在低 RAM 设备上,最好使用独立字体,而不是使用字体提供程序,因为下载字体成本高昂,并且 FontsProvider 会绑定服务来执行此操作。
  • Google 助理库:有时用于搜索和应用内搜索,如果可能,请替换此库。
    • 对于 Leanback 应用:使用 Gboard 文字转语音或 androidx.leanback 库。
      • 遵循搜索指南来实现搜索功能。
      • 注意:leanback 已弃用,应用应迁移到 TV Compose。
    • 对于 Compose 应用
      • 使用 Gboard 文字转语音功能实现语音搜索。
    • 实现接下来观看功能,以便用户发现您应用中的媒体内容。

前台服务

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

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

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

作业和闹钟

WorkManager 是用于安排后台周期性作业的最新 Android API。 WorkManager 将在可用时(SDK 23 及更高版本)使用新的 JobScheduler,否则使用旧的 AlarmManager。为了在电视上以最佳方式执行预定作业,请遵循以下建议:

  • 在 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 界面工具包中的 RecyclerView 中所示)来重复使用视图,而不是重新创建列表元素。
    • 缓存从外部内容提供程序读取的可能不会更改的元素,并定义更新间隔,以防止分配额外的外部内存。
  • 检查是否存在可能的内存泄漏。
    • 请注意典型的内存泄漏情况,例如匿名线程中的引用、从未释放的视频缓冲区重新分配以及其他类似情况。
    • 使用 堆转储来调试内存泄漏问题。
  • 生成基准配置文件,以最大限度地减少在冷启动时执行应用所需的即时编译量。

了解直接内存回收

当 Android TV 应用请求内存且系统压力过大时,作为 Android 基础的 Linux 内核可能必须使用直接内存回收

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

这可能会导致用户体验出现明显的暂停或卡顿,因为系统会暂停分配线程,直到有足够的内存可用为止。从这个意义上讲,分配线程并不限于 malloc() 等应用代码调用;例如,需要为换入代码页分配内存。

工具摘要