Android内存优化实战篇

声明:原创作品,转载请注明出处https://www.jianshu.com/p/87beb3b34771

作为一名Android开发者,对APP内存优化必须要有一定的了解,今天就总结下Android内存优化那些事。

什么是内存

首先看下这里的内存到底指的是什么?可以看下面这张图:


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

简单分析下,上图展示了2G内存设备的内存使用情况,当可用内存下降到某一阈值,即图中的kswapd threshold,这时kswapd会发挥作用,将缓存的内存转为使用内存。如果使用的内存越来越多,达到lmk threshold,那么低内存终止守护进程就会发挥作用,根据上面的优先级来杀死后台进程获取更多内存。

512M内存

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虚拟机是分代回收垃圾,在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的内存占用量。当然还有其他一些文件也会有影响,具体表格如下:


包体积对应用内存的影响

你可能感兴趣的:(Android内存优化实战篇)