Android 内存优化

5,296 阅读14分钟
原文链接: www.jianshu.com

本篇主要讲解android内存性能优化之检测方案。内存性能主要包括内存泄漏, 内存抖动, 内存持续增长(但GC后会下降), 内存占用过大等问题。

Android内存分析方向:

  • Java 内存分析
    • Java中的内存泄露主要特征:可达,无用
    • 无用指的是创建了但是不再使用之后没有释放
    • 能重用但是却创建了新的对象进行处理
  • Native 内存分析
    • 堆中new的对象未释放
    • 对象引用导致无法释放
  • JS 中内存分析

本篇主要讲解Java内存分析。

一. 日志分析

查看日志中是否有频繁的GC。通常通过log,我们可以初步定为大部分内存等问题。

二. 常见内存泄漏查找

Context 泄漏, 主要为Activity 传递泄漏, context 未使用applciationConext 在单例创建时。
Handler 泄漏 , handler中持有view ,context 等做耗时操作。
Cursor 泄漏 , cursor未关闭
register 未 unregister
Bitmap
adapter 未使用convertView
不良代码等

三. 命令dumpsys meminfo分析

adb shell dumpsys meminfo com.i2finance.shexpress 
Applications Memory Usage (kB):
Uptime: 142597122 Realtime: 236611715

** MEMINFO in pid 25126 [com.i2finance.shexpress] **
                   Pss  Private  Private  Swapped     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------
  Native Heap    61111    61084        0        0    69888    64350     5537
  Dalvik Heap    49451    49316        0        0    71737    67348     4389
 Dalvik Other     3333     3332        0        0                           
        Stack      960      960        0        0                           
       Cursor       12       12        0        0                           
       Ashmem      130       88        0        0                           
      Gfx dev    23780    23780        0        0                           
    Other dev        4        0        4        0                           
     .so mmap     4373      396     3108        0                           
    .jar mmap       80        0       76        0                           
    .apk mmap    17986       64    17580        0                           
    .ttf mmap       96        0       80        0                           
    .dex mmap    15729       16    14244        0                           
    .oat mmap     2378        0      624        0                           
    .art mmap     1859     1624        8        0                           
   Other mmap     2039       12     1308        0                           
      Unknown    84240    84240        0        0                           
        TOTAL   267561   224924    37032        0   141625   131698     9926

 App Summary
                       Pss(KB)
                        ------
           Java Heap:    50948
         Native Heap:    61084
                Code:    36188
               Stack:      960
            Graphics:    23780
       Private Other:    88996
              System:     5605

               TOTAL:   267561      TOTAL SWAP (KB):        0

 Objects
               Views:      429         ViewRootImpl:        2
         AppContexts:        2           Activities:        1
              Assets:        7        AssetManagers:        3
       Local Binders:       37        Proxy Binders:       31
       Parcel memory:       26         Parcel count:       65
    Death Recipients:        2      OpenSSL Sockets:        6

 SQL
         MEMORY_USED:      567
  PAGECACHE_OVERFLOW:      157          MALLOC_SIZE:       62

 DATABASES
      pgsz     dbsz   Lookaside(b)          cache  Dbname
         4       24             45         5/24/6  /data/user/0/com.i2finance.shexpress/databases/pa_data_cache.db
         4       28             19         1/16/2  /data/user/0/com.i2finance.shexpress/databases/mpush.db
         4       60             37         5/18/6  /data/user/0/com.i2finance.shexpress/databases/fstandard.db
         4       60             91      466/22/11  /data/user/0/com.i2finance.shexpress/databases/fstandard.db (2)
         4       24             40         5/24/6  /data/user/0/com.i2finance.shexpress/databases/pa_data_cache.db

 Asset Allocations
    zip:/data/user/0/com.i2finance.shexpress/files/paanydoor_resource_3.5.0.36.jar:/resources.arsc: 67K

meminfo的信息中各字段都是什么含义, 要理解各字段含义,我们才好进行内存的优化。

首先了解两个概念:

  • 私有内存(Dirty and Clean):
    进程独占内存。也就是进程销毁时可以回收的内存容量。通常private Dirty内存是最重要的部分,因为只被自己进程使用。Dirty内存是已经被修改的内存页,因此必须常驻内存(因为没有swap);Clean内存是已经映射持久文件使用的内存页(例如正在被执行的代码),因此一段时间不使用的话就可以置换出去。

  • 实际使用内存(PSS):
    将跨进程共享页也加入进来, 进行按比例计算PSS。这样能够比较准确的表示进程占用的实际物理内存。

通常我们需要关注PSS TOTALPrivate Dirty .

  • Dalvik Heap
    dalvik虚拟机分配的内存。PSS Total包含所有Zygote分配使用的内存,共享跨进程加权。PrivateDirty 是应用独占内存大小,包含独自分配的部分和应用进程从Zygote复制时被修改的Zygote分配的内存页。 HeapAlloc 是Dalvik堆和本地堆分配使用的大小,它的值比Pss Total和Private Dirty大,因为进程是从Zygote中复制分裂出来的,包含了进程共享的分配部分。
  • .so mmap & .dex mmap ... mmap 映射本地或虚拟机代码到使用的内存中。
  • Unknown 无法归类的其他项。主要包括大部分的本地分配。
  • Native Heap native代码申请的内存, 堆和栈,及静态代码块等。
  • TOTAL进程总使用的实际内存。
  • Objects 中显示持有对象的个数。这些数据也是分析内存泄漏的重要数据。如activity等。

四. Heap Viewer

Heap Viewer 能做什么?

  • 事实查看内存分配情况和空闲内存大小
  • 发现memory Leaks

AS中点击机器人图标打开Android Device Mointor, 如下:
选中进程进行Heap 分析,点击update heap, 查看右侧的heap标签页


Paste_Image.png

Heap视图显示了堆内存使用的情况,每次垃圾回收都会更新,要查看更新情况, 点击Cause GC即可。
下面的内容显示的是分配的内存,按照类型分类:


Paste_Image.png

如何检查内存泄漏

我们需要在执行查看内存是否有泄漏的用例之前和之后执行GC,即手动点击Cause GC,观察allocated大小,查看内存是否在一个稳定的数值,多次操作,只要内存稳定,即没有内存泄漏, 如果不断变大,即表示有内存泄漏。
该工具也可以用来查看是否会发生内存抖动

五. 生成Dump

分析内存泄漏,我们需要生成相关的内存Dump,那么我们如何生成dump文件来进行分析。

目前有两种方式:

  • 打开Android Device Monitor
    点击dump Hprof file

    Paste_Image.png

    会生成一份Hprof文件,但该hprof文件我们无法打开,需要进行转换之后才能用MAT工具打开,可以使用命令

hprof-conv com.i2finance.shexpress.hprof xxx.hprof 转换生成可用的hprof文件。

  • 使用Android Studio
    打开Android Studio 的Android Monitor , 选中Memory 标签:

    Paste_Image.png

    点击Dump Java Heap 即可生成对应的hprof文件,在侧边栏中打开Captures文件,选中文件点击右键,export 出标准的hprof文件。

    Paste_Image.png

六. Heap Snapshot

获取Java 堆内存详细信息,可以分析出内存泄漏的问题。
打开Android Studio 的Android Monitor , 选中Memory 标签, 点击Dump heap,生成hprof文件。AS会自动打开该文件,见下图,但是该功能有点弱,建议还是转换成mat可识别的hprof,使用mat进行分析。


Paste_Image.png

七. 使用LeakCanary

使用内存检测软件leakCanary

  • 添加依赖包

build.gradle 中增加依赖

debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
  • 开启leakCanary

Applciationoncreate中增加语句

LeakCanary.install(this);

查看leak详情。
当发生内存泄漏时,会生成leak 报告, 报告中会详细写明具体发现内存泄漏的语句。
其原理,可以自行上网搜索查看一下。

八. Allocation Tracker(DeviceMonitor)

Allocation Tracker 能够追踪内存分配信息, 按照顺序排列,这样我们能够清晰的看出来每一个内存对象是怎么一步一步的分配出来的。比如内存抖动的可疑点,我们可以通过查看其内存分配轨迹来查看段时间内有多少相同或相似对象被创建,进而找到问题发生的代码。

操作步骤:

  1. 进入追踪界面
  2. 点击start Tracking 按钮,开始跟踪内存分配轨迹
  3. 操作用例
  4. 点击Get Allocations,获取内存分配轨迹。

    Paste_Image.png

如上图,上行app 从后台切换道前台时会调用onResume,可以追踪到最后创建了多个Configuration对象。

上图中,Allcated class 表示创建的类型,第一个Allocated in 表示在哪个类中, 第二个Allocated in 表示在哪个方法中。

查看源代码如下:

public Resources getResources() {
    Resources res = super.getResources();
    Configuration config = new Configuration();
    config.setToDefaults();
    try {
        res.updateConfiguration(config, res.getDisplayMetrics());
    }catch (Exception e){
        e.printStackTrace();
    }
    return res;
}

九. Allocation Tracker(AndroidMonitor)

功能同Allocation Tracker(Andorid Device) , 但是展示更酷炫,更全面。
打开Android Monitor, 选中Memory 标签 , 点击图标


Paste_Image.png


, 进行内存tracker, 再次点击结束tracker。As会自动打开tracker文件。
下面我们详细看一下这个面板:


Paste_Image.png

AS给我们提供了多种展示方式

  • by Method :用方法来分类我们的内存分配
  • by Allocator : 用内存分配器来分类我们的内存分配
    点开每一项,都能够查看到方法调用栈, 点击右键可以跳转到源码。

AS 还为我们提供了统计,点击饼状图标按钮即可。

分为两种展示形式,有柱状图和轮胎图,分配比例可选分配次数和占用内存大小:

  • Sunburst
    轮胎图是以轮胎为起点,最外层是内存实际分配的对象,每一个同心圆可能被分配为多个部分,代表不同的子孙,每一个同心圆代表他的一个后代。双击同心圆中某一个分割部分,会变成以你点击的那一代为圆心再向外展开,如果想回到初始状态,双击圆心即可。
    下图为 Sunburst + by Method

    Paste_Image.png

下图为Sunburst + by Allocator
一个内存的完整路径


Paste_Image.png


比如上行的首页中trace 的数据, 我们看下我们自己的包:


Paste_Image.png

会发现,最外围有很多PageScrollEvent 对象, 我们去看下源代码:

代码如下, 我们发现自动loop的viewpager 每次滑动都会创建多个PageScorllEvent 对象。这样也就对应上面这幅图了。

private class PageChangeListener implements OnPageChangeListener {
    private PageChangeListener() {
    }

    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if (isLoop) {
            int count = getAdapter().getCount();
            if (position < 1 || position > count - 2) {
                return;
            }
        }
        LoopViewPager.this.mEventDispatcher.dispatchEvent(new PageScrollEvent(LoopViewPager.this.getId(), SystemClock.uptimeMillis(), position, positionOffset));
    }
  • Layout
    柱状图是以左边为起点,从左到右的顺序是某个的堆栈信息顺序,纵坐标上的宽度是以Count/Size 的大小决定的。其内容和轮胎图是一致的。
    下图为Layout + by Method

Paste_Image.png

十. MAT

MAT工具全称为Memory Analyzer Tool,一款详细分析Java堆内存的工具,该工具非常强大,为了使用该工具,我们需要hprof文件.

HPROF文件存储的是特定时间点,java进程的内存快照。有不同的格式来存储这些数据,总的来说包含了快照被触发时java对象和类在heap中的情况。由于快照只是一瞬间的事情,所以heap dump中无法包含一个对象在何时、何地(哪个方法中)被分配这样的信息。

几个关键概念

  • Histogram:列出内存中的对象,对象的个数以及大小
  • Dominator Tree:列出最大的对象以及其依赖存活的Object (大小是以Retained Heap为标准排序的)
  • Top Consumers : 通过图形列出最大的object
  • Duplicate Class:通过MAT自动分析泄漏的原因
  • Shallow heap : 对象本身占用内存的大小,不包含其引用的对象。
    (常规对象(非数组)的Shallow size有其成员变量的数量和类型决定。数组的shallow size有数组元素的类型(对象类型、基本类型)和数组长度决定. 因为不像c++的对象本身可以存放大量内存,java的对象成员都是些引用。真正的内存都在堆上,看起来是一堆原生的byte[], char[], int[],所以我们如果只看对象本身的内存,那么数量都很小。所以我们看到Histogram图是以Shallow size进行排序的,排在第一位第二位的是byte,char 。)
  • Retained Heap : 它表示如果一个对象被释放掉,那会因为该对象的释放而减少引用进而被释放的所有的对象(包括被递归释放的)所占用的heap大小。
    (于是,如果一个对象的某个成员new了一大块int数组,那这个int数组也可以计算到这个对象中。相对于shallow heap,Retained heap可以更精确的反映一个对象实际占用的大小(因为如果该对象释放,retained heap都可以被释放)。)
  • outgoing references :表示该对象的出节点(被该对象引用的对象)。
  • incoming references :表示该对象的入节点(引用到该对象的对象)。
  • GC Root: GC发现通过任何reference chain(引用链)无法访问某个对象的时候,该对象即被回收。所以JVM就是GC Roots。
  • Unreachable指的是可以被垃圾回收器回收的对象,但是由于没有GC发生,所以没有释放,这时抓的内存使用中的Unreachable就是这些对象。

1. 预览信息

打开dump 文件,通常我们需要关注一下几个重要信息, 内存占用饼图,Actions部分的Histogram, Top Consumers.
我们打开Top Consumers,会生成一个报告,我们可以Biggets Objects overview, 能够看到主要内存占用者


Paste_Image.png

点击下面的biggest Objects 可以查看具体的地址。
还有Biggest Top Level Dominator Classes , 可以看到主要占用内存的都是些什么东东。

2. dump分析

2.1 Histogram

MAT中Histogram的主要作用是查看一个instance的数量,一般用来查看自己创建的类的实例的个数。 可以分不同维度来查看对象的Dominator Tree视图,Group by class、Group by class loader、Group by package 和Histogram类似,时间久了,通过多次对比也可以把溢出对象找出来。 Histogram 中可以分Group,Thread 区分信息。 通常为:选中某一项-> show objects and class -> by incoming reference->merge shortest path to gc root -> exclude weadk reference
等流程来查看具体情况。

可以在上面过滤相关包名,查看到具体类型, 关注objects个数, 表示内存dump 中有多少个相关类型对象, 比如不改存在的 对象存在了,或者有的对象内存中有太多的份数, 这样就可以进行一个全面分析。

也可以选择Group by package ,这样方便根据package来进行分析。


Paste_Image.png

也可以选择thread来进行分析, 这样查看占用内存最多的线程,这些线程可能为有内存问题的线程。

点击右键常用的几个选项:

  • List Objects -> with incoming references 查看这个对象被哪些外部对象引用
  • List Objects-> with outcoming references 查看这个对象持有的外部对象引用
  • Path to GC Roots -> exclude ... references 查看这个对象的GC Root,不包含xxx引用,剩下的基本就是强引用了。因为只有强引用一直存在,gc就一直无法回收该对象,从而也就出现内存泄露。
  • Merge shortest path to GC root 找到从GC根结点到一个对象或者一组对象的共同路径。从这里可以查看到对象的引用关系。
2.2 Debug Bitmap

图片一直是内存占用的一个大头,也是引起内存泄露,OOM的常客。所以对图片的分析是需要非常了解,这样才能更好的优化项目。注意:图片在内存中占用的大小:ARGB_8888 类型的图片 为 内存中图片宽度*内存中图片高度*4, 此处需要注意原始图片宽高和内存图片宽高不一致,包括拉伸和压缩,尤其是图片位置放错,比如1080p设备,xxxhdpi下面没有图片,会去别的目录下寻找图片,此时将会对图片拉伸。
下面我们来看一下图片的处理。通常dump信息中图片表现为两种类型,Bitmap, byte[]。我们需要知道该图片是哪张图片,这样才能好优化相关的图片代码。

  • Bitmap类型
    在mat中通常能够看到bitmap类型,占用了大量的内存,如下面这张图片,在内存中占用2M。 我们可以打开,查看mBuffer变量。

    Paste_Image.png

选中mBuffer-> 右键选中Copy-> 选择Save Value To File -> 生成一个xxx.data 文件。

  • Byte[] 类型
    如下,查看byte的 in comming, 即可看到它是一个bitmap,此时如下图,我们可以直接将该byte数据写入xxx.data 文件。

    Paste_Image.png

下一步是选中对应的bitmap,打开Inspector 窗口,查看bitmap的尺寸,并且使用GIMP工具(可以安装一个,开源的)打开刚才的data文件,图像类型选择RGB Alpha, 宽度和高度填入图像的宽高,打开即可。


Paste_Image.png

Paste_Image.png
2.3 堆对比

通常为了分析内存是否泄露,内存是否持续增长但没有释放等问题,我们需要dump两次来进行内存堆的对比。

打开两个或多个dump文件,打开Navigation History视图,点击Historgam,选择Add to Comp are Basket,最后选中Compare the Result 。


Paste_Image.png

在对比结果中,主要分析类型或者对象的数量是否有变化, 内存是否有变化。

通过以上手段,我们可以定位到大部分内存问题。