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

[Android] 最全内存分析—2016年10月9日

2016-10-09 02:21 176 查看
转载请注明出处

管理 App 的内存

随机存取存储器(RAM)是在任何软件开发环境的宝贵资源,它更在物理存储器常常受限的移动操作系统中大放异彩。尽管 Android 的 Dalvik 虚拟机执行日常垃圾收集,但这也不允许你忽视分配和释放应用程序内存的时机和场景。

为了使垃圾收集器从应用回收内存,应该避免引入内存泄漏(通常由持有到全局变量引用造成),并在适当的时候释放由下面将进一步讨论生命周期回调定义的参考对象。对于大多数应用程序中,Dalvik垃圾收集花费其余的工作:当相应的对象留下您的应用程序的活动线程的范围时,系统会回收内存分配。

本文介绍 Android 如何管理应用程序和内存分配,以及如何积极主动地降低内存的使用,同时开发Android。

Android 如何管理内存

Android 内存不提供交换空间,但它使用分页和内存映射(mmapping)来管理内存。这意味着你修改的任何内存 - 无论是通过分配新的对象或(touching mmapped pages:触摸 mmapped 页?),仍驻留在内存中,不能被调出。因此,释放你可能持有的对象引用是从 App 彻底释放内存的唯一途径,垃圾收集器可以回收内存。一个例外:如任何没有修改 mmapped 中文件(如代码),如果系统要在其他地方使用该内存就可以换出内存。

共享内存

为了适应在RAM中的任何情况,Android会在进程间共享内存页。

有三种方式实现:

每个App进程都是从一个叫 Zygote 的进程分支(fork)出来的。Zygote进程伴随系统一起启动,并开始加载一些通用框架的代码和资源(如Activity的主题)。在启动一个新的App进程时,系统会分支(fork)出 Zygote 进程,然后在新的进程里面加载并运行 App 的代码。这使得大部分分配给框架代码和资源 RAM pages 跨所有 APP 共享。

大部分的静态数据被映射到了一个进程(注:静态数据的特点是READ-Only)。这不仅允许在不同进程间共享相同的数据,也可以在被需要的时候换出。典型的静态数据包括:Dalvik虚拟机的代码(放在一个提前链接好了,可以正确映射(mmapping)的.odex文件中),App 资源(资源表被设计成一种可以被映射的结构以及by aligning the zip entries of the APK)还有像放在 .so 文件中的本地代码的项目元素。

在很多地方,Android在不同进程间共享内存是通过明确分配共享内存区域(无论是与 ashmem 或gralloc)。例如,窗口界面会在 App 进程和屏幕图像合成器(screen compositor)之间共享内存、光标缓冲(cursor buffers) 会在 内容提供器 和 客户 端共享内存。

由于大量使用共享内存,需要确定 App 使用多少内存。确定 App 内存使用量的相关内容请查看

Investigating Your RAM Usage

分配和回收 App 内存

以下有一些关于 Android 如何分配和回收内存的内容:

每个进程的 Dalvik 堆被限制在单个的虚拟内存范围,定义了一个可以根据需要成长的逻辑堆的大小(但系统会限制每个 App 的最大内存占用)。

堆的逻辑大小和堆使用的物理内存不同,当检查 App 的堆时,Android的计算一个称为比例大小(PSS)的值,这个值包含了共享的脏页和干净页(dirty and clean pages). 但是它会按照有多少App 共享了这块内存来按比例算出均摊的部分作为PSS。而这个 PSS 的总共大小才是占用物理内存的大小。有关PSS的详细信息,请参阅 Investigating Your RAM Usage

Dalvik 堆不会压缩堆的逻辑大小,这意味着 Android 不会整理堆来腾出空间。Android只会在堆的末尾有未使用空间时亚索逻辑堆的大小。但是,这并不意味着堆占用的物理内存不能压缩。在垃圾回收之后,Dalvik会遍历整个堆,找到未使用的页面,使用 madsive 返回给内核。因此大量的分配和释放内存会导致回收使用过的物理内存。然而,从较小的资源分配回收存储是低效的,因为分配小资源的页面,可能被其他对象共享,不能释放。

限制 App 内存

Android 为了保持多任务功能环境,对每个 App 在堆大小的设置上有严格限制。Android 设备 RAM 不同,堆大小限制也不同。如果 App 已达到堆上限,并试图继续分配更多的内存,它会收到一个 OutOfMemoryError 异常。

在某些情况下,想查询系统,以确定当前设备到底有多少堆空间可用——例如,以确定有多少数据保持在缓存中是安全的。可以通过调用 getMemoryClass() 查询这个数字系统。它会返回一个以 M 为单位的int 型值。这在下面进一步讨论,检查下你应该使用多少内存,这在检查你应该使用多少内存章节中进一步讨论。

切换 App

当用户在 App 之间进行切换时,Android 使用近期最少使用算法(LRU)将进程保存在一个前台应用组件列表中,而非存放在交换空间。例如,当用户第一次启动一个 App,为它创建一个进程,但是当用户离开 App,进程不会关闭。系统会使进程缓存起来,因此如果用户过段时间返回到该 App,进程重新启用,加速 App 的切换。

如果 App 有一个缓存的进程,它保留了当前不需要的内存,那么当用户没有使用 App 的时候,它影响了系统的整体性能。因此,当系统运行时内存不足时,系统可能会杀死在 LRU 缓存中近期没有使用的进程,但是也同时会考虑杀死占用内存大的进程。为了尽可能长时间地使进程缓存,跟着下面的部分学习何时释放引用。

更多有关进程在非前台环境下是如何被缓存,以及Android如何决定杀死哪个进程的信息,请看 Processes and Threads

监视可用内存和内存使用

Android Framwork,Android Studio,和 Android SDK 可以帮助你分析并调整 App 的内存使用情况。 Android framework 提供了几个 API,允许你的应用程序在运行时动态地降低其内存使用情况。 Android Studio 和 Android SDK 包含了多种工具,让你了解 App 是怎样使用内存的。

分析内存占用的工具

在解决 App 的内存使用的问题之前,首先需要找到它们。Android Studio 和 Android SDK包含多种分析 App 内存使用的工具:

设备监视器(Device Monitor)具有的 Dalvik 调试监视器服务器(DDMS)工具,让你查看 App 进程中的内存分配情况。通过此信息可以了解 App 使用内存的整体情况。例如,你可以强制垃圾回收(GC)事件,然后查看保留在内存中的对象类型。你可以使用此信息到 App,来识别操作——分配或保留对象的过多的内存。

更多有关如何使用DDMS工具的信息,请参阅 Using DDMS

Android Studio 中的内存监视器(Memory Monitor )会告诉你在一个会话的过程中 App 是如何分配内存的。该工具显示一个随着时间推移的,可用和已分配的 Java 内存的曲线图,包括垃圾收集事件。当 App 运行时,你也可以初始化垃圾收集事件以及 take a snapshot of the Java heap。当 App 经过大量垃圾收集事件,内存监视器工具的输出结果可以告诉你导致到 App 运行缓慢的原因。更多有关如何使用内存监视器工具的信息,请参阅查看 Viewing Heap Updates

垃圾收集事件也会出现在 Traceview Viewer 中。 Traceview 可以查看跟踪日志文件 as both a timeline and as a profile of what happened within a method。当一个垃圾回收事件执行的时候,你可以使用此工具来确定代码执行了什么。更多有关如何使用 Traceview viewer 的信息,请参阅 Profiling with Traceview and dmtracedump.

Android Studio 中的分配跟踪(Allocation Tracker)工具,使你能够详细研究 App 如何分配内存。该分配跟踪记录应用程序的内存分配和列出所有分配的对象 within the profiling snapshot。您可以使用此工具来跟踪该分配在过多的对象上的部分代码。更多有关如何使用分配追踪工具的信息,请参阅(噢,原网址已经挂掉了)。

释放响应事件内存

Android 设备可以根据不同设备上 RAM 的物理内存来动态使用可运行的内存,以及用户如何操作该内存。系统广播信号表明它在内存压力,App 应该监听这些信号并适当调整它们的内存使用情况。

你可以使用 ComponentCallbacks2 API 监听这些信号并对 App 生命周期或者设备事件的响应,调整内存使用量。当 App 在前台运行(可见)和在后台运行时,onTrimMemory(int) 方法可以让 App 监听内存相关的事件。

要监听这些事件,在 Activity 类中实现 onTrimMemory(int) 回调即可,例子如下:

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(int) 回调是在 Android 4.0 中(API级别14)添加的。对于早期版本,您可以使用 onLowMemory() 回调为旧版本的回退,这大致相当于 TRIM_MEMORY _COMPLETE 事件。

检查你应该使用多少内存

要允许多个正在运行的进程,Android 设置了分配每个应用程序堆大小的上限。不同的设备上,堆的大小上限值也不同。如果 App 已达到堆上限,并试图分配更多的内存,系统会抛出一个 OutOfMemoryError(OOM)。

为了避免耗尽内存,你可以通过查询系统,来确定有多少堆空间可用当前设备上。你可以通过调用 getMemoryInfo(android.app.ActivityManager.MemoryInfo) 查询数字系统,会返回 ActivityManager.MemoryInfo 对象,提供有关设备当前状态内存信息,包括可用内存,总内存和内存阈值——内存等级低于系统开始杀死进程的等级。ActivityManager.MemoryInfo 类也显示了一个简单的 boolean field,lowMemory 告诉你运行的设备是否内存不足。

下面的代码片段展示了你可以如何使用 getMemoryInfo(android.app.ActivityManager.MemoryInfo) 的例子。

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 类和代码结构倾向于使用更多的内存。你可以在你的代码选择更有效的方法来减少 App 使用的内存。

谨慎使用服务

当一个服务不被需要的时候还让它运行,在 Android App 内存管理中是最愚蠢的行为。如果 App 需要后台运行服务,除非它必须运行,否则不要留它。当服务完成了它的任务一定要停掉它,否则在不经意间你就导致了内存泄漏。

当你启动服务时,系统会优先选择始终保持服务运行的进程。这种行为花费巨大,因为 RAM 总是留给了服务,导致其他进程不可用。这减少了可以保持在LRU缓存中进程的数目,使得 App 切换效率低下。

通常你应该避免使用持续化的服务,因为保证它的运行需要很多的内存。相反,我们推荐你使用别的实现,如 JobScheduler 更多有关如何使用的 JobScheduler 调度后台进程的信息,请参阅 Running in a Background Service

如果你必须使用一个服务,限制你的服务寿命的最佳方法是使用 IntentService,它只要完成了它的任务就会自己消失。欲了解更多信息,请阅读 Running in a Background Service

使用优化的数据容器

一些编程语言提供的类都没有在移动设备上进行优化。例如,HashMap 实现就很糟糕,因为它需要对每个单独的条目对象一一映射。

Android框架包括几个优化的数据容器,包括 SparseArraySparseBooleanArrayLongSparseArray。例如,SparseArray 类就挺好的,因为它机智地避免了系统的自动装箱 key 和 value(已经产生了另一个对象或两个条目)的需求。

如果有必要,你可以随时切换到原数组 for 一个非常精简的数据结构。

注意抽象代码

开发人员经常使用的抽象只是作为一个良好的编程习惯,因为抽象可以提高代码的灵活性和可维护性。然而,抽象花费很大:通常它们需要很多的代码来执行,代码被映射到内存中也需要更多的时间和 RAM。因此,使用抽象如果没给你带来显著的好处,那么尽量不要使用它们。

例如,枚举往往比静态常量多出两倍的内存。快别在 Android 中使用枚举了大兄弟。

使用 nano protobufs 序列化数据

协议缓冲区(Protocol buffers )是一个由谷歌序列化结构化数据,可扩展的中立的语言平台,类似于XML,但是更小,更快,更简单。如果您决定使用 protobufs 为您的数据,你应该在你的客户端代码使用 nano protobufs。protobufs 会有规律地产生冗长的代码,这可能在 App 中出现很多问题,如增加RAM的使用,APK 尺寸显著增加,以及执行速度变慢。欲了解更多信息,请参阅 protobuf readme 的“Nano version”部分。

避免内存流失

正如前面提到的,正常情况垃圾收集事件不会影响 App 的性能。然而,发生在很短的时间内的垃圾收集事件会很快消耗你的 frame time。系统会在垃圾收集花费更多的时间,它就没时间干别地了,例如渲染或音频流。

通常情况下,存储流失可能会导致大量的垃圾收集事件。在实际中,存储流失表现了发生在给定时间量里分配的临时对象数目。

例如,你可能会在 for 循环内分配多个临时对象。又或者你可能在 view 的 onDraw() 方法里创建新的 Paint 或者 Bitmapl 对象。在这两种情况下,App 以高内存快速产生大量的对象。在年轻代区域里,这些所有可得的内存都会被快速消耗,迫使垃圾回收事件出现。

当然,你需要找到在你的代码中内存流失高的地方,才能解决这些问题。可以使用 Analyze your RAM usage 工具。

一旦你确定你的代码中的问题区域,尽量减少性能在关键领域分的配数量。从内部循环中转移出来或者将它们转移到基于分配结构的 Factory 里。

移除内存密集型资源库

你的代码中的一些资源和库可以在你不知情的情况下吞噬内存。 APK 的整体规模,包括第三方库或嵌入的资源,会影响你的App 内存占用。你可以从你的代码中删除任何多余的,不必要,臃肿的组件,资源或库来完善应用程序的内存占用。

降低 APK 的整体尺寸

您可以减少 App 的整体规模来降低 App 的内存使用情况。位图规模,资源,动画帧数和第三方库都可以有助于调整你的 APK 的大小。 Android Studio 和 Android SDK 提供了多种工具来帮助你降低资源和外部依赖的大小。更多有关如何降低总体 APK 大小的信息,请参阅 Reduce APK Size

谨慎使用依赖注入框架

依赖注入框架,如 GuiceRoboGuice 可以简化你写的代码,并提供一个自适应环境,这对测试和其他配置更改非常有用。然而,依赖框架并不总是能优化移动设备。

例如,这些框架往往通过扫描你的代码注解初始化过程。这可需要大量的代码不必要地映射到RAM中。系统分配这些页,映射成清洁内存,所以 Android 可以删除它们;然而,直到页留在内存的很长一段时间内也不能发生。

如果你需要在 App 内使用依赖注入框架,可以考虑使用 Dagger 来代替。例如,Dagger 不使用再反射来扫描 App 代码。Dagger 的严格执行意味着它可以在Android App 中愉快地被使用,而不增加不必要的内存。

小心使用外部库

外部库的代码往往不是为移动环境而生以及用于工作在移动客户端上时,可能低效。当你决定使用一个外部库,你可能需要为移动设备优化它。在决定使用它之前,要分析代码大小和内存占用等。

即使是一些移动设备优化的库可能会由于不同的实现而出现问题。例如,一个库可以使用纳米 protobufs 而另一个采用微 protobufs,导致 App 两种不同的 protobuf 实现。这可以通过记录,分析,图像加载框架以及缓存不同实现。

虽然 ProGuard 可以用正确的标志移除 API 和资源,它不能删除库的大型内部依赖。你在这些库所需的功能可能需要更低级别的依赖性。当你从一个库(which will tend to have wide swaths of dependencies)中使用 Activity 的子类,库使用反射(这很常见,意味着你需要花费大量的时间来手动调整 ProGuard 使它工作)时,它会有问题。

Also avoid using a shared library for just one or two features out of dozens. You don’t want to pull in a large amount of code and overhead that you don’t even use. When you consider whether to use a library, look for an implementation that strongly matches what you need. Otherwise, you might decide to create your own implementation.

最后这段懒得翻译了,我真的要去睡觉了。

全文完。感谢你的阅读。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: