Android开发内存管理

181 阅读11分钟

Android内存优化是性能优化中很重要的一部分,而避免内存溢出(OOM)又是内存优化中比较核心的一点。本篇主要介绍内存占用与OOM相关的知识点。

Android内存管理机制

Google在Android官网上初步介绍了Android系统是如何管理进程间的内存分配管理应用内存。Android 运行时(ART)和Dalvik虚拟机使用paging和memory-mapping来管理内存。下面简要概述一些Android系统中重要的内存管理基础概念。

共享内存

Android系统通过下面几种方式来实现共享内存:

  • 应用进程都是从一个名为Zygote的进程fork出来的。Zygote进程在系统启动,并加载Framework代码与资源之后开始启动。为了启动新的应用进程,系统会fork Zygote进程生成一个新的进程,然后在新的进程中加载并运行应用程序的代码。这种方法使为框架代码和资源分配的大多数 RAM 页面可在所有应用进程之间共享。
  • 大多数static的数据被mmapped到一个进程中。这不仅仅让同样的数据能够在进程间进行共享,而且使得它能够在需要的时候被paged out。常见的static数据包括Dalvik Code、app resources、so文件等。
  • 在很多地方,Android 使用明确分配的共享内存区域(通过 ashmem 或 gralloc)在进程间共享同一动态 RAM。例如,window surfaces在app与screen compositor之间使用共享的内存,cursor buffers在content provider与client之间共享内存。

分配与回收应用内存

  • 每一个进程的Dalvik Heap都反映了使用内存的占用范围。这就是通常逻辑意义上提到的Dalvik Heap Size,它可以随着需要进行增长,但是增长行为会有一个系统为它设定上限。
  • 逻辑上讲的Heap Size和实际物理意义上使用的内存大小是不对等的,Proportional Set Size(PSS)记录了应用程序自身占用以及与其他进程进行共享的内存。
  • Android系统并不会对Heap中空闲内存区域做碎片整理。系统仅仅会在新的内存分配之前判断Heap的尾端剩余空间是否足够,如果空间不够会触发GC操作,从而腾出更多空闲的内存空间。

限制应用的内存

  • 为了整个系统的内存控制需要,Android系统为每一个应用程序都设置一个硬性的Dalvik Heap Size最大限制阈值,这个阈值在不同的设备上会因为RAM大小不同而各有差异。如果你的应用占用内存空间已经接近这个阈值,此时再尝试分配内存的话,很容易引发OutOfMemoryError错误。
  • ActivityManager.getMemoryClass()可以用来查询当前应用的Heap Size阈值,这个方法会返回一个整数,表明应用的Heap Size阈值是多少MB。

应用切换操作

  • Android系统并不会在用户切换应用的时候执行交换内存操作。Android会把那些不包含前台组件的应用进程放到LRU Cache中。例如,当用户开始启动一个应用时,系统会为它创建一个进程。但是当用户离开此应用,进程不会立即被销毁,而是被放到系统的Cache当中。如果用户后来再切换回到这个应用,此进程就能够被马上完整地恢复,从而实现应用的快速切换。
  • 如果你的应用中有一个被缓存的进程,这个进程会占用一定的内存空间,它会对系统的整体性能有影响。因此,当系统开始进入Low Memory的状态时,它会由系统根据LRU的规则与应用的优先级,内存占用情况以及其他因素的影响综合评估之后决定是否被杀掉。
  • 对于那些非前台的进程,Android系统是如何判断Kill掉哪些进程的问题,请参阅进程和线程

内存监控

内存监控的主要指标为:内存占用、OOM。

内存占用情况

通过命令行查看内存占用情况:

adb shell dumpsys meminfo -a com.efs.demo

通过Android Studio的Profiler工具查看内存占用情况(可参考:分析内存使用情况)。

内存占用指标

主要指标如下:

字段指标含义获取方式
JavaHeapJava内存占用Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory()
JavaHeapUsedRateJava内存占用率JavaHeap/Runtime.getRuntime().maxMemory()
Graphics显存Debug.MemoryInfo.getMemoryStat("summary.graphics")
VMSize虚拟内存/proc/进程pid/status
TotalPss物理内存Debug.MemoryInfo.getTotalPss()
DalvikPssJava物理内存Debug.MemoryInfo.dalvikPss
NativePssNative物理内存Debug.MemoryInfo.nativePss

OOM产生条件

待申请的内存大于系统分配给应用的剩余内存

OOM原因归类

对于Android平台,OOM主要有如下原因:

内存优化三方面

主要从以下方面进行优化:

  • 优化大对象
  • 合理复用对象
  • 避免对象泄露

优化大对象

减小新分配出来的对象占用内存的大小,使用轻量的对象。

数据结构可以考虑使用SparseArray而不是HashMap等。

  • 采样率:在加载大图之前,先计算出一个合适的缩放比例,在加载图片
  • 解码格式:ARGB_8888、RBG_565、ARGB_4444、ALPHA_8等存在很大差异

合理复用对象

合理的缓存和复用对象。

Android系统本身内置了很多的资源,如字符串、颜色、动画、样式等,都可以在应用中直接引用。

在ImageView等显示大量图片的控件里,需要使用LRU Cache的机制来缓存处理Bitmap。

onDraw等频繁调用的方法,避免创建对象,因为他会迅速增加内存的使用,而且很容易引起频繁的gc,甚至是内存抖动。

避免对象泄露

内存泄漏,会导致一些不再使用的对象无法及时释放,很容易导致后续需要分配内存的时候,剩余空间不足而出现OOM。

  • 内部类引用导致Activity泄漏
  • Activity被传递到其他实例中,可能导致被引用而发生泄漏

临时创建的Bitmap对象,在经过变换得到新的Bitmap对象之后,应该回收原始的Bitmap。

Android应用中有许多需要register与unregister的监听器,需确保使用后在合适的时机调用unregister注销监听器。

Cursor对象使用后,及时的调用close()。

操作系统内存管理基础

不论什么操作系统,内存管理都是绝对的重点和难点。内存管理旨在为系统中所有 Task 提供稳定可靠的内存分配、释放和保护机制。你可能会疑问,学习 Android 系统有必要了解 Linux Kernel 的内存管理机制吗?

是的!不论是 Android 的音频系统、GUI 系统,还是 Binder 的实现机理等,都是和内存管理息息相关的。

虚拟内存

虚拟内存就是当内存资源不足时,借用硬盘中的一部分的空间,充当内存使用。系统会挑选优先级低的内存数据放入硬盘,后续若要用到硬盘中的数据,系统会产生一次缺页中断,然后把数据交换回内存中。

要理解虚拟内存机制,就要理解三种地址空间,分别是逻辑地址、线性地址和物理地址:

1.逻辑地址(Logical Address)

逻辑地址是程序编译后产生的地址,也称为相对地址,由两部分组成:

段选择子(Segment Selector):描述逻辑地址所处的段

Offset:描述所在段内的偏移值

2.线性地址(Linear Address)

线性地址是由逻辑地址经过分段机制转换后得到的。

大致转换过程为:通过段选择子确定段的基地址,然后结合 Offset 得到线性地址。

3.物理地址(Physical Address)

物理地址就是指机器真实的物理内存地址,任何操作系统,最终都要通过物理地址来访问内存。若系统开启了分页机制,则在得到线性地址后需要通过分页机制转换后,才能得到物理地址。

简单来说,由逻辑地址得到物理地址过程如下:

逻辑地址 -> 分段机制转换 -> 线性地址 -> 分页机制转换 -> 物理地址

内存分配与回收

内存的分配与回收是操作系统的重要组成部分,需要解决的核心问题包括:

1.操作系统应保证应用程序的硬件无关性,硬件差异不能体现在应用程序上

2.内存划分的区域、分配粒度、最小单位,管理区分已使用和未使用的内存,回收等等

3.优化内存碎片,考虑整体机制的高效性

mmap

mmap(Memory Map) 可以将某个设备或文件映射到应用进程的内存空间中,这样应用程序访问这块内存,相当于直接对设备/文件读写,不再需要 read、write 等 IO 操作。

mmap 函数如下:

//映射成功返回0,否则返回错误码 void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset); addr:指文件/设备应该映射到进程空间的哪个起始地址 len:指被映射到进程空间的内存块大小 prot:指定被映射内存的访问权限,包括 PROT_READ(可读)、PROT_WRITE(可写) 等 flags:指定程序对内存块所做改变造成的影响,包括 MAP_SHARED(保存到文件) 等 fd:被映射到进程空间的文件描述符 offset:指定从文件的哪一部分开始映射 mmap 可用于跨进程通信,Linux Kernel 和 Android 中就频繁的用到了这个函数,比如 Android 的 Binder 驱动,下面分析 MemoryFile 原理时还会提到这个函数。

Copy on Write

  • Copy on Write(写时拷贝) 是指如果有多个调用者要请求同一资源,他们会获取到相同的指向这一资源的指针,直到某个调用者需修改资源时,系统才会复制一份副本给该调用者,而其他调用者仍使用最初的资源。
  • 如果调用者不需要修改资源,就不会建立副本,多个调用者共享读取同一份资源。
  • Linux 的 fork() 函数就是 Copy on Write 的,实际开销很小,主要是给子进程创建进程描述符等,并且推迟甚至免除了数据拷贝操作。比如 fork() 后子进程需立即调用 exec() 装载新程序到进程的内存空间,即不需要父进程的任何数据,这种情况 Copy on Write 技术就避免了不必要的数据拷贝,从而提升了运行速度。

以上为Android的开发内存管理解析;更多Android开发的技术进阶可参考传送直达↓↓↓ :link.juejin.cn/?target=htt…点击可前往。

文末

Android的内存管理方式:

Android采取了一种有别于Linux的进程管理策略,有别于Linux的在进程活动停止后就结束该进程,Android把这些进程都保留在内存中,直到系统需要更多内存为止。这些保留在内存中的进程通常情况下不会影响整体系统的运行速度,并且当用户再次激活这些进程时,提升了进程的启动速度。

那Android什么时候结束进程?结束哪个进程呢?之前普遍的认识是Android是依据一个名为LRU(last recently used 最近使用过的程序)列表,将程序进行排序,并结束最早的进程。其实安卓的内存管理机制是这样的,如下:

  1. 系统会对进程的重要性进行评估,并将重要性以“oom_adj”这个数值表示出来,赋予各个进程;(系统会根据“oom_adj”来判断需要结束哪些进程,一般来说,“oom_adj”的值越大,该进程被系统选中终止的可能就越高)
  2. 前台程序的“oom_adj”值为0,这意味着它不会被系统终止,一旦它不可访问后,会获得个更高的“oom_adj”,我们推测“oom_adj”的值是根据软件在LRU列表中的位置所决定的;
  3. Android不同于Linux,有一套自己独特的进程管理模块,这个模块有更强的可定制性,可根据“oom_adj”值的范围来决定进程管理策略,比如可以设定“当内存小于X时,结束“oom_adj”大于Y的进程”。这给了进程管理脚本的编写以更多的选择。