管理应用内存

随机存取存储器 (RAM) 在任何软件开发环境中都是一项宝贵资源;在移动操作系统中,物理内存通常有限,因此 RAM 更为宝贵。虽然 Android 运行时 (ART) 和 Dalvik 虚拟机都会定期执行垃圾回收任务,但这并不意味着您可以忽略应用分配和释放内存的位置和时间。您仍然需要避免引入内存泄漏问题(通常因在静态成员变量中保留对象引用而引起),并在适当时间(如生命周期回调所定义)释放所有 Reference 对象。

本页面介绍了如何积极减少应用的内存用量。如需了解 Android 操作系统如何管理内存,请参阅 Android 内存管理概览

监控可用内存和内存用量

您需要先找到应用中的内存使用问题,然后才能修复问题。Android Studio 中的内存分析器可以通过以下方式帮助您查找和诊断内存问题:

  1. 了解您的应用在一段时间内是如何分配内存的。内存分析器可以显示实时图表,说明应用的内存用量、分配的 Java 对象数量以及垃圾回收事件发生的时间。
  2. 发起垃圾回收事件,并在应用运行时拍摄 Java 堆的快照。
  3. 记录应用的内存分配情况,然后检查所有分配的对象、查看每项分配的堆栈轨迹,并在 Android Studio 编辑器中跳转到相应代码。

释放内存以响应事件

如 Android 内存管理概览中所述,Android 可以通过多种方式从应用中回收内存,或在必要时完全终止应用,从而释放内存以执行关键任务。为了进一步帮助平衡系统内存,避免系统需要终止您的应用进程,您可以在 Activity 类中实现 ComponentCallbacks2 接口。借助所提供的 onTrimMemory() 回调方法,您的应用可以在处于前台或后台时监听与内存相关的事件,然后释放对象以响应指示系统需要回收内存的应用生命周期事件或系统事件。

例如,您可以实现 onTrimMemory() 回调以响应与内存相关的不同事件,如下所示:

KotlinJava

import android.content.ComponentCallbacks2
// Other import statements ...

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

    // Other activity code ...

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that was raised.
     */
    override fun onTrimMemory(level: Int) {

        // Determine which lifecycle or system event was raised.
        when (level) {

            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                /*
                   Release any UI objects that currently hold memory.

                   The user interface has moved to the background.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                /*
                   Release any memory that your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system will
                   begin killing background processes.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
            ComponentCallbacks2.TRIM_MEMORY_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process will be one of
                   the first to be terminated.
                */
            }

            else -> {
                /*
                  Release any non-critical data structures.

                  The app received an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
            }
        }
    }
}

Android 4.0(API 级别 14)中添加了 onTrimMemory() 回调。对于早期版本,您可以使用 onLowMemory(),此回调大致相当于 TRIM_MEMORY_COMPLETE 事件。

查看您应该使用多少内存

为了允许多个进程同时运行,Android 针对为每个应用分配的堆大小设置了硬性限制。设备的确切堆大小限制因设备总体可用的 RAM 容量而异。如果您的应用已达到堆容量上限并尝试分配更多内存,系统就会抛出 OutOfMemoryError

为了避免用尽内存,您可以查询系统以确定当前设备上可用的堆空间大小。您可以通过调用 getMemoryInfo() 向系统查询此数值。它将返回一个 ActivityManager.MemoryInfo 对象,其中会提供与设备当前的内存状态有关的信息,包括可用内存、总内存和内存阈值(如果内存用量达到此数值,系统就会开始终止进程)。ActivityManager.MemoryInfo 对象还会公开一个简单的布尔值 lowMemory,您可以根据此值确定设备是否内存不足。

以下代码段示例演示了如何在应用中使用 getMemoryInfo() 方法。

KotlinJava

fun doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check to see whether the device is in a low memory state.
    if (!getAvailableMemory().lowMemory) {
        // Do memory intensive work ...
    }
}

// Get a MemoryInfo object for the device's current memory status.
private fun getAvailableMemory(): ActivityManager.MemoryInfo {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    return ActivityManager.MemoryInfo().also { memoryInfo ->
        activityManager.getMemoryInfo(memoryInfo)
    }
}

使用内存效率更高的代码结构

某些 Android 功能、Java 类和代码结构所使用的内存往往多于其他功能、类和结构。您可以在代码中选择效率更高的替代方案,以尽可能降低应用的内存用量。

谨慎使用服务

在不需要某项服务时让其保持运行状态,是 Android 应用最严重的内存管理错误之一。如果您的应用需要某项服务在后台执行工作,请不要让其保持运行状态,除非其需要运行作业。请记得在服务完成任务后使其停止运行。否则,您可能会在无意中导致内存泄漏。

在您启动某项服务后,系统更倾向于让此服务的进程始终保持运行状态。这种行为会导致服务进程占用大量内存,因为一旦服务使用了某部分 RAM,那么这部分 RAM 就不再可供其他进程使用。这会减少系统可以在 LRU 缓存中保留的缓存进程数量,从而降低应用切换效率。当内存紧张,并且系统无法维护足够的进程以托管当前运行的所有服务时,这甚至可能导致系统出现抖动。

您通常应该避免使用持久性服务,因为它们会对可用内存提出持续性的要求。我们建议您采用 JobScheduler 等替代实现方式。如需详细了解如何使用 JobScheduler 调度后台进程,请参阅后台优化

如果您必须使用某项服务,限制此服务的生命周期的最佳方式是使用 IntentService,它会在处理完启动它的 intent 后立即自行结束。如需了解详情,请参阅在后台服务中运行

使用经过优化的数据容器

编程语言所提供的部分类并未针对移动设备做出优化。例如,常规 HashMap 实现的内存效率可能十分低下,因为对于每个映射,都需要有一个单独的条目对象。

Android 框架包含几个经过优化的数据容器,包括 SparseArraySparseBooleanArray 和 LongSparseArray。例如,SparseArray 类的效率更高,因为在使用这些类时,系统不需要对键(有时还对值)进行自动装箱(这会为每个条目分别再创建 1-2 个对象)。

如果需要,您可以随时切换到原始数组以获得非常精简的数据结构。

谨慎对待代码抽象

开发者往往会将抽象简单地当做一种良好的编程做法,因为抽象可以提高代码灵活性,且更便于维护。不过,抽象的代价很高:通常它们需要更多的代码才能执行,需要更多的时间和 RAM 才能将代码映射到内存中。因此,如果抽象不能带来显著的好处,您就应该避免使用抽象。

针对序列化数据使用精简版 protobuf

协议缓冲区是 Google 设计的一种无关语言和平台且可扩展的机制,用于对结构化数据进行序列化。该机制与 XML 类似,但更小、更快也更简单。如果您决定针对数据使用 protobuf,应始终在客户端代码中使用精简版 protobuf。常规 protobuf 会生成极其冗长的代码,这会导致应用出现多种问题,例如 RAM 使用量增多、APK 大小显著增加以及执行速度变慢。

如需了解详情,请参阅 protobuf 自述文件中的“精简版”部分。

避免内存抖动

如前所述,垃圾回收事件不会影响应用的性能。但是,由于垃圾回收器和应用线程之间需要进行交互,如果在短时间内发生许多垃圾回收事件,就可能会快速消耗电量,而设置帧所用的时间也会略微增加。系统花在垃圾回收上的时间越多,耗电越快。

通常,“内存抖动”可能会导致出现大量的垃圾回收事件。实际上,内存抖动可以说明在给定时间内出现的已分配临时对象的数量。

例如,您可以在 for 循环中分配多个临时对象。或者,您也可以在视图的 onDraw() 函数中创建新的 Paint 或 Bitmap 对象。在这两种情况下,应用都会快速创建大量对象。这些操作可以快速消耗“新生代”区域中的所有可用内存,从而迫使垃圾回收事件发生。

当然,您必须先在代码中找到内存抖动问题较严重的位置,然后才能进行修复。为此,您应该使用 Android Studio 中的内存分析器

确定代码中的问题区域后,请尝试减少对性能至关重要的区域中的分配数量。您可以考虑将某些代码逻辑从内部循环中移出,或将其移到基于工厂的分配结构中。

您还可以评估对象池对用例是否有益。借助对象池,您可以在不再需要某个对象实例时将其释放到池中,而不是将其丢弃。下次需要此类对象实例时,您可以从对象池中获取,而不是分配。

全面的性能评估对于确定某个对象池是否适合指定场景至关重要。在某些情况下,对象池可能会导致性能下降。虽然对象池可以避免分配,但它们会产生其他开销。例如,维护对象池通常涉及到同步,这会产生较大的开销。此外,在释放期间清除对象池中的对象实例(以免内存泄漏),然后在获取实例期间对其进行初始化,这也可能会产生一定的开销。最后,在对象池中保留的对象实例数量超出预期也会给 GC 带来负担。尽管对象池可以减少 GC 调用次数,但最终会增加每次调用时所需完成的工作量,因为它与活跃(可访问的)字节数成比例。

移除会占用大量内存的资源和库

代码中的某些资源和库可能会在您不知情的情况下吞噬内存。应用的总体大小(包括第三方库或嵌入式资源)可能会影响应用的内存消耗量。您可以通过从代码中移除任何冗余、不必要或臃肿的组件、资源或库,降低应用的内存消耗量。

缩减总体 APK 大小

您可以通过缩减应用的总体大小来显著降低应用的内存用量。位图大小、资源、动画帧数和第三方库都会影响应用的大小。Android Studio 和 Android SDK 提供了可帮助您缩减资源和外部依赖项大小的多种工具。这些工具支持现代代码缩减方法,例如 R8 编译(Android Studio 3.3 及更低版本使用 ProGuard,而不是 R8 编译)。

如需详细了解如何缩减应用的总体大小,请参阅有关如何缩减应用大小的指南

使用 Dagger 2 实现依赖项注入

依赖项注入框架可以简化您编写的代码,并提供一个可供您进行测试及其他配置更改的自适应环境。

如果您打算在应用中使用依赖项注入框架,请考虑使用 Dagger 2。Dagger 不使用反射来扫描您应用的代码。Dagger 的静态编译时间实现意味着它可以在 Android 应用中使用,而不会带来不必要的运行时代价或内存用量。

其他使用反射的依赖项注入框架倾向于通过扫描代码中的注释来初始化进程。此过程可能需要更多的 CPU 周期和 RAM,并可能在应用启动时导致明显的延迟。

谨慎使用外部库

外部库代码通常不是针对移动环境编写的,在移动客户端上运行时可能效率低下。如果您决定使用外部库,可能需要针对移动设备优化该库。在决定是否使用该库之前,请提前规划,并在代码大小和 RAM 消耗量方面对库进行分析。

即使是一些针对移动设备进行了优化的库,也可能因实现方式不同而导致问题。例如,一个库可能使用的是精简版 protobuf,而另一个库使用的是 micro protobuf,这会导致您的应用出现两种不同的 protobuf 实现。日志记录、分析、图像加载框架和缓存以及许多您意料之外的其他功能的不同实现都可能导致这种情况。

虽然 ProGuard 可以使用适当的标志移除 API 和资源,但无法移除库的大型内部依赖项。您所需要的这些库中的功能可能需要较低级别的依赖项。如果存在以下情况,这就特别容易出现问题:您使用某个库中的 Activity 子类(往往会有大量的依赖项)、库使用反射(这很常见,意味着您需要花费大量的时间手动调整 ProGuard 以使其运行)等。

此外,请避免仅针对数十个功能中的一两个功能使用共享库。您一定不希望产生大量您甚至根本用不到的代码和开销。在考虑是否使用某个库时,请查找与您的需求十分契合的实现。否则,您可以决定自己去创建实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值