您的位置:首页 > 移动开发

管理Android APP的内存

2016-10-17 20:49 239 查看
[SDK官网原文链接](https://developer.android.com/topic/performance/memory.html

在任何一个软件开发环境中,RAM都是有价值的资源,然而在物理内存受限的移动操作系统中,它显得更有价值。尽管由Android的Dalvik虚拟机负责内存垃圾的回收,但是在什么时候以及在哪里分配和释放内存都是不容忽视的问题。

为了便于GC更合理的回收APP的内存,你应该避免内存泄漏(通常由于持有全局成员的对象引用所导致),和对象引用在合适的时机释放(它被定义在下面讨论的生命周期回调)。对于大部分APP而言,

当对象离开APP活动线程的范围之外,但没有被系统回收的内存,将由Dalvik GC去负责处理。

本文解释了Android如何管理应用程序进程和内存分配,以及如何主动减少内存使用。

一、Android如何管理内存

Android 没有提供内存交换空间,但是它可以用分页和内存映射去管理内存。这意味着你修改的任何的内存,不论是分配的新对象还是产生的映射分页,都将驻留在RAM,不能被换出。因此完全释放内存的唯一方式就是释放你持有的对象的引用。但有一个例外:mmap任何文件没有修改,例如代码,可以调出内存如果系统要使用内存。

共享内存

为了满足一切RAM的需要,Android可以跨进程共享RAM分页。主要体现在一下几种形式:

每个APP进程都是从一个现有的称作Zygote进程fork出来的。当系统启动,加载公共framework代码和资源(比如:activity主题)时,Zygote进程就开始启动了。为了开启一个新的APP进程,系统会从Zygote进程fork一个新的进程,然后加载和运行APP代码在这个新的进程里。对于分配给framework代码和资源的大部分RAM分页,都可以在所有APP进程间共享。

大部分静态数据被映射到同一进程。这不仅允许同一数据在不同进程间共享而且能够在需要的时候被换出。示例的静态数据包括:Dalvik代码(处在用于直接映射的预链接的.odex文件),APP 资源(用于设计能直接映射的资源表结构和调整APK的ZIP条目),以及传统项目元素比如.so文件的native代码。

在许多地方,Android 跨进程分享相同动态RAM主要体现在显示分配的内存共享区域(ashmem或者gralloc)。例如:window surface在APP和屏幕渲染之间共享内存,cursor缓存在content provider和使用者之间共享内存。

(1)APP内存的分配和回收(待完善)

下面是一些关于Android如何从你的APP分配和回收内存的事实:

每个进程的Dalvik堆都限制在一个虚拟内存的区间里。它定义了逻辑的heap size,heap size可以根据需要增加,但是系统为每个app定义了最大size。

堆的逻辑大小和堆使用的物理内存的大小并不是一致的。当观察APP堆时,你会发现Android会计算称为比例设置大小(PSS)的一个值,它包括跨进程分享的脏的和干净的页,但是这个数量是和有多少个APP共享那个RAM成比例的。系统把PSS总量当做物理内存的占用量。

二、监控可用内存和内存使用

Android框架,Android Studio 以及Android SDK可以帮助你分析和调整你的APP内存使用,Android 框架公开了一些允许你运行时动态减少内存使用的API,Android Studio 和Android SDK提供了一些工具允许你查看APP内存使用情况。

(1)分析RAM使用情况的工具

在你解决APP内存使用问题之前,你首先应当找到问题的地方。Android Studio 和Android SDK包含了一些分析你的APP内存使用的工具。

1.设备有一个叫做DDMS工具,允许你检查APP进程之间的内存使用。你能用这些信息去查看你的APP整体使用情况。例如,你可以强制触发一个GC,然后查看保持在内存中对象类型,你可以使用这些信息来区分你在APP中的操作是否在内存中分配或留下过多的对象。

了解更多的如何使用DDMS工具,请参看 Using DDMS

2.在Android Studio里的Memory Monitor会显示在一个单独会话过程中的内存分配情况,这个工具会显示Java内存可用和分配推移图,以及垃圾回收事件。当你的APP运行时,你也可以启动垃圾回收事件和Java堆的快照。Memory Monitor 工具输出的信息可以帮你识别过度的垃圾回收导致的APP运行缓慢时间节点。

了解更多的如何使用Memory Monitor工具,请参看 Viewing Heap Updates

3.垃圾回收事件也可以显示在Traceview视图里,Traceview允许你通过时间轴和方法内部发生的概要内容来查看跟踪日志文件。你能用这个工具来确定当GC事件发生时什么代码正在执行。

了解更多的如何使用Traceview viewer,请参看Profiling with Traceview and dmtracedump

4.Android Studio里的Allocation Tracker工具会给你内存分配情况的详细展示,它记录了一个APP内存分配情况,以及通过简要快照来列出所有的分配对象,你可以用这个工具来追踪部分代码分配了太多对象。

了解更多的如何使用Allocation Tracker,请参看 Allocation Tracker Walkthrough

(2)释放内存响应事件

一个Android设备能够不同数量的空闲内存上,不管设备的物理内存多大,以及用户如何操作它。当内存有压力时,系统会发出广播信号,APP应当监听这些信号,调整合适的内存运行。

你可以使用API ComponentCallbacks2来监听这些信号,然后调整内存使用以响应app的生命周期和设备事件,onTrimMemory() 方法允许你的APP监听内存相关事件,不管它是运行在前台,还是运行在后台。监听这些事件,然后实现Activity的onTrimMemory()回调,如下面代码所示:

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

public class MainActivity extends AppCompatActivity
implements 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.
*/
public void onTrimMemory(int level) {

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

case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

/*
Release any UI objects that currently hold memory.

The user interface has moved to the background.
*/

break;

case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
case 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.
*/

break;

case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
case 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.
*/

break;

default:
/*
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.
*/
break;
}
}
}


onTrimMemory()回调被新增在Android 4.0(API级别14),对于早期版本,您可以使用onLowMemory()回调为旧版本回退,大致相当于TRIM_MEMORY_COMPLETE事件。

(3)检查你应该使用多少内存

为了允许多个运行时进程共存,Android为每个APP设定了严格的堆大小限制。具体堆大小限制数值和具体设备整体可用的RAM有关,如果你的APP已经达到了堆的容量,依然尝试申请分配更多内存,系统就会抛出OOM异常。

为了避免耗尽内存,你可以查询系统来确认当前设备还有多少可用空间,你可以通过调用getMemoryInfo()来查询这一数字,它将返回一个提供关于设备的当前内存状态信息的ActivityManager.MemoryInfo 对象,这个对象包括可用内存,总内存,内存阈值(系统开始杀死进程的临界值)。ActivityManager.MemoryInfo类也公开了一个布尔域-lowMemory,它将告诉你设备是否以低内存的状态运行。

下面的代码片段,显示了如何在你的应用中使用getMemoryInfo()方法的例子。

public void doSomethingMemoryIntensive() {

// Before doing something that requires a lot of memory,
// check to see whether the device is in a low memory state.
ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();

if (!memoryInfo.lowMemory) {
// Do memory intensive work ...
}
}

// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);
return memoryInfo;
}


三、如何更高效的使用内存结构

一些Android 特性,Java classes,以及代码结构相比于其他的更耗内存。你应该在你的代码中作出有效的选择来最小化你的使用的内存。

(1)使用较少的Service

保持一个不再需要的Service一直运行,是Android APP内存管理犯的最严重的问题之一。如果你的APP需要一个Service来执行后台工作,其实并不需要一直保持它运行,除非它一直在运行一个任务。

当你的Service完成任务时,记得要停止你的Service。否则,你将无意中导致内存泄漏。

当你启动一个Service时,系统更倾向于保持你的Service进程一直运行,这一行为将导致你的Service非常昂贵,因为一个Service占用的RAM对于其他进程而言依然是不可用的,这将减少系统维持的在LRU Chache中的缓存进程的数量,那将导致APP的切换显得低效。那甚至有可能导致系统抖动当系统内存紧张以至于不能维持足够的进程来确保所有Service运行。

一般情况下应该避免使用持续的Service,因为他们对可用内存有持续性的需求。相反,我们建议您使用另一种JobScheduler等实现。了解更多的信息关于如何使用JobScheduler处理后台进程,参看JobScheduler

如果你必须使用一个Service,最好的方式就是使用IntentService来限制你的Service的生命周期,IntentService可以尽可能早的结束,当处理完成你启动的Intent。

(2)使用优化的数据容器

编程语言提供一些类在移动终端设备上并不是最优的,例如原生HashMap的实现是比较内存低效的,因为对于每一个映射都需要一个单独的条目对象。Android框架包括几个优化的数据容器,包括SparseArray SparseBooleanArray,LongSparseArray。例如,SparseArray是比较内存有效的,因为它避免了系统自动对key,有时可能是value的自动装箱(int转为Integer类型)。如果有必要,你可以切换到很瘦数据结构的原始数组。

(3)谨慎使用代码抽象

开发者通常使用抽象编程作为一个良好的编程实践,因为抽象代码可以提高代码的灵活性和可维护性。然而抽象的同时也付出了巨大的代价:那通常需要大量更多的执行代码,更耗费时间,以及需要更多的RAM用以将代码映射到内存中。如果你的代码抽象并不能带来重大好处,应当尽量避免。

例如:枚举通常需要超过两倍于静态常量的内存来存储,在Android上你应该严格避免使用枚举类型。

(3)使用nano版本的序列号对象数据Protocol buffers

Protocol buffers是语言中立的,平台无关的,可扩展的机制由谷歌设计用于序列化结构化数据类似于XML,但更小、更快,更简单。如果你决定使用protobufs数据,你应该总是使用nano版本的protobufs在客户端代码。常规protobufs会生成非常冗长的代码,这可能会导致在APP中的多种问题,增大RAM使用,增加APK大小增加,执行慢。

(4)避免内存生产

如前所述,垃圾回收通常不会影响应用程序的性能。然而,许多垃圾收集的事件发生同一个短时间内可以迅速吃掉你的帧时间。系统花在垃圾回收的时间越多,相对的做其他事情(渲染或流式音频)的时间就越少。

通常,内存的产生会导致大量垃圾回收事件的发生。实际上,大量内存的产生往往是在一段时间大量临时对象的创建导致的。例如,你可能会在一个for循环分配多个临时对象,或者你可能在View的OnDraw回调里创建Paint或者Bitmap对象。 在这两种情况下,APP会以较高的频率产生大量的对象。这些会导致快速消耗年轻代中所有可用内存,强制垃圾回收事件的发生。当然,在你解决这个问题之前,你应当首先找到在代码中内存高发的地方,一旦你识别出了代码中的问题区域,应当尽量减少在这些关键区域的分配数量。应当考虑将其移出到内部循环外或者有可能放到用于分配数据结构的工厂类中。

四、移除内存密集型资源和库

一些资源和库能够在你不知情的情况下吞噬你的内存。APK的总体大小,包括第三方库和嵌入的资源,都会影响应用程序消耗多少内存。你可以提高你的应用程序的内存消耗通过从你的代码中删除任何多余的,不必要的,或者膨胀的组件,资源。

(1)减少APK的整体大小

通过减小APK的整体大小,你可以很大程度的减少你的APP所使用的内存。位图的大小,资源,动画帧,以及第三方库都会增加你的APK的大小。Android Studio 和 Android SDK提供多种工具来帮助你减少资源的大小和外部依赖。如何减小APK的整体大小,请参看之前译文。

(2)谨慎使用依赖注入框架

依赖注入框架( 例如:Guice or RoboGuice)能够简化你的代码,和提供便于测试和其他配置更改的环境。然而,依赖框架对移动终端并不是一直是优化的。

例如:这些框架会在初始化进程的过程中通过扫描你的代码来寻找注释,这就需要大量的不必要的代码映射到RAM中。系统分配这些映射页面到干净内存以便于回收它们。然而这些页面会在内存中驻留很长一段时间才会被回收。

如果你需要在你的APP中使用一种依赖注入框架,可以考虑Dagger。例如:Dagger不会使用反射来扫描你的代码,Dagger严谨的实现使得它可以在用你的代码中,而不会增加不必要的内存使用。

(3)小心使用外部库

外部库代码通常不是写给移动开发环境的,在移动客户端使用外部库是效率不高的。当你决定使用一个外部库时,你可能需要优化你的外部库为移动设备。在你使用它之前,要在代码的大小和内存占用方面做好前期的计划和库分析。

甚至一些经过移动端优化的库也可能由于实现上的差异,导致一些的问题的产生。举例:一个库可能会使用nano版本的protobufs,然而另一个库可能使用micro版本的库,这就产生了两种不同的实现在你的APP代码中。这可以发生在日志记录,分析,图像加载框架,缓存以及其他你未能预料的事情上。

尽管ProGuard能通过使用正确的标志来移除API和资源,但是它不能移除库本身大量的内部依赖。你可能只是对这些库的很少部分有依赖。那就变得特别有问题,当你使用外部库的一个Activity子类,而它会有一大片依赖项时,当你所使用的外部库使用反射,那是常见的,但是需要你花费大量时间来手动配置ProGuard以使它工作时,等等场景。

你应当尽量避免当你依赖一个外部库,而只是使用库中众多功能的一两个而已。你可能自己也不希望引入一大堆你用不上的代码和开销,因此当你寻找采用外部库时,应当采用和你需求强烈匹配的库。否则,你可能需要创建你自己的实现。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息