内存优化对于确保流畅的性能、防止应用崩溃以及维护系统稳定性和平台健康至关重要。虽然每个应用都应该监控和优化内存使用情况,但 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 应用应分析其内存使用情况,并努力使其类别低于本节中定义的阈值。
在内存计数方式部分,您将找到所报告内存数据的详细说明。对于 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
功能。启用此功能可避免位图重复,否则位图将同时存在于图形内存和匿名内存中。
- 在 Glide 等库中,启用默认禁用的
- 避免中间渲染和重复渲染
- 这些可以使用 Android GPU Inspector 识别
- 在“纹理”部分查找那些是最终渲染步骤而非仅构成其元素的图像,这通常被称为“中间渲染”。
- 对于 Android SDK 应用,您通常可以通过使用布局标志
forceHasOverlappedRendering:false
来禁用此布局的中间渲染,从而移除这些渲染。 - 请参阅关于重叠渲染的 避免重叠渲染,这是一个很棒的资源。
- 尽可能避免加载占位符图像,对于占位符纹理,请使用
@android:color/
或@color
。 - 当可以在离线执行图像合成时,避免在设备上合成多个图像。优先加载独立图像,而不是从下载的图像进行图像合成。
- 遵循处理位图指南,更好地处理位图。
匿名+交换内存
匿名+交换由 Android Studio 内存分析器中的 Native + Java + Stack 分配组成。使用 ActivityManager.isLowRAMDevice()
检查设备是否受内存限制,并根据这些指南适应这种情况。
- 媒体
- 根据设备 RAM 和视频播放分辨率,为媒体缓冲区指定可变大小。这应考虑 1 分钟的视频播放:
- 1 GB / 1080p 为 40-60 MB
- 1.5 GB / 1080p 为 60-80 MB
- 1.5 GB / 2160p 为 80-100 MB
- 2 GB / 2160p 为 100-120 MB
- 在更改剧集时释放媒体内存分配,以防止匿名内存总量增加。
- 当您的应用停止时,立即释放并停止媒体资源:使用 activity 生命周期回调来处理音频和视频资源。如果您不是音频应用,请在活动上发生
onStop()
时停止播放,保存您正在执行的所有工作,并设置要释放的资源。要安排您稍后可能需要的工作,请参阅作业和警报部分。- 您可以使用 生命周期感知组件,例如
LiveData
和LifecycleOwner
,以帮助您处理 Activity 生命周期调用。 - 为了使您的工作生命周期感知,您还可以使用 Kotlin 协程和 Kotlin 流。
- 您可以使用 生命周期感知组件,例如
- 视频查找时注意缓冲区的内存:开发者通常在查找时分配额外的 15-60 秒的未来内容,以便视频为用户做好准备,但这会产生额外的内存开销。通常,在用户选择新的视频位置之前,不要占用超过 5 秒的未来缓冲区。如果您确实需要在查找时预缓冲额外时间,请确保:
- 提前分配查找缓冲区并重复使用。
- 缓冲区大小不应超过 15-25 MB(取决于设备内存)。
- 根据设备 RAM 和视频播放分辨率,为媒体缓冲区指定可变大小。这应考虑 1 分钟的视频播放:
- 分配
- 使用图形内存指南,确保您不会在匿名内存中复制图像
- 图像通常是最大的内存使用者,因此复制图像可能会给设备带来很大压力。在大量导航图像网格视图时尤其如此。
- 通过在屏幕切换时丢弃对它们的引用来释放分配:确保没有位图和对象的引用遗留。
- 使用图形内存指南,确保您不会在匿名内存中复制图像
- 库
- 添加新库时分析库的内存分配,因为它们也可能加载额外的库,这可能还会进行分配并创建绑定。
- 网络
- 不要在应用启动期间执行阻塞网络调用,它们会减慢应用启动时间,并在启动时产生额外的内存开销,此时内存尤其受应用加载的限制。首先显示加载或启动画面,然后在 UI 就位后进行网络请求。
绑定
绑定会引入额外的内存开销,因为它们会将其他应用带入内存或增加已绑定应用的内存消耗(如果它已在内存中)以促进 API 调用。因此,这会减少前台应用的可用内存。绑定服务时,请注意何时以及使用绑定多长时间。请务必在不需要时立即释放绑定。
典型绑定和最佳实践
- Play integrity API:用于检查设备完整性
- 在加载画面之后和媒体播放之前检查设备完整性
- 在播放内容之前,释放对 PlayIntegrity
StandardIntegrityManager
的引用。
- Play Billing Library:用于使用 Google Play 管理订阅和购买
- 在加载画面之后初始化库,并在播放任何媒体之前处理所有计费工作。
- 使用完库后,以及在播放视频或媒体之前,始终使用
BillingClient.endConnection()
。 - 使用
BillingClient.isReady()
和BillingClient.getConnectionState()
检查服务是否已断开连接,以防需要再次执行任何计费工作,然后在完成后再次执行BillingClient.endConnection()
。
- GMS FontsProvider
- 在低 RAM 设备上,优先使用独立字体而不是字体提供商,因为下载字体成本较高,并且 FontsProvider 会绑定服务来完成此操作。
- Google Assistant 库:有时用于搜索和应用内搜索,如果可能,请替换此库。
- 对于 Leanback 应用:使用 Gboard 文本转语音或 androidx.leanback 库。
- 遵循搜索指南来实现搜索。
- 注意:Leanback 已弃用,应用应迁移到 TV Compose。
- 对于 Compose 应用:
- 使用 Gboard 文本转语音来实现语音搜索。
- 实现下一观看,使您的应用中的媒体内容可被发现。
- 对于 Leanback 应用:使用 Gboard 文本转语音或 androidx.leanback 库。
前台服务
前台服务是一种特殊类型的服务,与通知相关联。此通知显示在手机和平板电脑的通知托盘中,但 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()
之类的应用程序代码调用;例如,需要分配内存来分页代码页。
工具摘要
- 使用Android Studio 内存分析器工具来检查您在使用过程中的内存消耗。
- 使用Android GPU Inspector来检查图形分配。