【Using English】61 Android管理应用内存

随机存取存储器(RAM)在任何软件开发环境中都是宝贵的资源,但是在物理内存常常受到限制的移动操作系统中变得更加珍贵。尽管Android运行时(ART)和Dalvik虚拟机都会例行地执行垃圾回收,但这不意味着应用可以不顾时间和位置地分配和释放内存。仍然要避免引入内存泄漏,通常的原因是static变量持有一个对象的引用。并且在恰当的时机(如定义好的生命周期回调方法)所有的释放所有Reference对象。

本文讲解释如何主动地减少应用使用的内存。关于Android系统如何管理内存的信息,请查看Android内存管理概述

监控可用内存 和 内存使用量

在修复应用内存使用量的问题之前,需要先找到它们。Android Studio 的Memory Profiler(内存分析器)可以通过以下方法帮助你发现且诊断内存问题:

  1. 查看应用在一段时间内如何分配内存。内存分析器会提供一个实时图像,图像里包括应用使用的内存,分配的java对象数,以及垃圾回收触发的时间。
  2. 发起一次垃圾回收事件,并且抓取一张应用运行时的Java堆的快照。
  3. 记录应用的内存分配然后检查所有分配的对象,查看每一次分配的调用栈,在Android Studio的编辑器中跳转到相关的代码中。

在响应事件中释放内存

正如Android内存管理概述一文所述,Android有很多种办法可以回收应用的内存,如果必要的话,也会终止整个应用进程来释放资源。为了进一步帮助平衡系统内存,避免系统需要终止应用进程的情况发生,应用可以在Activity中实现ComponentCallback2接口。该接口中提供了onTrimMemory()回调方法,这个方法允许应用监听内存相关的事件,不论应用是在前台还是后台,并且通过释放对象来响应表示系统需要回收内存的事件,这些事件包括应用生命周期的事件和系统事件。

例如下面的代码,应用可以实现onTrimMemory()接口回调来响应不同的内存相关的事件,

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.
                   释放当前占用内存你的UI对象,当前UI已经移到后台。
                */
            }

            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.
                   释放所有应用不需要的内存
                   设备处于低内存状态,这时应用还在运行。
                   该事件突出了内存相关事件的严重程度。
                   如果当前事件是`TRIM_MEMORY_RUNNING_CRITICAL`,那么系统将开始终止后台进程
                */
            }

            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.
                   
                   尽可能地释放进程的内存。
                   应用处于`LRU`列表中,且系统处于低内存状态。该事件指明了当前应用处于`LRU`列表中。
                   如果事件是`TRIM_MEMORY_COMPLETE`,应用进程将第一个被终止。
                */
            }

            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.
                  
                  释放非必要数据结构
                  应用收到了一个未识别的系统内存事件,把该是当做通常的低内存信息来处理。
                */
            }
        }
    }
}

onTrimMemory()回调方法在Android4.0(API level 14)被添加。更早期的系统版本,可以使用onLowMemory(),这个回调方法几乎等同于TRIM_MEMORY_COMPLETE事件。

查看应用该使用多少内存

为了允许多个进程同时运行,Android对每个应用进程堆内存的大小设置了硬性的限制。这个限制的具体值会根据设备总体可用RAM的不同而变化。如果应用到达了对内存容量的极限,这时尝试分配更多的内存,系统会抛出OutOfMemory错误。

为了避免运行时超过内存限制,应用可以查询系统来获知当前设备应用可用的最大堆内存空间。应用可以通过getMemoryInfo()方法向系统查询该值。该方法会返回一个ActivityManager.MemoryInfo对象,这个对象提供了关于设备当前内存状态的信息,包括可用的内存,总内存以及触发系统终止进程的内存阈值。ActivityManager.MemoryInfo对象还会提供一个简单的lowMemory布尔值,它表示当前设备是否内存不足。

以下的代码片段是一个如何使用getMemoryInfo()的例子:

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.
//获取一个代表了当前内存状态的`MemoryInfo`对象
private fun getAvailableMemory(): ActivityManager.MemoryInfo {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    return ActivityManager.MemoryInfo().also { memoryInfo ->
        activityManager.getMemoryInfo(memoryInfo)
    }
}

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

一些Android特性,Java类和代码结构会使用更多的内存。应用代码可以选择更高效的替代方案来缩减占用的内存。

谨慎使用Service

让不需要的Service持续运行,这是Android内存管理中犯下的最严重的错误之一。如果应用需要一个Service以便在后台执行任务,除非真实需要执行任务,否则请它停止运行。请注意在Service完成工作后使其停止运行。否则,您可能无意中造成了一起内存泄漏。

当应用启动一个Service,系统会尽可能让它持续运行。由于被Service使用的内存不再会被其他内存使用,这种策略导致Service进程是成本很高。这降低了系统可以在LRU缓存中维护的进程数量,从而降低应用切换效率。当内存紧张,系统无法维护足够的进程来托管当前运行的所有服务时,甚至会导致系统出现内存抖动。

因为对于内存的持续性需求,通常要避免使用持续性的Service。我们推荐您使用JobScheduler等替代实现方案。关于如何使用JobScheduler来调度后台进程的更多信息,请查看后台优化

如果应用必须使用Service,限制生命周期的最佳方案是使用IntentService,它会在完成启动时的意图后就停止运行。更多信息请查看在后台服务中运行

使用优化过的数据容器

编程语言提供的一些类没有针对移动设备进行优化。例如,常规的HashMap实现内存效率很低,因为每一个映射都需要对应一个单独的实体对象。

Android狂阿基包含了多个优化过的数据容器,包括SparseArray,SparseBooleanArray,和LongSparseArray.例如,SparseArray这个类是更高效的,因为它避免了系统需要对键(有时候值也需要)自动装箱的操作(这个操作会为每个实体创建额外的一个或两个对象)。

如果必须,您可以随时切换回原始数组已获得非常精简的数据结构。

谨慎对待代码抽象

开发者会经常把抽象作为一个好的编程实践,因为抽象可以提高代码的灵活性和可维护性。但是,抽象带来一个显著的成本:它需要更多的代码才能执行,这需要更多的时间和RAM(空间)把代码映射到内存。所以如果你的抽象没有带来显著的好处,应该避免它们。

数据的序列化使用精简版的Protobuf

Protocol buffers是一种跨语言、跨平台、可扩展的机制,Google研发它用于序列化结构性数据,与XML类似,但是更小,更快,更简单。如果应用决定使用ProtoBufs来序列化数据,您应该在客户端上始终使用精简版的ProtoBufs。常规版本的ProtoBufs会生成非常冗长的代码,这会导致应用出现很多问题,如增加了RAM使用,显著增大了APK的尺寸,执行速度变慢。

关于精简版本ProtoBufs的更多信息,请查看ProtoBufs自述文件

避免内存抖动

正如之前提到的,垃圾回收时间通常不会影响应用的性能。但是,短时间内触发多次垃圾回收事件可以消耗掉一帧的绘制时间。系统用在垃圾回收的时间越多,它可以用在绘制或传输音频上的时间就越少。

通常,"内存抖动"会触发大量的垃圾回收事件。实际上,内存抖动描述了给定时间内分配临时对象的数量。

例如,您可能在for循环中分配了多个对象。或者在View的onDraw()方法中创建了新的PaintBitmap对象。在这两种情况下,应用都会快速创建大量的对象。这会导致快速消耗新生代中所有的内存,强制触发一个垃圾回收事件。

当然,在修复内存抖动之前,你需要先在代码中找到它们。为此,您应该使用Android Studio提供的Memory Profiler

确定了代码中的问题区域后,尝试减少在性能的关键区域分配内存的数量。可以考虑把相关点从最内存的循环中移出,或许可以使用一个基于工厂模式的对象创建结构中。

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

一些资源和库会在你不知情的情况下吃掉大量内存。增大apk的总体大小,包括第三方的库或内嵌的资源可以影响应用消耗内存的大小。您可以提优化应用的内存消耗,通过在代码中移除所有重复的,不需要的或者臃肿的模块。

缩减总体apk的大小

缩减apk的总体大小可以显著地缩减应用的内存使用。位图大小,资源,动画帧和第三方库都会增加apk的大小。Android Studio 和 Android SDK提供了很多工具,这些工具可以帮助您减小资源和外部库的体积。这些工具支持现代的代码裁剪方法,例如R8编译。(Android Studio 3.3之前的版本使用Proguard代替R8编译)

关于缩减整体apk大小的更多信息,请查看如何缩减应用大小

依赖注入使用Dagger2

依赖注入狂阿基可以简化代码的编写,并且提供了一个可用于测试和配饰更改的自适应环境。

如果您打算在应用中使用依赖注入框架,请考虑使用Dagger2Dagger并没有使用反射来扫描代码。Dagger的静态,编译时实现方案意味着它在Android应用中不会增加运行时的成本和内存使用。

其他由反射实现的依赖注入框架倾向于扫描代码中的注解来初始化进程。这个进程会要求更多的CPU时间片和RAM,并且会导致一个应用启动明显的延迟。

谨慎使用外部库

外部库的代码通常没有针对移动环境优化,用于移动客户端可能很低效。当您决定使用一个外部库时,您可能需要针对移动环境优化该库。在决定使用这个库之前,要提前规划,并且分析这个库在代码大小和内存消耗方面的表现。

即使是针对移动场景做过优化的库,也可能因为实现方式的不同而导致问题。例如,一个库使用了精简版的ProtoBufs,当另一个库使用了Micro ProtoBufs,导致应用内实现了两种ProtoBufs。在日志记录,统计工具,图片加载框架,缓存以及很多想象不到的方面,都可能发生这种多个实现的问题。

尽管Proguard可以通过恰当的标签来移除API和资源,但是不能移除一个库的大型内部依赖项。在用这些库的时候,您需要的功能可能需要低级别的依赖项。下面的情况中,这会造成显著的问题:当您从一个库中使用一个Activity子类(往往需要大量的依赖项)时这个库使用了反射(这很常见,也意味着您需要花很多时间手动调整Proguard规则来让它正常工作)等等。

另外,避免使用只用到一两个功能的共享库,但是它提供了十几个功能。您一定不希望引入大量的用不到的代码。当您考虑是否使用一个库时,请寻找与你需求最匹配的实现。否则,您可以考虑创建自己的实现。

original

About 【Using English】

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容