【官网翻译】性能篇(六)管理应用内存
前言
本文翻译自Android开发者官网的一篇文档,主要用于介绍管理应用内存方面的知识。
中国版官网原文地址为:https://developer.android.google.cn/topic/performance/memory。
路径为:Android Developers > Docs > 指南 > Best practies > Performance > Manager Your App`s Memory
正文
随机可存取内存(RAM)在任何软件开发环境中都是有价值的资源,但是在移动操作系统中是更加有价值的,因为在移动操作系统中物理内存经常是受到限制的。虽然Android Runtime(ART)和Dalvik虚拟机执行常规的垃圾回收,但这并不意味着您可以忽略应用于何时何处分配和释放内存。您仍然需要避免引入内存泄漏以及在合适的时间释放所有由生命周期回调定义的引用对象,这些内存泄漏经常是由在静态成员变量中持有对象引用引起的。
本页阐述了您可以如何积极主动地减少应用的内存使用。关于Android操作系统如何管理内存的信息,请查阅【Android内存管理概述】。
监视可用内存和内存使用
在您可以修复应用中内存使用问题之前,您首先应该找到它们。Android Studio中的【Memory Profiler】可以通过如下的方式帮您找到并诊断内存问题:
1. 查看随着时间的推移应用是如何分配内存的。【Memory Profiler】显示了一份实时图像,该图像包含了应用正在使用多少内存,被分配的Java对象数量,以及什么时候发生垃圾收集。
2. 当应用运行时,发起垃圾收集事件并且抓取Java堆快照。
3. 记录应用的内存分配,然后检查所有分配的对象,查看每一个分配的栈追踪,并且跳转到Android Studio编辑器中相应的代码处。
在事件响应中释放内存
正如【Android内存管理概述】中所描述的,Android会以多种方式从应用中收回内存,或者如果有必要的话,为了极重要的任务甚至会杀死整个应用。为了进一步地帮助平衡系统内存以及避免系统杀死您应用进程的需求,您可以在Activity类中实现ComponentCallbacks2接口。提供的onTrimMemory()回调方法允许应用无论在前台还是后台都能监听内存相关的事件,然后释放对象以响应应用生命周期或者指示需要收回内存的系统事件。
例如,您可以实现onTrimMemory()回调来响应不同的内存相关事件,正如下面所展示的:
1 import android.content.ComponentCallbacks2; 2 // Other import statements ... 3 4 public class MainActivity extends AppCompatActivity 5 implements ComponentCallbacks2 { 6 7 // Other activity code ... 8 9 /** 10 * 当UI隐藏或者系统资源变得不足时,释放内存. 11 * @param level 引发的内存相关事件. 12 */ 13 public void onTrimMemory(int level) { 14 15 // 判断引发了哪个生命周期或系统事件. 16 switch (level) { 17 18 case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN: 19 20 /* 21 释放所有当前持有内存的UI对象. 22 23 用户接口已经转移到了后台。 24 */ 25 26 break; 27 28 case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE: 29 case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW: 30 case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL: 31 32 /* 33 释放所有您的应用不再需要运行的内存。 34 35 当应用正在运行时,设备内存不足。 36 被引发的事件指示了内存相关事件的严重性。 37 如果引发的事件是TRIM_MEMORY_RUNNING_CRITICAL, 然后系统将会开始杀死背景进程。*/ 38 39 break; 40 41 case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND: 42 case ComponentCallbacks2.TRIM_MEMORY_MODERATE: 43 case ComponentCallbacks2.TRIM_MEMORY_COMPLETE: 44 45 /* 46 进程尽可能多地释放内存。 47 48 应用在LRU(译者注:Least Recently Used,最近最少使用)列表中,并且系统内存不足。 49 该引发的事件指示了应用在LRU列表中的位置。 50 如果该事件是TRIM_MEMORY_COMPLETE,该进程将会成为首先被终止的进程之一。*/ 51 52 break; 53 54 default: 55 /* 56 释放所有不是特别重要的数据结构。 57 58 应用收到了一个来自系统的不被识别的内存等级值。把它当成是一个一般的低内存消息。*/ 59 break; 60 } 61 } 62 }
onTrimMemory()回调方法是在Android4.0(API等级为14)中新增的。对于更早的版本,您可以使用onLowMemory(),它大致等同于TRIM_MEMORY_COMPLETE事件。
检查您应该使用多少内存
为了允许多个进程同时运行,Android为每一个应用的堆大小分配设置了一个硬性限制。确切的堆大小限制因设备总体可用RAM的多少不同而不同。如果应用已经达到了堆的容量,并且试图分配更多内存,系统就会抛出OutOfMemoryError。
为了避免运行时内存溢出,您可以查询系统来确定在当前设备上有多少堆空间可以使用。您可以通过调用getMemoryInfo()查询系统的这份数据。该方法会返回一个ActivityManager.MemoryInfo对象,该对象会提供关于设备当前的内存状态,包括可用内存,总内存,以及内存阈值——系统开始杀死进程的内存水平。ActivityManager.MemoryInfo对象也提供了一个简单的boolean型变量lowMemory,它会告诉您是否设备内存不足。
下面的代码片段显示了一个示例,用于展示如何在应用中使用getMemoryInfo()方法。
1 public void doSomethingMemoryIntensive() { 2 3 // 在做需要申请大量内存的事情之前,检查看看是否设备处于内存不足状态。 4 ActivityManager.MemoryInfo memoryInfo = getAvailableMemory(); 5 6 if (!memoryInfo.lowMemory) { 7 // 处理内存密集型的工作 ... 8 } 9 } 10 11 // 为设备当前的内存状态获取MemoryInfo对象. 12 private ActivityManager.MemoryInfo getAvailableMemory() { 13 ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE); 14 ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo(); 15 activityManager.getMemoryInfo(memoryInfo); 16 return memoryInfo; 17 }
使用更节省内存的代码结构
一些Android特性,Java类,以及代码结果趋向于比其它的使用更多内存。您可以通过在代码中选择更高效的替代者来最小化应用使用的内存数量。
谨慎使用service
当不需要使用service时仍让它保持运行是Android应用可能犯的最严重的内存管理错误之一。如果应用需要service在后台执行工作,不要保持它一直运行,除非需要运行一项作业。当service完成任务后,记得停止它。否则,您可能在不经意间引入内存泄漏。
当您启动一个service,系统偏向于始终保持该service的进程运行。这个行为让service进程消耗很大,因为被service使用的RAM不能被其它进程使用。这减少了系统保留在LRU缓存的缓存进程的数量,使得应用的切换变得更加低效。当内存紧张并且系统不会维持足够的进程来承载所有正在运行的service时,这甚至可能导致系统震荡。
一般来说,您应该避免使用持久性的service,因为它们对可用内存的持续需求。相反,我们建议您使用一个替代的实现方案,比如【JobScheduler】。关于怎样使用【JobScheduler】来调度后台进程的信息,请查【后台优化】。
如果您必须使用service,限制service寿命最好的办法就是使用【IntentService】,当处理完启动它的intent,它会尽快结束自己。更多的信息,请查阅【运行后台service】。
使用优化过的数据容器
有一些由编程语言提供的类在移动设备上的使用没有被优化。例如,一般的HashMap实现可能内存效率很低,因为每一个映射都需要一个单独的入口对象。
Android框架包含了若干优化后的数据容器,包括SparseArray,SparseBooleanArray,以及LongSparseArray。例如,SparseArray类更有效,因为它们避免了系统对键或值装箱(译者注:原始类型到对象类的自动转换,如int到Integer)的需求(这会为每个条目创建一个或两个对象)。
如果有必要,您可以始终切换到原始队列来获得真正精简的数据结构。
注意代码抽象
开发者经常使用抽象简单地作为一个良好的编程实践,因为抽象可以改善代码灵活性和可维护性。可是,抽象带来了显著的代价:通常它们需要跟多的代码来执行,需要更多时间和更多RAM来将这些代码映射到内存。所以,如果您的抽象没有提供显著的好处,您应该避免它们。
为序列化数据使用纳米协议缓存
【协议缓存】是一个由Google为序列化结构化数据设计的语言无关,平台无关,可继承的机制——类似于XML,但是更小,更快,更简单。如果您决定为您的数据使用协议缓存,您应该始终在客户端代码中使用纳米协议缓存。常规的协议缓存生成了极其冗余的代码,它们可能导致应用中各种类型的问题,比如RAM使用的增长,显著的APK大小增长,以及执行更慢。
更多信息,请查阅【protobuf readme】中“Nano 版本”部分。
避免内存搅动
正如前面提到的,垃圾收集事件一般不会影响应用的性能。但是,许多很短时间内发生的垃圾收集事件可能很快占用您的帧时间。系统花费在垃圾收集上的时间越多,那么花费在处理渲染或者流式音频等事件上的时间就越少。
通常,内存搅动会导致大量的垃圾收集事件发生。实际上,内存搅动描述了发生在给定的时间内分配的临时对象的数量。
例如,您可能在for循环中分配多个临时对象。或者您可能在一个视图的onDraw()方法中创建新的Paint或者Bitmap对象。在这两种情况下,应用迅速以高容量创建了许多对象。这些可能快速地消耗所有“年轻代”中可用的内存,强制发生垃圾收集事件。
当然,在您可以修复它们之前,您需要在代码中找到这些内存搅动高地方。为了实现这些,您应用使用Android Studio中的【Memory Profiler】。
一旦您确认了您代码中发生问题的区域,就尝试降低性能重要区域内的分配数量。考虑将事物移出内部循环,或者可能把它们移动到一个基于分配结构的【工厂】。
移除内存密集型的资源和库
您代码中的一些资源和库可能在您毫不知情的情况下吞噬掉内存。您的APK的总体大小,包括第三方库或者嵌入的资源,可能影响到您应用消耗了多少内存。您可以通过从您代码中移除冗余的、不必要的、臃肿的组件、资源、或者库来改善应用内存消耗。
减少总体APK大小
通过减少应用的总体大小,您可以显著地降低应用的内存使用。Bitmap大小、资源、动画帧、以及第三方库都有可能影响APK的大小。Android Studio和Android SDK提供了多种工具来帮您缩小资源的大小和外部依赖。
更多关于如何降低整体应用大小的信息,请查阅【缩减应用大小】。
为依赖注入使用Dagger2
依赖注入框架可以简化您写的代码,并且提供一个可以适应的环境,该环境对测试和其它配置的改变是有用的。
如果您意图在应用中使用依赖注入框架,考虑使用【Dagger2】。Dagger没有使用反射来扫描应用的代码。Dagger的静态编译时实现意味着它可以应用于Android应用,而不用在意不必要的运行时消耗或者内存使用。
其它的使用反射的依赖注入框架趋向于通过扫描注解代码来初始化进程。该进程可能需要明显多的CPU周期和RAM,并且当应用启动时可能导致引入注目的滞后。
谨慎使用外部库
外部库代码经常不是为移动环境编写的,并且在移动客户端使用时可能是无效的。当您决定使用外部库时,您可能需要为移动设备优化那个库。在全然决定使用它前,为当前的工作做好计划,并且依据代码大小和RAM足迹分析这个库。
即使是一些为移动优化库也可能导致问题,因为不同的实现方式。例如,一个库可能使用纳米协议缓存,然而其它应用却使用微型协议缓存,这导致了在您应用中有两种不同的协议缓存实现。这可能发生于不同的日志、分析、图片加载框架、缓存以及您无法预料的许多其他事情的实现方式中。
虽然【ProGuard】可以帮您移除具有正确标记的API和资源,但它无法移除库中大型的内部依赖项。这些库中您想要的特性可能需要较低级别的依赖项。当您使用库中Activity子类时(它将趋向于拥有广泛的依赖链),当库使用反射时(这是很普遍的并且意味着您需要花费很多时间手动调整ProGuard来让它工作),等等,这将变得尤为有问题。
同时避免为了几十个特性中的一两个而使用共享库。您不想导入大量的您甚至不使用的代码和开销。当您考虑是否使用库时,寻找一种和您所需的高度匹配的实现方案。否者,您可能要决定创建您自己的实现方式。
结语
本文最大限度保持原文的意思,由于笔者水平有限,若有翻译不准确或不妥当的地方,请指正,谢谢!