Android内存优化实战篇

作者: 蛇发女妖 | 来源:发表于2021-05-23 19:09 被阅读0次

    声明:原创作品,转载请注明出处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内存优化实战篇

        本文链接:https://www.haomeiwen.com/subject/qujjdltx.html