Android最佳性能实践(二)——分析内存的使用状况

转载自:http://blog.csdn.net/guolin_blog/article/details/42238633php

因为Android是为移动设备开发的操做系统,咱们在开发应用程序的时候应当始终把内存问题充分考虑在内。虽然Android系统拥有垃圾自动回收机制,但这并不意味着咱们就能够彻底忽略什么时候去分配或释放内存。即便咱们所有按照上一篇文章中给出的编程建议来去编写程序,仍是会颇有可能出现内存泄露或其它类型的内存问题。因此,惟一可以解决问题的办法,就是尝试去分析应用程序的内存使用状况,那么本篇文章就会教你们如何进行分析。若是你尚未看过前面一篇文章,建议先去阅读 Android最佳性能实践(一)——合理管理内存java

虽然说如今的手机内存都已经很是大了,可是咱们你们都知道,系统是不可能将全部的内存都分配给咱们的应用程序的。没错,每一个程序都会有可以使用的内存上限,这被称为堆大小(Heap Size)。不一样的手机,堆大小也不尽相同,随着如今硬件设备不断提升,堆大小也已经由Nexus One时的32MB,变成了Nexus 5时的192MB。若是你们想要知道本身手机的堆大小是多少,能够调用以下代码:正则表达式

  1. ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE); 
  2. int heapSize = manager.getMemoryClass(); 
ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
int heapSize = manager.getMemoryClass();

结果是以MB为单位进行返回的,咱们在开发应用程序时所使用的内存不能超出这个限制,不然就会出现OutOfMemoryError。所以,好比说咱们的程序中须要缓存一些数据,就能够根据堆大小来决定缓存数据的容量。编程

下面咱们来讨论一下Android的GC操做,GC全称是Garbage Collection,也就是所谓的垃圾回收。Android系统会在适当的时机触发GC操做,一旦进行GC操做,就会将一些再也不使用的对象进行回收。那么哪些对象会被认为是再也不使用,而且能够被回收的呢?咱们来看下面一张图:缓存

上图当中,每一个蓝色的圆圈就表明一个内存当中的对象,而圆圈之间的箭头就是它们的引用关系。这些对象有些是处于活动状态的,而有些就已经再也不被使用了。那么GC操做会从一个叫做Roots的对象开始检查,全部它能够访问到的对象就说明还在使用当中,应该进行保留,而其它的对象就表示已经再也不被使用了,以下图所示:并发

能够看到,目前全部黄色的对象仍然会被系统继续保留,而蓝色的对象就会在GC操做当中被系统回收掉了,这大概就是Android系统一次简单的GC流程。eclipse

那么何时会触发GC操做呢?这个一般都是由系统去决定的,咱们通常状况下都不须要主动通知系统应该去GC了(虽然咱们确实能够这么作,下面会讲到),可是咱们仍然能够去监听系统的GC过程,以此来分析咱们应用程序当前的内存状态。那么怎样才能去监听系统的GC过程呢?其实很是简单,系统每进行一次GC操做时,都会在LogCat中打印一条日志,咱们只要去分析这条日志就能够了,日志的基本格式以下所示:ide

  1. D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>,  <Pause_time> 
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>,  <Pause_time>

注意这里我仍然是以dalvik虚拟机来进行说明,art状况下打印的内容也是基本相似的。。工具

 

首先第一部分GC_Reason,这个是触发此次GC操做的缘由,通常状况下一共有如下几种触发GC操做的缘由:性能

  • GC_CONCURRENT:   当咱们应用程序的堆内存快要满的时候,系统会自动触发GC操做来释放内存。
  • GC_FOR_MALLOC:   当咱们的应用程序须要分配更多内存,但是现有内存已经不足的时候,系统会进行GC操做来释放内存。
  • GC_HPROF_DUMP_HEAP:   当生成HPROF文件的时候,系统会进行GC操做,关于HPROF文件咱们下面会讲到。
  • GC_EXPLICIT:   这种状况就是咱们刚才提到过的,主动通知系统去进行GC操做,好比调用System.gc()方法来通知系统。或者在DDMS中,经过工具按钮也是能够显式地告诉系统进行GC操做的。

接下来第二部分Amount_freed,表示系统经过此次GC操做释放了多少内存。

而后Heap_stats中会显示当前内存的空闲比例以及使用状况(活动对象所占内存 / 当前程序总内存)。

最后Pause_time表示此次GC操做致使应用程序暂停的时间。关于这个暂停的时间,Android在2.3的版本当中进行过一次优化,在2.3以前GC操做是不能并发进行的,也就是系统正在进行GC,那么应用程序就只能阻塞住等待GC结束。虽然说这个阻塞的过程并不会很长,也就是几百毫秒,可是用户在使用咱们的程序时仍是有可能会感受到略微的卡顿。而自2.3以后,GC操做改为了并发的方式进行,就是说GC的过程当中不会影响到应用程序的正常运行,可是在GC操做的开始和结束的时候会短暂阻塞一段时间,不过优化到这种程度,用户已是彻底没法察觉到了。

下面是一次GC操做在LogCat中打印的日志:

能够看出,和咱们上面所介绍的格式是彻底一致的,最后的暂停时间31ms+7ms,一次就是GC开始时的暂停时间,一次是结束时的暂停时间。另外能够根据进程id来区分这是哪一个程序中进行的GC操做,那么从上图就能够看出这条GC日志是属于24699这个程序的。

那么这是使用dalvik运行环境时所打印的GC日志,而自Android 4.4版本以后加入了art运行环境,在art中打印GC日志基本和dalvik是相同的,以下图所示:

相信没有什么难理解的地方吧,art中只是内容显示的格式有了稍许变化,打印的主体内容仍然是不变的。

好的,经过日志的方式咱们能够简单了解到系统的GC工做状况,可是若是咱们想要更加清楚地实时知晓当前应用程序的内存使用状况,只经过日志就有些力不从心了,咱们须要经过DDMS中提供的工具来实现。

打开DDMS界面,在左侧面板中选择你要观察的应用程序进程,而后点击Update Heap按钮,接着在右侧面板中点击Heap标签,以后不停地点击Cause GC按钮来实时地观察应用程序内存的使用状况便可,以下图所示:

接着继续操做咱们的应用程序,而后继续点击Cause GC按钮,若是你发现反复操做某一功能会致使应用程序内存持续增高而不会降低的话,那么就说明这里颇有可能发生内存泄漏了。

好了,讨论完了GC,接下来咱们讨论一下Android中内存泄漏的问题。你们须要知道的是,Android中的垃圾回收机制并不能防止内存泄漏的出现,致使内存泄漏最主要的缘由就是某些长存对象持有了一些其它应该被回收的对象的引用,致使垃圾回收器没法去回收掉这些对象,那也就出现内存泄漏了。好比说像Activity这样的系统组件,它又会包含不少的控件甚至是图片,若是它没法被垃圾回收器回收掉的话,那就算是比较严重的内存泄漏状况了。

下面咱们来模拟一种Activity内存泄漏的场景,内部类相信你们都有用过,若是咱们在一个类中又定义了一个非静态的内部类,那么这个内部类就会持有外部类的引用,以下所示:

  1. public class MainActivity extends ActionBarActivity { 
  2.  
  3.     @Override 
  4.     protected void onCreate(Bundle savedInstanceState) { 
  5.         super.onCreate(savedInstanceState); 
  6.         setContentView(R.layout.activity_main); 
  7.         LeakClass leakClass = new LeakClass(); 
  8.     } 
  9.  
  10.     class LeakClass { 
  11.  
  12.     } 
  13.     ...... 
public class MainActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        LeakClass leakClass = new LeakClass();
    }

    class LeakClass {

    }
    ......
}

目前来看,代码仍是没有问题的,由于虽然LeakClass这个内部类持有MainActivity的引用,可是只要它的存活时间不会长于MainActivity,就不会阻止MainActivity被垃圾回收器回收。那么如今咱们来将代码进行以下修改:

  1. public class MainActivity extends ActionBarActivity { 
  2.  
  3.     @Override 
  4.     protected void onCreate(Bundle savedInstanceState) { 
  5.         super.onCreate(savedInstanceState); 
  6.         setContentView(R.layout.activity_main); 
  7.         LeakClass leakClass = new LeakClass(); 
  8.         leakClass.start(); 
  9.     } 
  10.  
  11.     class LeakClass extends Thread { 
  12.  
  13.         @Override 
  14.         public void run() { 
  15.             while (true) { 
  16.                 try { 
  17.                     Thread.sleep(60 * 60 * 1000); 
  18.                 } catch (InterruptedException e) { 
  19.                     e.printStackTrace(); 
  20.                 } 
  21.             } 
  22.         } 
  23.     } 
  24.     ...... 
public class MainActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        LeakClass leakClass = new LeakClass();
        leakClass.start();
    }

    class LeakClass extends Thread {

        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(60 * 60 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    ......
}

这下就有点不太同样了,咱们让LeakClass继承自Thread,而且重写了run()方法,而后在MainActivity的onCreate()方法中去启动LeakClass这个线程。而LeakClass的run()方法中运行了一个死循环,也就是说这个线程永远都不会执行结束,那么LeakClass这个对象就一直不能获得释放,而且它持有的MainActivity也将没法获得释放,那么内存泄露就出现了。

如今咱们能够将程序运行起来,而后不断地旋转手机让程序在横屏和竖屏之间切换,由于每切换一次Activity都会经历一个从新建立的过程,而前面建立的Activity又没法获得回收,那么长时间操做下咱们的应用程序所占用的内存就会愈来愈高,最终出现OutOfMemoryError。

下面我贴出一张不断切换横竖屏时GC日志打印的结果图,以下所示:

能够看到,应用程序所占用的内存是在不断上升的。最可怕的是,这些内存一旦升上去了就永远不会再降下来,直到程序崩溃为止,由于这部分泄露的内存一直都没法被垃圾回收器回收掉。

那么经过上面学习的GC日志以及DDMS工具这两种方式,如今咱们已经能够比较轻松地发现应用程序中是否存在内存泄露的现象了。可是若是真的出现了内存泄露,咱们应该怎么定位到具体是哪里出的问题呢?这就须要借助一个内存分析工具了,叫作Eclipse Memory Analyzer(MAT)。咱们须要先将这个工具下载下来,下载地址是:http://eclipse.org/mat/downloads.php。这个工具分为Eclipse插件版和独立版两种,若是你是使用Eclipse开发的,那么可使用插件版MAT,很是方便。若是你是使用Android Studio开发的,那么就只能使用独立版的MAT了。

下载好了以后下面咱们开始学习如何去分析内存泄露的缘由,首先仍是进入到DDMS界面,而后在左侧面板选中咱们要观察的应用程序进程,接着点击Dump HPROF file按钮,以下图所示:

点击这个按钮以后须要等待一段时间,而后会生成一个HPROF文件,这个文件记录着咱们应用程序内部的全部数据。可是目前MAT仍是没法打开这个文件的,咱们还须要将这个HPROF文件从Dalvik格式转换成J2SE格式,使用hprof-conv命令就能够完成转换工做,以下所示:

  1. hprof-conv dump.hprof converted-dump.hprof 
hprof-conv dump.hprof converted-dump.hprof

hprof-conv命令文件存放于<Android Sdk>/platform-tools目录下面。另外若是你是使用的插件版的MAT,也能够直接在Eclipse中打开生成的HPROF文件,不用通过格式转换这一步。

好的,接下来咱们就能够来尝试使用MAT工具去分析内存泄漏的缘由了,这里须要提醒你们的是,MAT并不会准确地告诉咱们哪里发生了内存泄漏,而是会提供一大堆的数据和线索,咱们须要本身去分析这些数据来去判断究竟是不是真的发生了内存泄漏。那么如今运行MAT工具,而后选择打开转换事后的converted-dump.hprof文件,以下图所示:

MAT中提供了很是多的功能,这里咱们只要学习几个最经常使用的就能够了。上图最中央的那个饼状图展现了最大的几个对象所占内存的比例,这张图中提供的内容并很少,咱们能够忽略它。在这个饼状图的下方就有几个很是有用的工具了,咱们来学习一下。

Histogram能够列出内存中每一个对象的名字、数量以及大小。

Dominator Tree会将全部内存中的对象按大小进行排序,而且咱们能够分析对象之间的引用结构。

通常最经常使用的就是以上两个功能了,那么咱们先从Dominator Tree开始学起。

如今点击Dominator Tree,结果以下图所示:

这张图包含的信息很是多,我来带着你们一块儿解析一下。首先Retained Heap表示这个对象以及它所持有的其它引用(包括直接和间接)所占的总内存,所以从上图中看,前两行的Retained Heap是最大的,咱们分析内存泄漏时,内存最大的对象也是最应该去怀疑的。

另外你们应该能够注意到,在每一行的最左边都有一个文件型的图标,这些图标有的左下角带有一个红色的点,有的则没有。带有红点的对象就表示是能够被GC Roots访问到的,根据上面的讲解,能够被GC Root访问到的对象都是没法被回收的。那么这就说明全部带红色的对象都是泄漏的对象吗?固然不是,由于有些对象系统须要一直使用,原本就不该该被回收。咱们能够注意到,上图当中全部带红点的对象最右边都有写一个System Class,说明这是一个由系统管理的对象,并非由咱们本身建立并致使内存泄漏的对象。

那么上图中就没法看出内存泄漏的缘由了吗?确实,内存泄漏原本就不是这么容易找出的,咱们还须要进一步进行分析。上图当中,除了带有System Class的行以外,最大的就是第二行的Bitmap对象了,虽然Bitmap对象如今不能被GC Roots访问到,但不表明着Bitmap所持有的其它引用也不会被GC Roots访问到。如今咱们能够对着第二行点击右键 -> Path to GC Roots -> exclude weak references,为何选择exclude weak references呢?由于弱引用是不会阻止对象被垃圾回收器回收的,因此咱们这里直接把它排除掉,结果以下图所示:

能够看到,Bitmap对象通过层层引用以后,到了MainActivity$LeakClass这个对象,而后在图标的左下角有个红色的图标,就说明在这里能够被GC Roots访问到了,而且这是由咱们本身建立的Thread,并非System Class了,那么因为MainActivity$LeakClass能被GC Roots访问到致使不能被回收,致使它所持有的其它引用也没法被回收了,包括MainActivity,也包括MainActivity中所包含的图片。

经过这种方式,咱们就成功地将内存泄漏的缘由找出来了。这是Dominator Tree中比较经常使用的一种分析方式,即搜索大内存对象通向GC Roots的路径,由于内存占用越高的对象越值得怀疑。

接下来咱们再来学习一下Histogram的用法,回到Overview界面,点击Histogram,结果以下图所示:

这里是把当前应用程序中全部的对象的名字、数量和大小所有都列出来了,须要注意的是,这里的对象都是只有Shallow Heap而没有Retained Heap的,那么Shallow Heap又是什么意思呢?就是当前对象本身所占内存的大小,不包含引用关系的,好比说上图当中,byte[]对象的Shallow Heap最高,说明咱们应用程序中用了不少byte[]类型的数据,好比说图片。能够经过右键 -> List objects -> with incoming references来查看具体是谁在使用这些byte[]。

那么经过Histogram又怎么去分析内存泄漏的缘由呢?固然其实也能够用和Dominator Tree中比较类似的方式,即分析大内存的对象,好比上图中byte[]对象内存占用很高,咱们经过分析byte[],最终也是能找到内存泄漏所在的,可是这里我准备使用另一种更适合Histogram的方式。你们能够看到,Histogram中是能够显示对象的数量的,那么好比说咱们如今怀疑MainActivity中有可能存在内存泄漏,就能够在第一行的正则表达式框中搜索“MainActivity”,以下所示:

能够看到,这里将包含“MainActivity”字样的全部对象所有列出了出来,其中第一行就是MainActivity的实例。可是你们有没有注意到,当前内存中是有11个MainActivity的实例的,这太不正常了,经过状况下一个Activity应该只有一个实例才对。其实这些对象就是因为咱们刚才不断地横竖屏切换所产生的,由于横竖屏切换一次,Activity就会经历一个从新建立的过程,可是因为LeakClass的存在,以前的Activity又没法被系统回收,那么就出现这种一个Activity存在多个实例的状况了。

接下来对着MainActivity右键 -> List objects -> with incoming references查看具体MainActivity实例,以下图所示:

若是想要查看内存泄漏的具体缘由,能够对着任意一个MainActivity的实例右键 -> Path to GC Roots -> exclude weak references,结果以下图所示:

能够看到,咱们再次找到了内存泄漏的缘由,是由于MainActivity$LeakClass对象所致使的。

好了,这大概就是MAT工具最经常使用的一些用法了,固然这里还要提醒你们一句,工具是死的,人是活的,MAT也没有办法保证必定能够将内存泄漏的缘由找出来,仍是须要咱们对程序的代码有足够多的了解,知道有哪些对象是存活的,以及它们存活的缘由,而后再结合MAT给出的数据来进行具体的分析,这样才有可能把一些隐藏得很深的问题缘由给找出来。

那么今天也是介绍了挺多内容了,本篇文章的讲解就到这里,因为春节立刻就要到了,这也是今年的最后一篇文章,这里先给你们拜个早年,祝你们春节快乐。放假期间但愿你们能够和我同样,放下代码,好好休息一段时间,所以下篇文章将会在年后更新,介绍一些高性能编码的技巧,感兴趣的朋友请继续阅读 Android最佳性能实践(三)——高性能编码优化

相关文章
相关标签/搜索