声明:原创作品,转载请注明出处https://www.jianshu.com/p/87beb3b34771
作为一名Android开发者,对APP内存优化必须要有一定的了解,今天就总结下Android内存优化那些事。
什么是内存
首先看下这里的内存到底指的是什么?可以看下面这张图:
手机中主要的存储部分分两块RAM和ROM,RAM存储程序的运行时数据,设备关机就会清空,我们也称之为内存;ROM也就是磁盘,存放一些永久的数据。
上图我们看到这个RAM中还有一个zRAM分区,这个zRAM分区会在内存不足时发挥作用,稍后会说到。
到这里简单介绍了手机的内存是指什么,当我们不断打开APP时,手机的内存会被占的越来越多,而我们知道我们的手机总内存是一定的,那么当内存不够时手机会发生什么呢?
内存不够怎么办
当我们手机内存不足时,系统会有两套机制发挥作用。分别是内核交换守护进程
和低内存终止守护进程
内核交换守护进程(kswapd)
内核交换守护进程 (kswapd) 是 Linux 内核的一部分,用于将已使用内存转换为可用内存。当设备上的可用内存不足时,该守护进程将变为活动状态。Linux 内核设有可用内存上下限阈值。当可用内存降至下限阈值以下时,kswapd 开始回收内存。当可用内存达到上限阈值时,kswapd 停止回收内存。
kswapd可以删除不再被使用到的内存,如下图:
kswapd也可以对暂时不用的内存移到zRAM进行压缩,如果被用到时,会解压重新移到到RAM中,如下图:
低内存终止守护进程
很多时候,kswapd
不能为系统释放足够的内存。在这种情况下,系统会使用 onTrimMemory()
通知应用内存不足,应该减少其分配量。如果这还不够,内核会开始终止进程以释放内存。它会使用低内存终止守护进程 (LMK) 来执行此操作。
LMK 使用一个名为 oom_adj_score
的“内存不足”分值来确定正在运行的进程的优先级,以此决定要终止的进程。最高得分的进程最先被终止。后台应用最先被终止,系统进程最后被终止。下表列出了从高到低的 LMK 评分类别。评分最高的类别,即第一行中的项目将最先被终止:
[站外图片上传中...(image-c78455-1621768365460)]
以下是上表中各种类别的说明:
后台应用:之前运行过且当前不处于活动状态的应用。LMK 将首先从具有最高 oom_adj_score 的应用开始终止后台应用。
上一个应用:最近用过的后台应用。上一个应用比后台应用具有更高的优先级(得分更低),因为相比某个后台应用,用户更有可能切换到上一个应用。
主屏幕应用:这是启动器应用。终止该应用会使壁纸消失。
服务:服务由应用启动,可能包括同步或上传到云端。
可觉察的应用:用户可通过某种方式察觉到的非前台应用,例如运行一个显示小界面的搜索进程或听音乐。
前台应用:当前正在使用的应用。终止前台应用看起来就像是应用崩溃了,可能会向用户提示设备出了问题。
持久性(服务):这些是设备的核心服务,例如电话和 WLAN。
系统:系统进程。这些进程被终止后,手机可能看起来即将重新启动。
原生:系统使用的极低级别的进程(例如,kswapd)。
设备制造商可以更改 LMK 的行为。
设备对内存的影响
接下来可以看下不同设备,内存使用情况:
2G内存
简单分析下,上图展示了2G内存设备的内存使用情况,当可用内存下降到某一阈值,即图中的
kswapd threshold
,这时kswapd会发挥作用,将缓存的内存转为使用内存。如果使用的内存越来越多,达到lmk threshold
,那么低内存终止守护进程就会发挥作用,根据上面的优先级来杀死后台进程获取更多内存。
512M内存
上图显示了只有512M内存的设备内存使用情况,可以看到由于总内存很小,随着使用时长的增加,内存很快就到了低内存终止守护进程阈值线,基本打开一个应用就会杀死后台一个应用,使用体验很差。
接下来看下下面这张图:
上图显示了应用数据量和内存占用关系PSS(下面会讲到),一般数据量越多内存占用越多,红黄绿分别代表不同的手机设备,手机配置依次升高。
RSS、PSS和USS
接下来看几个内存概念:RSS、PSS和USS。在讲这几个概念前首先来了解下内存的占用量是如何计算的,内存是分页计算的,一个应用内存可能会占用好几页,如下图所示:
当然,也会存在几个应用共享内存页面,例如,Google Play 服务和某个游戏应用可能会共享位置信息服务,如下所示:
为了确定应用的内存占用量,可以使用以下任一指标:
- 常驻内存大小 (RSS):应用使用的共享和非共享页面的数量
- 按比例分摊的内存大小 (PSS):应用使用的非共享页面的数量加上共享页面的均匀分摊数量(例如,如果三个进程共享 3MB,则每个进程的 PSS 为 1MB)
- 独占内存大小 (USS):应用使用的非共享页面数量(不包括共享页面)
如果操作系统想要知道所有进程使用了多少内存,那么 PSS 非常有用,因为页面只会统计一次。计算 PSS 需要花很长时间,因为系统需要确定共享的页面以及共享页面的进程数量。RSS 不区分共享和非共享页面(因此计算起来更快),更适合跟踪内存分配量的变化。
我们可以通过adb来直观的看下某个APP的内存占用情况,adb命令如下:
adb shell dumpsys meminfo 应用完整包名
显示结果如下:
Pss Private Private SwapPss Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 6736 6680 4 114 22528 17971 4556
Dalvik Heap 0 0 0 0 5502 2751 2751
Stack 80 80 0 0
Ashmem 21 0 20 0
Gfx dev 280 280 0 0
Other dev 2 0 0 0
.so mmap 8081 196 5848 11
.apk mmap 319 0 72 0
.ttf mmap 55 0 28 0
.dex mmap 6261 16 5180 0
.oat mmap 75 0 20 0
.art mmap 7913 7180 248 69
Other mmap 68 4 0 0
EGL mtrack 18496 18496 0 0
GL mtrack 3348 3348 0 0
Unknown 7139 7064 28 40
TOTAL 59108 43344 11448 234 28030 20722 7307
App Summary
Pss(KB)
------
Java Heap: 7428
Native Heap: 6680
Code: 11360
Stack: 80
Graphics: 22124
Private Other: 7120
System: 4316
TOTAL: 59108 TOTAL SWAP PSS: 234
Objects
Views: 16 ViewRootImpl: 1
AppContexts: 5 Activities: 1
Assets: 7 AssetManagers: 0
Local Binders: 16 Proxy Binders: 31
Parcel memory: 4 Parcel count: 17
Death Recipients: 2 OpenSSL Sockets: 0
WebViews: 0
SQL
MEMORY_USED: 0
PAGECACHE_OVERFLOW: 0 MALLOC_SIZE: 0
我们重点看下``App Summary`部分,这部分展示了APP内存(PSS)总占有量,以及各个内存占用明细:
Java:从 Java 或 Kotlin 代码分配的对象的内存。
Native:从 C 或 C++ 代码分配的对象的内存。即使您的应用中不使用 C++,您也可能会看到此处使用了一些原生内存,因为即使您编写的代码采用 Java 或 Kotlin 语言,Android 框架仍使用原生内存代表您处理各种任务,如处理图像资源和其他图形。
Graphics:图形缓冲区队列为向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。(请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。)
Stack:您的应用中的原生堆栈和 Java 堆栈使用的内存。这通常与您的应用运行多少线程有关。
Code:您的应用用于处理代码和资源(如 dex 字节码、经过优化或编译的 dex 代码、.so 库和字体)的内存。
Others:您的应用使用的系统不确定如何分类的内存。
System:系统内存。
减小应用内存占用
接下来看下如何减小我们APP内存的占用,这里有两种方式:
- 减小Java Heap内存
- 减小应用包体积
减小Java Heap内存
这里你可能会比较好奇,上面我们可以看到内存有很多部分组成,为什么这里偏偏只减小Java Heap的内存就可以。主要是其他内存分析比较困难,Android官方也不太推荐分析除了Java Heap之外的内存,主要有以下几个原因:
- 工具不能很好支持
- 用户接口不友好
- 需要非常深的系统底层知识
- root设备,或者重新编译源码
- 很多内存无法控制
下面这张是Android系统架构图,从图中也可以看出,系统底层都是直接或者间接和应用层有关,就是说底层占用的内存和Java Heap内存都是有关联的,优化了Java Heap内存也就间接优化了其他内存部分。
接下来具体看下如何减小Java Heap内存,首先来了解下什么是Java Heap,Java Heap即Java堆,也是虚拟机对象主要存放的地方,一般也是垃圾回收的重点位置。当然Java 虚拟机除了Java堆外还有其他分区:程序计数器、方法区、虚拟机栈和本地方法栈,这里由于篇幅有限这几个分区作用就不过多介绍了,如下:
由于Java虚拟机是分代回收垃圾,在Java 7中Java Heap被分为新生代、老年代和永久代,新生代又分为Eden、From Survivor和To Survivor区,他们的大小比例是8:1:1,来说下它的工作机制,首先新生成的对象会被分配到Eden区,当Eden区内存满了时会发生一次小型的垃圾回收,回收时会将Eden区中存活的对象复制到From Survivor区中,然后把Eden区清空。当From Survivor也满了,就会把Eden区和From Survivor中存活的对象复制到To Survivor区中,然后清空Eden和From Survivor区,接着会把From Survivor区和To Survivor区做交换,保持To Survivor区中的对象为空,就这样不断重复,当To Survivor中的空间也满了,无法存在Eden和From Survivor区中的对象时,就会把他们存入老年代。另外当某个对象在Survivor区中经历一次GC而存活下来,那么他的年龄就会加一,默认情况下当超过15岁时就会被丢入老年代中。当老年代空间也满了虚拟机会发生一次Full GC,一般大对象会直接放入老年代,比如大数组这种需要连续存储空间的。永久代一般存放静态对象比如class文件,静态方法、常量等。永久代对垃圾回收没有显著的影响,主要回收无用类和废弃常量。在Java 8中移除了永久代改为了元空间(Metaspace),因此不会再出现“java.lang.OutOfMemoryError: PermGen error”错误。
对象存活检测
上面我们了解了有关Java Heap内存分配和回收相关原理,接下来你可能会有疑问,Java虚拟机是如何判断一个对象是否是存活状态。换句话说一个对象在什么情况下该被回收呢,这个问题自然而然就是对象不再用到的时候就可以被回收,而这个“不再被用到”就是这个对象不被任何一个对象所引用的意思。这里主要有两种方式来判断一个对象有没有被其他对象引用:引用计数法
和根搜索算法
。
引用计数法
引用计数法顾名思义,就是在这个对象里有一个计数的量,当这个对象被其他对象引用时这个计数量就会加1,比如被2两个对象引用就是2,并且当其他对象不再引用这个对象时,这个计数量会相应减1。当这个计数量为0时就代表这个对象不被任何对象引用,那么虚拟机在下次垃圾回收时就会回收这个对象。不过这种方式有一个弊端,就是互相引用,也就是有两个对象互相引用对方,那么他们的计数值都是1,然而这两个对象除了互相引用外就没有其他对象引用了,其实针对这种情况,这两个对象都应该被回收的,但是他们的计数值都是1,导致虚拟机无法对他们进行回收。
为了解决这个问题,虚拟机使用了另一种方式也就是 根搜索算法
。
根搜索算法
这种算法的基本思路:
(1)通过一系列名为“GC Roots”的对象作为起始点,寻找对应的引用节点。
(2)找到这些引用节点后,从这些节点开始向下继续寻找它们的引用节点。
(3)重复(2)。
(4)搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。
在java语言中,可作为GCRoot的对象包括以下几种:
java虚拟机栈(栈帧中的本地变量表)中的引用的对象。
方法区中的类静态属性引用的对象。
方法区中的常量引用的对象。
-
本地方法栈中JNI本地方法的引用对象。
内存泄漏和内存溢出
了解了根搜索算法后,我们知道当一个对象不在引用链上的时候就可以对它进行回收了,但是可能存在一种情况就是假如我们想回收一个对象,但是由于某些原因导致这个对象一直在引用链上,这样这个对象就一直无法被回收了。我们称这种现象为内存泄漏
。当我们的应用很多地方存在这种内存泄漏现象时,随着应用的使用,内存占用会越来越高,当内存使用量达到系统规定的上限,APP就会报一个OutOfMemery的异常,我们称这种现象为内存溢出。
那么什么情况会导致内存泄漏呢,这里简单罗列下:
- 集合类的不规范使用
- static修饰的成员变量
- 非静态内部类或者匿名内部类
- 资源对象使用后未关闭
上面只是简单的列了下可能会导致内存泄漏的情况,更加详细的原因可以参看其他相关内存泄漏的文章。
知道内存泄漏的现象及其后果后,接下来我们就应该去解决我们应用中的内存泄漏问题,但是导致内存泄漏的代码往往很隐蔽我们一时是很难排查的,这是就可以借助一些内存分析工具。比如可以用Android Studio自带的性能分析工具Profiler
,或者也可以在应用工程中集成一个内存分析的三方库LeakCanary
,这个库具体的使用方式可以上他们的官网了解LeakCanary
减小包体积
上面我们用大量篇幅来分析了减小Java Heap大小来优化内存,其实应用的安装包大小对内存也是由影响的,比如如果安装包中有很多图片等资源的话,这会增加Java Heap、Native Heap和Graphics的内存占用量。当然还有其他一些文件也会有影响,具体表格如下: