Android合理管理内存

有很多朋友都问过我,怎样才能写出高性能的应用程序,如何避免程序出现OOM,或者当程序内存占用太高的时候该怎么样去排查。确实,一个优秀的应用程序,不只仅要功能完成得好,性能问题也应该处理得恰到好处。为此,我也是阅读了很多Android官方给出的高性能编程建议,那么从本篇文章开始,我就准备开始写一个全新系列的博文,来把这些建议进行整理和分析,帮助你们可以写出更加出色的应用程序。php

内存(RAM)对于任何一个软件开发环境都是种很是珍贵的资源,而对于移动操做系统来说的话,则会显得更加珍贵,由于手机的硬件条件相对于PC毕竟是比较落后的。尽管Android系统的虚拟机拥有自动回收垃圾的机制,但这并不表明咱们就能够忽视应该在何时分配和释放内存。java

为了使垃圾回收器能够正常释放程序所占用的内存,在编写代码的时候就必定要注意尽可能避免出现内存泄漏的状况(一般都是因为全局成员变量持有对象引用所致使的),而且在适当的时候去释放对象引用。对于大多数的应用程序而言,后面其它的事情就能够都交给垃圾回收器去完成了,若是一个对象的引用再也不被其它对象所持有,那么系统就会将这个对象所分配的内存进行回收。android

咱们在开发软件的时候应当自始至终都把内存的问题充分考虑进去,这样的话才能开发出更加高性能的软件。而内存问题也并非无规律可行的,Android系统给咱们提出了不少内存优化的建议技巧,只要按照这些技巧来编写程序,就可让咱们的程序在内存性能发面表现得至关不错,下面咱们就来一一学习一下这些技巧。程序员

节制地使用Service

若是应用程序当中须要使用Service来执行后台任务的话,请必定要注意只有当任务正在执行的时候才应该让Service运行起来。另外,当任务执行完以后去中止Service的时候,要当心Service中止失败致使内存泄漏的状况。正则表达式

当咱们启动一个Service时,系统会倾向于将这个Service所依赖的进程进行保留,这样就会致使这个进程变得很是消耗内存。而且,系统能够在LRU cache当中缓存的进程数量也会减小,致使切换应用程序的时候耗费更多性能。严重的话,甚至有可能会致使崩溃,由于系统在内存很是吃紧的时候可能已没法维护全部正在运行的Service所依赖的进程了。算法

为了可以控制Service的生命周期,Android官方推荐的最佳解决方案就是使用IntentService,这种Service的最大特色就是当后台任务执行结束后会自动中止,从而极大程度上避免了Service内存泄漏的可能性。编程

让一个Service在后台一直保持运行,即便它并不执行任何工做,这是编写Android程序时最糟糕的作法之一。因此Android官方极度建议开发人员们不要过于贪婪,让Service在后台一直运行,这不只可能会致使手机和程序的性能很是低下,并且被用户发现了以后也有可能直接致使咱们的软件被卸载(我我的就会这么作)。数组

当界面不可见时释放内存

当用户打开了另一个程序,咱们的程序界面已经再也不可见的时候,咱们应当将全部和界面相关的资源进行释放。在这种场景下释放资源可让系统缓存后台进程的能力显著增长,所以也会让用户体验变得更好。缓存

那么咱们如何才能知道程序界面是否是已经不可见了呢?其实很简单,只须要在Activity中重写onTrimMemory()方法,而后在这个方法中监听TRIM_MEMORY_UI_HIDDEN这个级别,一旦触发了以后就说明用户已经离开了咱们的程序,那么此时就能够进行资源释放操做了,以下所示:网络

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1. @Override  

  2. public void onTrimMemory(int level) {  

  3.     super.onTrimMemory(level);  

  4.     switch (level) {  

  5.     case TRIM_MEMORY_UI_HIDDEN:  

  6.         // 进行资源释放操做  

  7.         break;  

  8.     }  

  9. }  

注意onTrimMemory()方法中的TRIM_MEMORY_UI_HIDDEN回调只有当咱们程序中的全部UI组件所有不可见的时候才会触发,这和onStop()方法仍是有很大区别的,由于onStop()方法只是当一个Activity彻底不可见的时候就会调用,好比说用户打开了咱们程序中的另外一个Activity。所以,咱们能够在onStop()方法中去释放一些Activity相关的资源,好比说取消网络链接或者注销广播接收器等,可是像UI相关的资源应该一直要等到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)这个回调以后才去释放,这样能够保证若是用户只是从咱们程序的一个Activity回到了另一个Activity,界面相关的资源都不须要从新加载,从而提高响应速度。

当内存紧张时释放内存

除了刚才讲的TRIM_MEMORY_UI_HIDDEN这个回调,onTrimMemory()方法还有不少种其它类型的回调,能够在手机内存下降的时候及时通知咱们。咱们应该根据回调中传入的级别来去决定如何释放应用程序的资源:

  • TRIM_MEMORY_RUNNING_MODERATE    表示应用程序正常运行,而且不会被杀掉。可是目前手机的内存已经有点低了,系统可能会开始根据LRU缓存规则来去杀死进程了。

  • TRIM_MEMORY_RUNNING_LOW    表示应用程序正常运行,而且不会被杀掉。可是目前手机的内存已经很是低了,咱们应该去释放掉一些没必要要的资源以提高系统的性能,同时这也会直接影响到咱们应用程序的性能。

  • TRIM_MEMORY_RUNNING_CRITICAL    表示应用程序仍然正常运行,可是系统已经根据LRU缓存规则杀掉了大部分缓存的进程了。这个时候咱们应当尽量地去释听任何没必要要的资源,否则的话系统可能会继续杀掉全部缓存中的进程,而且开始杀掉一些原本应当保持运行的进程,好比说后台运行的服务。

以上是当咱们的应用程序正在运行时的回调,那么若是咱们的程序目前是被缓存的,则会收到如下几种类型的回调:

  • TRIM_MEMORY_BACKGROUND    表示手机目前内存已经很低了,系统准备开始根据LRU缓存来清理进程。这个时候咱们的程序在LRU缓存列表的最近位置,是不太可能被清理掉的,但这时去释放掉一些比较容易恢复的资源可以让手机的内存变得比较充足,从而让咱们的程序更长时间地保留在缓存当中,这样当用户返回咱们的程序时会感受很是顺畅,而不是经历了一次从新启动的过程。

  • TRIM_MEMORY_MODERATE    表示手机目前内存已经很低了,而且咱们的程序处于LRU缓存列表的中间位置,若是手机内存还得不到进一步释放的话,那么咱们的程序就有被系统杀掉的风险了。

  • TRIM_MEMORY_COMPLETE    表示手机目前内存已经很低了,而且咱们的程序处于LRU缓存列表的最边缘位置,系统会最优先考虑杀掉咱们的应用程序,在这个时候应当尽量地把一切能够释放的东西都进行释放。

避免在Bitmap上浪费内存

当咱们读取一个Bitmap图片的时候,有一点必定要注意,就是千万不要去加载不须要的分辨率。在一个很小的ImageView上显示一张高分辨率的图片不会带来任何视觉上的好处,但却会占用咱们至关多宝贵的内存。须要仅记的一点是,将一张图片解析成一个Bitmap对象时所占用的内存并非这个图片在硬盘中的大小,可能一张图片只有100k你以为它并不大,可是读取到内存当中是按照像素点来算的,好比这张图片是1500*1000像素,使用的ARGB_8888颜色类型,那么每一个像素点就会占用4个字节,总内存就是1500*1000*4字节,也就是5.7M,这个数据看起来就比较恐怖了。

至于如何去压缩图片,以及更多在图片方面节省内存的技术,你们能够去参考我以前写的一篇博客 Android高效加载大图、多图解决方案,有效避免程序OOM 。

使用优化过的数据集合

Android API当中提供了一些优化事后的数据集合工具类,如SparseArray,SparseBooleanArray,以及LongSparseArray等,使用这些API可让咱们的程序更加高效。传统Java API中提供的HashMap工具类会相对比较低效,由于它须要为每个键值对都提供一个对象入口,而SparseArray就避免掉了基本数据类型转换成对象数据类型的时间。

知晓内存的开支状况

咱们还应当清楚咱们所使用语言的内存开支和消耗状况,而且在整个软件的设计和开发当中都应该将这些信息考虑在内。可能有一些看起来无关痛痒的写法,结果却会致使很大一部分的内存开支,例如:

  • 使用枚举一般会比使用静态常量要消耗两倍以上的内存,在Android开发当中咱们应当尽量地不使用枚举。

  • 任何一个Java类,包括内部类、匿名类,都要占用大概500字节的内存空间。

  • 任何一个类的实例要消耗12-16字节的内存开支,所以频繁建立实例也是会必定程序上影响内存的。

  • 在使用HashMap时,即便你只设置了一个基本数据类型的键,好比说int,可是也会按照对象的大小来分配内存,大概是32字节,而不是4字节。所以最好的办法就是像上面所说的同样,使用优化过的数据集合。

谨慎使用抽象编程

许多程序员都喜欢各类使用抽象来编程,认为这是一种很好的编程习惯。固然,这一点不能否认,由于的抽象的编程方法更加面向对象,并且在代码的维护和可扩展性方面都会有所提升。可是,在Android上使用抽象会带来额外的内存开支,由于抽象的编程方法须要编写额外的代码,虽然这些代码根本执行不到,可是却也要映射到内存当中,不只占用了更多的内存,在执行效率方面也会有所下降。固然这里我并非提倡你们彻底不使用抽象编程,而是谨慎使用抽象编程,不要认为这是一种很酷的编程方式而去肆意使用它,只在你认为有必要的状况下才去使用。

尽可能避免使用依赖注入框架

如今有不少人都喜欢在Android工程当中使用依赖注入框架,好比说像Guice或者RoboGuice等,由于它们能够简化一些复杂的编码操做,好比能够将下面的一段代码:

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1. class AndroidWay extends Activity {   

  2.     TextView name;   

  3.     ImageView thumbnail;   

  4.     LocationManager loc;   

  5.     Drawable icon;   

  6.     String myName;   

  7.   

  8.     public void onCreate(Bundle savedInstanceState) {   

  9.         super.onCreate(savedInstanceState);   

  10.         setContentView(R.layout.main);  

  11.         name      = (TextView) findViewById(R.id.name);   

  12.         thumbnail = (ImageView) findViewById(R.id.thumbnail);   

  13.         loc       = (LocationManager) getSystemService(Activity.LOCATION_SERVICE);   

  14.         icon      = getResources().getDrawable(R.drawable.icon);   

  15.         myName    = getString(R.string.app_name);   

  16.         name.setText( "Hello, " + myName );   

  17.     }   

  18. }   

简化成这样的一种写法:

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1. @ContentView(R.layout.main)  

  2. class RoboWay extends RoboActivity {   

  3.     @InjectView(R.id.name)             TextView name;   

  4.     @InjectView(R.id.thumbnail)        ImageView thumbnail;   

  5.     @InjectResource(R.drawable.icon)   Drawable icon;   

  6.     @InjectResource(R.string.app_name) String myName;   

  7.     @Inject                            LocationManager loc;   

  8.   

  9.     public void onCreate(Bundle savedInstanceState) {   

  10.         super.onCreate(savedInstanceState);   

  11.         name.setText( "Hello, " + myName );   

  12.     }   

  13. }  

看上去确实十分诱人,咱们甚至能够将findViewById()这一类的繁琐操做所有省去了。可是这些框架为了要搜寻代码中的注解,一般都须要经历较长的初始化过程,而且还可能将一些你用不到的对象也一并加载到内存当中。这些用不到的对象会一直占用着内存空间,可能要过好久以后才会获得释放,相较之下,也许多敲几行看似繁琐的代码才是更好的选择。

使用ProGuard简化代码

ProGuard相信你们都不会陌生,不少人都会使用这个工具来混淆代码,可是除了混淆以外,它还具备压缩和优化代码的功能。ProGuard会对咱们的代码进行检索,删除一些无用的代码,而且会对类、字段、方法等进行重命名,重命名以后的类、字段和方法名都会比原来简短不少,这样的话也就对内存的占用变得更少了。

使用多个进程

这个技巧其实并非很是建议使用,但它确实是一种能够帮助咱们节省和管理内存的高级技巧。若是你要使用它的话必定要谨慎使用,由于绝大多数的应用程序都不该该在多个进程当中运行的,一旦使用不当,它甚至会增长额外的内存而不是帮咱们节省内存。这个技巧比较适用于那些须要在后台去完成一项独立的任务,和前台的功能是能够彻底区分开的场景。

这里举一个比较适合去使用多进程技巧的场景,好比说咱们正在作一个音乐播放器软件,其中播放音乐的功能应该是一个独立的功能,它不须要和UI方面有任何关系,即便软件已经关闭了也应该能够正常播放音乐。若是此时咱们只使用一个进程,那么即便用户关闭了软件,已经彻底由Service来控制音乐播放了,系统仍然会将许多UI方面的内存进行保留。在这种场景下就很是适合使用两个进程,一个用于UI展现,另外一个则用于在后台持续地播放音乐。

想要实现多进程的功能也很是简单,只须要在AndroidManifest文件的应用程序组件中声明一个android:process属性就能够了,好比说咱们但愿播放音乐的Service能够运行在一个单独的进程当中,就能够这样写:

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1. <service android:name=".PlaybackService"  

  2.          android:process=":background" />  

这里指定的进程名是background,你也能够将它改为任意你喜欢的名字。须要注意的是,进程名的前面都应该加上一个冒号,表示该进程是一个当前应用程序的私有进程。

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

[java] view plaincopy

  1. ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);  

  2. int heapSize = manager.getMemoryClass();  

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

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


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


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

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

[plain] view plaincopy在CODE上查看代码片派生到个人代码片

  1. 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内存泄漏的场景,内部类相信你们都有用过,若是咱们在一个类中又定义了一个非静态的内部类,那么这个内部类就会持有外部类的引用,以下所示:

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  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.     ......  

  14. }  

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

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  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.     ......  

  25. }  

这下就有点不太同样了,咱们让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命令就能够完成转换工做,以下所示:

[plain] view plaincopy在CODE上查看代码片派生到个人代码片

  1. 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 2.3版本当中又增长了并发垃圾回收器机制(详见 Android最佳性能实践(二)——分析内存的使用状况),这让GC操做时的停顿时间也变得难以察觉,可是这些理由都不足以让咱们能够肆意地建立对象,须要建立的对象咱们天然要建立,可是没必要要的对象咱们就应该尽可能避免建立。

下面来看一些咱们能够避免建立对象的场景:

  • 若是咱们有一个须要拼接的字符串,那么能够优先考虑使用StringBuffer或者StringBuilder来进行拼接,而不是加号链接符,由于使用加号链接符会建立多余的对象,拼接的字符串越长,加号链接符的性能越低。

  • 在没有特殊缘由的状况下,尽可能使用基本数据类来代替封装数据类型,int比Integer要更加高效,其它数据类型也是同样。

  • 当一个方法的返回值是String的时候,一般能够去判断一下这个String的做用是什么,若是咱们明确地知道调用方会将这个返回的String再进行拼接操做的话,能够考虑返回一个StringBuffer对象来代替,由于这样能够将一个对象的引用进行返回,而返回String的话就是建立了一个短生命周期的临时对象。

  • 正如前面所说,基本数据类型要优于对象数据类型,相似地,基本数据类型的数组也要优于对象数据类型的数组。另外,两个平行的数组要比一个封装好的对象数组更加高效,举个例子,Foo[]和Bar[]这样的两个数组,使用起来要比Custom(Foo,Bar)[]这样的一个数组高效得多。

固然上面所说的只是一些表明性的例子,咱们所要遵照的一个基本原则就是尽量地少建立临时对象,越少的对象意味着越少的GC操做,同时也就意味着越好的程序性能和用户体验。

静态优于抽象

若是你并不须要访问一个对象中的某些字段,只是想调用它的某个方法来去完成一项通用的功能,那么能够将这个方法设置成静态方法,这会让调用的速度提高15%-20%,同时也不用为了调用这个方法而去专门建立对象了,这样还知足了上面的一条原则。另外这也是一种好的编程习惯,由于咱们能够放心地调用静态方法,而不用担忧调用这个方法后是否会改变对象的状态(静态方法内没法访问非静态字段)。

对常量使用static final修饰符

咱们先来看一下在一个类的最顶部定义以下代码:

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1. static int intVal = 42;  

  2. static String strVal = "Hello, world!";  

编译器会为上述代码生成一个初始化方法,称为<clinit>方法,该方法会在定义类第一次被使用的时候调用。而后这个方法会将42的值赋值到intVal当中,并从字符串常量表中提取一个引用赋值到strVal上。当赋值完成后,咱们就能够经过字段搜寻的方式来去访问具体的值了。

可是咱们还能够经过final关键字来对上述代码进行优化:

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1. static final int intVal = 42;  

  2. static final String strVal = "Hello, world!";  

通过这样修改以后,定义类就再也不须要一个<clinit>方法了,由于全部的常量都会在dex文件的初始化器当中进行初始化。当咱们调用intVal时能够直接指向42的值,而调用strVal时会用一种相对轻量级的字符串常量方式,而不是字段搜寻的方式。

另外须要你们注意的是,这种优化方式只对基本数据类型以及String类型的常量有效,对于其它数据类型的常量是无效的。不过,对于任何常量都是用static final的关键字来进行声明仍然是一种很是好的习惯。

使用加强型for循环语法

加强型for循环(也被称为for-each循环)能够用于去遍历实现Iterable接口的集合以及数组,这是jdk 1.5中新增的一种循环模式。固然除了这种新增的循环模式以外,咱们仍然还可使用原有的普通循环模式,只不过它们之间是有效率区别的,咱们来看下面一段代码:

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1. static class Counter {  

  2.     int mCount;  

  3. }  

  4.   

  5. Counter[] mArray = ...  

  6.   

  7. public void zero() {  

  8.     int sum = 0;  

  9.     for (int i = 0; i < mArray.length; ++i) {  

  10.         sum += mArray[i].mCount;  

  11.     }  

  12. }  

  13.   

  14. public void one() {  

  15.     int sum = 0;  

  16.     Counter[] localArray = mArray;  

  17.     int len = localArray.length;  

  18.     for (int i = 0; i < len; ++i) {  

  19.         sum += localArray[i].mCount;  

  20.     }  

  21. }  

  22.   

  23. public void two() {  

  24.     int sum = 0;  

  25.     for (Counter a : mArray) {  

  26.         sum += a.mCount;  

  27.     }  

  28. }  

能够看到,上述代码当中咱们使用了三种不一样的循环方式来对mArray中的全部元素进行求和。其中zero()方法是最慢的一种,由于它是把mArray.length写在循环当中的,也就是说每循环一次都须要从新计算一次mArray的长度。而one()方法则相对快得多,由于它使用了一个局部变量len来记录数组的长度,这样就省去了每次循环时字段搜寻的时间。two()方法在没有JIT(Just In Time Compiler)的设备上是运行最快的,而在有JIT的设备上运行效率和one()方法不相上下,惟一须要注意的是这种写法须要JDK 1.5以后才支持。

可是这里要跟你们提一个特殊状况,对于ArrayList这种集合,本身手写的循环要比加强型for循环更快,而其余的集合就没有这种状况。所以,对于咱们来讲,默认状况下能够都使用加强型for循环,而遍历ArrayList时就仍是使用传统的循环方式吧。

多使用系统封装好的API

Java语言当中其实给咱们提供了很是丰富的API接口,咱们在编写程序时若是可使用系统提供的API就应该尽可能使用,系统提供的API完成不了咱们须要的功能时才应该本身去写,由于使用系统的API在不少时候比咱们本身写的代码要快得多,它们的不少功能都是经过底层的汇编模式执行的。

好比说String类当中提供的好多API都是拥有极高的效率的,像indexOf()方法和一些其它相关的API,虽然说咱们经过本身编写算法也可以完成一样的功能,可是效率方面会和这些方法差的比较远。这里举个例子,若是咱们要实现一个数组拷贝的功能,使用循环的方式来对数组中的每个元素一一进行赋值固然是可行的,可是若是咱们直接使用系统中提供的System.arraycopy()方法将会让执行效率快9倍以上。

避免在内部调用Getters/Setters方法

咱们平时写代码时都被告知,必定要使用面向对象的思惟去写代码,而面向对象的三大特性咱们都知道,封装、多态和继承。其中封装的基本思想就是不要把类内部的字段暴漏给外部,而是提供特定的方法来容许外部操做相应类的内部字段,从而在Java语言当中就出现了Getters/Setters这种封装技巧。

然而在Android上这个技巧就再也不是那么的受推崇了,由于字段搜寻要比方法调用效率高得多,咱们直接访问某个字段可能要比经过getters方法来去访问这个字段快3到7倍。不过咱们确定不能仅仅由于效率的缘由就将封装这个技巧给抛弃了,编写代码仍是要按照面向对象思惟的,可是咱们能够在能优化的地方进行优化,好比说避免在内部调用getters/setters方法。

那什么叫作在内部调用getters/setters方法呢?这里我举一个很是简单的例子:

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1. public class Calculate {  

  2.       

  3.     private int one = 1;  

  4.       

  5.     private int two = 2;  

  6.   

  7.     public int getOne() {  

  8.         return one;  

  9.     }  

  10.   

  11.     public int getTwo() {  

  12.         return two;  

  13.     }  

  14.       

  15.     public int getSum() {  

  16.         return getOne() + getTwo();  

  17.     }  

  18. }  

能够看到,上面是一个Calculate类,这个类的功能很是简单,先将one和two这两个字段进行了封装,而后提供了getOne()方法获取one字段的值,提供了getTwo()方法获取two字段的值,还提供了一个getSum()方法用于获取总和的值。

这里咱们注意到,getSum()方法当中的算法就是将one和two的值相加进行返回,可是它获取one和two的值的方式也是经过getters方法进行获取的,其实这是一种彻底没有必要的方式,由于getSum()方法自己就是Calculate类内部的方法,它是能够直接访问到Calculate类中的封装字段的,所以这种写法在Android上是不推崇的,咱们能够进行以下修改:

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1. public class Calculate {  

  2.       

  3.     private int one = 1;  

  4.       

  5.     private int two = 2;  

  6.   

  7.     ......  

  8.       

  9.     public int getSum() {  

  10.         return one + two;  

  11.     }  

  12. }  

改为这种写法以后,咱们就避免了在内部调用getters/setters方法,而对于外部而言Calculate类仍然是具备很好的封装性的。

相关文章
相关标签/搜索