Android开发内存管理
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工具查看内存占用情况(可参考:分析内存使用情况)。
内存占用指标
主要指标如下:
字段 | 指标含义 | 获取方式 |
---|---|---|
JavaHeap | Java内存占用 | Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory() |
JavaHeapUsedRate | Java内存占用率 | JavaHeap/Runtime.getRuntime().maxMemory() |
Graphics | 显存 | Debug.MemoryInfo.getMemoryStat("summary.graphics") |
VMSize | 虚拟内存 | /proc/进程pid/status |
TotalPss | 物理内存 | Debug.MemoryInfo.getTotalPss() |
DalvikPss | Java物理内存 | Debug.MemoryInfo.dalvikPss |
NativePss | Native物理内存 | 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开发的技术进阶可参考《Android核心技术手册》点击可前往。
文末
Android的内存管理方式:
Android采取了一种有别于Linux的进程管理策略,有别于Linux的在进程活动停止后就结束该进程,Android把这些进程都保留在内存中,直到系统需要更多内存为止。这些保留在内存中的进程通常情况下不会影响整体系统的运行速度,并且当用户再次激活这些进程时,提升了进程的启动速度。
那Android什么时候结束进程?结束哪个进程呢?之前普遍的认识是Android是依据一个名为LRU(last recently used 最近使用过的程序)列表,将程序进行排序,并结束最早的进程。其实安卓的内存管理机制是这样的,如下:
- 系统会对进程的重要性进行评估,并将重要性以“oom_adj”这个数值表示出来,赋予各个进程;(系统会根据“oom_adj”来判断需要结束哪些进程,一般来说,“oom_adj”的值越大,该进程被系统选中终止的可能就越高)
- 前台程序的“oom_adj”值为0,这意味着它不会被系统终止,一旦它不可访问后,会获得个更高的“oom_adj”,我们推测“oom_adj”的值是根据软件在LRU列表中的位置所决定的;
- Android不同于Linux,有一套自己独特的进程管理模块,这个模块有更强的可定制性,可根据“oom_adj”值的范围来决定进程管理策略,比如可以设定“当内存小于X时,结束“oom_adj”大于Y的进程”。这给了进程管理脚本的编写以更多的选择。