大礼包!ANDROID内存优化(大汇总)

时间:2022-03-06 21:50:44

写在最前:

本文的思路主要借鉴了2014年AnDevCon开发者大会的一个演讲PPT,加上把网上搜集的各种内存零散知识点进行汇总、挑选、简化后整理而成。

所以我将本文定义为一个工具类的文章,如果你在ANDROID开发中遇到关于内存问题,或者马上要参加面试,或者就是单纯的学习或复习一下内存相关知识,都欢迎阅读。(本文最后我会尽量列出所参考的文章)。

OOM:

内存泄露可以引发很多的问题:

1.程序卡顿,响应速度慢(内存占用高时JVM虚拟机会频繁触发GC)

2.莫名消失(当你的程序所占内存越大,它在后台的时候就越可能被干掉。反之内存占用越小,在后台存在的时间就越长)

3.直接崩溃(OutOfMemoryError)

ANDROID内存面临的问题:

1.有限的堆内存,原始只有16M

2.内存大小消耗等根据设备,操作系统等级,屏幕尺寸的不同而不同

3.程序不能直接控制

4.支持后台多任务处理(multitasking)

5.运行在虚拟机之上

5R:

本文主要通过如下的5R方法来对ANDROID内存进行优化:

1.Reckon(计算)

首先需要知道你的app所消耗内存的情况,知己知彼才能百战不殆

2.Reduce(减少)

消耗更少的资源

3.Reuse(重用)

当第一次使用完以后,尽量给其他的使用

5.Recycle(回收)

回收资源

4.Review(检查)

回顾检查你的程序,看看设计或代码有什么不合理的地方。

 

内存简介Reckon(计算):

关于内存简介,和Reckon的内容请看:ANDROID内存优化(大汇总——上)

 

 

Reduce(减少) ,Reuse(重用):

关于Reduce,和Reuse的内容请看:ANDROID内存优化(大汇总——中)

 

 

Recycle(回收):

 

Recycle(回收),回收可以说是在内存使用中最重要的部分。因为内存空间有限,无论你如何优化,如何节省内存总有用完的时候。而回收的意义就在于去清理和释放那些已经闲置,废弃不再使用的内存资源和内存空间。

因为在Java中有垃圾回收(GC)机制,所以我们平时都不会太关注它,下面就来简单的介绍一下回收机制:

 

垃圾回收(GC):

 

Java垃圾回收器:

在C,C++或其他程序设计语言中,资源或内存都必须由程序员自行声明产生和回收,否则其中的资源将消耗,造成资源的浪费甚至崩溃。但手工回收内存往往是一项复杂而艰巨的工作。

于是,Java技术提供了一个系统级的线程,即垃圾收集器线程(Garbage Collection Thread),来跟踪每一块分配出去的内存空间,当Java 虚拟机(Java Virtual Machine)处于空闲循环时,垃圾收集器线程会自动检查每一快分配出去的内存空间,然后自动回收每一快可以回收的无用的内存块。

作用:

1.清除不用的对象来释放内存:

采用一种动态存储管理技术,它自动地释放不再被程序引用的对象,按照特定的垃圾收集算法来实现资源自动回收的功能。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用。

2.消除堆内存空间的碎片:

由于创建对象和垃圾收集器释放丢弃对象所占的内存空间,内存会出现碎片。碎片是分配给对象的内存块之间的空闲内存洞。碎片整理将所占用的堆内存移到堆的一端,JVM将整理出的内存分配给新的对象。

垃圾回收器优点:

1.减轻编程的负担,提高效率:

使程序员从手工回收内存空间的繁重工作中解脱了出来,因为在没有垃圾收集机制的时候,可能要花许多时间来解决一个难懂的存储器问题。在用Java语言编程的时候,靠垃圾收集机制可大大缩短时间。

2.它保护程序的完整性:

因此垃圾收集是Java语言安全性策略的一个重要部份。

垃圾回收器缺点:

1.占用资源时间:

Java虚拟机必须追踪运行程序中有用的对象, 而且最终释放没用的对象。这一个过程需要花费处理器的时间。

2.不可预知:

垃圾收集器线程虽然是作为低优先级的线程运行,但在系统可用内存量过低的时候,它可能会突发地执行来挽救内存资源。当然其执行与否也是不可预知的。

3.不确定性:

不能保证一个无用的对象一定会被垃圾收集器收集,也不能保证垃圾收集器在一段Java语言代码中一定会执行。

同样也没有办法预知在一组均符合垃圾收集器收集标准的对象中,哪一个会被首先收集。

4.不可操作

垃圾收集器不可以被强制执行,但程序员可以通过调用System. gc方法来建议执行垃圾收集器。

垃圾回收算法:

1.引用计数(Reference Counting) 
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。 

2.标记-清除(Mark-Sweep) 
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
3.复制(Copying) 
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不过出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
4.标记-整理(Mark-Compact) 
此算法结合了 “标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象 “压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。 
5.增量收集(Incremental Collecting) 
实施垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么原因JDK5.0中的收集器没有使用这种算法的。 
6.分代(Generational Collecting) 
基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。

finalize():

每一个对象都有一个finalize方法,这个方法是从Object类继承来的。

当垃圾回收确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。

Java 技术允许使用finalize方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。一旦垃圾回收器准备好释放对象占用的空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
简单的说finalize方法是在垃圾收集器删除对象之前对这个对象调用的

System.gc():

我们可以调用System.gc方法,建议虚拟机进行垃圾回收工作(注意,是建议,但虚拟机会不会这样干,我们也无法预知!)

下面来看一个例子来了解finalize()System.gc()的使用:

  1. public class TestGC {
  2. public TestGC() {}
  3. //当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
  4. protected void finalize() {
  5. System.out.println("我已经被垃圾回收器回收了...");
  6. }
  7. public static void main(String [] args) {
  8. TestGC gc = new TestGC();
  9. gc = null;
  10. // 建议虚拟机进行垃圾回收工作
  11. System.gc();
  12. }
  13. }

如上面的例子所示,大家可以猜猜重写的finalize方法会不会执行?

答案是:不一定

因为无论是设置gc的引用为null还是调用System.gc()方法都只是"建议"垃圾回收器进行垃圾回收,但是最终所有权还在垃圾回收器手中,它会不会进行回收我们无法预知!

垃圾回收面试题:
最后通过网上找到的3道面试题来结束垃圾回收的内容。

面试题一:

  1. 1.fobj = new Object ( ) ;
  2. 2.fobj. Method ( ) ;
  3. 3.fobj = new Object ( ) ;
  4. 4.fobj. Method ( ) ;

问:这段代码中,第几行的fobj 符合垃圾收集器的收集标准? 
答:第3行。因为第3行的fobj被赋了新值,产生了一个新的对象,即换了一块新的内存空间,也相当于为第1行中的fobj赋了null值。这种类型的题是最简单的。

面试题二: 

  1. 1.Object sobj = new Object ( ) ;
  2. 2.Object sobj = null ;
  3. 3.Object sobj = new Object ( ) ;
  4. 4.sobj = new Object ( ) ;

问:这段代码中,第几行的内存空间符合垃圾收集器的收集标准? 
答:第2行和第4行。因为第2行为sobj赋值为null,所以在此第1行的sobj符合垃圾收集器的收集标准。而第4行相当于为sobj赋值为null,所以在此第3行的sobj也符合垃圾收集器的收集标准。

如果有一个对象的句柄a,且你把a作为某个构造器的参数,即 new Constructor ( a )的时候,即使你给a赋值为null,a也不符合垃圾收集器的收集标准。直到由上面构造器构造的新对象被赋空值时,a才可以被垃圾收集器收集。

面试题三: 

  1. 1.Object aobj = new Object ( ) ;
  2. 2.Object bobj = new Object ( ) ;
  3. 3.Object cobj = new Object ( ) ;
  4. 4.aobj = bobj;
  5. 5.aobj = cobj;
  6. 6.cobj = null;
  7. 7.aobj = null;

问:这段代码中,第几行的内存空间符合垃圾收集器的收集标准? 
答:第4,7行。注意这类题型是认证考试中可能遇到的最难题型了。 
行1-3:分别创建了Object类的三个对象:aobj,bobj,cobj
行4:此时对象aobj的句柄指向bobj,原来aojb指向的对象已经没有任何引用或变量指向,这时,就符合回收标准。
行5:此时对象aobj的句柄指向cobj,所以该行的执行不能使aobj符合垃圾收集器的收集标准。 
行6:此时仍没有任何一个对象符合垃圾收集器的收集标准。 
行7:对象cobj符合了垃圾收集器的收集标准,因为cobj的句柄指向单一的地址空间。在第6行的时候,cobj已经被赋值为null,但由cobj同时还指向了aobj(第5行),所以此时cobj并不符合垃圾收集器的收集标准。而在第7行,aobj所指向的地址空间也被赋予了空值null,这就说明了,由cobj所指向的地址空间已经被完全地赋予了空值。所以此时cobj最终符合了垃圾收集器的收集标准。 但对于aobj和bobj,仍然无法判断其是否符合收集标准。

总之,在Java语言中,判断一块内存空间是否符合垃圾收集器收集的标准只有两个: 
1.给对象赋予了空值null,以下再没有调用过。 
2.给对象赋予了新值,既重新分配了内存空间。

最后再次提醒一下,一块内存空间符合了垃圾收集器的收集标准,并不意味着这块内存空间就一定会被垃圾收集器收集。

资源的回收:

刚才讲了一堆理论的东西,下面来点实际能用上的,资源的回收:

Thread(线程)回收:

线程中涉及的任何东西GC都不能回收(Anything reachable by a thread cannot be GC'd ),所以线程很容易造成内存泄露。

如下面代码所示:

  1. Thread t = new Thread() {
  2. public void run() {
  3. while (true) {
  4. try {
  5. Thread.sleep(1000);
  6. System.out.println("thread is running...");
  7. } catch (InterruptedException e) {
  8. }
  9. }
  10. }
  11. };
  12. t.start();
  13. t = null;
  14. System.gc();

如上在线程t中每间隔一秒输出一段话,然后将线程设置为null并且调用System.gc方法。

最后的结果是线程并不会被回收,它会一直的运行下去。

因为运行中的线程是称之为垃圾回收根(GC Roots)对象的一种,不会被垃圾回收。当垃圾回收器判断一个对象是否可达,总是使用垃圾回收根对象作为参考点。

Cursor(游标)回收:

Cursor是Android查询数据后得到的一个管理数据集合的类,在使用结束以后。应该保证Cursor占用的内存被及时的释放掉,而不是等待GC来处理。并且Android明显是倾向于编程者手动的将Cursor close掉,因为在源代码中我们发现,如果等到垃圾回收器来回收时,会给用户以错误提示。

所以我们使用Cursor的方式一般如下:

  1. Cursor cursor = null;
  2. try {
  3. cursor = mContext.getContentResolver().query(uri,null, null,null,null);
  4. if(cursor != null) {
  5. cursor.moveToFirst();
  6. //do something
  7. }
  8. } catch (Exception e) {
  9. e.printStackTrace();
  10. } finally {
  11. if (cursor != null) {
  12. cursor.close();
  13. }
  14. }

有一种情况下,我们不能直接将Cursor关闭掉,这就是在CursorAdapter中应用的情况,但是注意,CursorAdapter在Acivity结束时并没有自动的将Cursor关闭掉,因此,你需要在onDestroy函数中,手动关闭。

  1. @Override
  2. protected void onDestroy() {
  3. if (mAdapter != null && mAdapter.getCurosr() != null) {
  4. mAdapter.getCursor().close();
  5. }
  6. super.onDestroy();
  7. }

Receiver(接收器)回收

调用registerReceiver()后未调用unregisterReceiver(). 
当我们Activity中使用了registerReceiver()方法注册了BroadcastReceiver,一定要在Activity的生命周期内调用unregisterReceiver()方法取消注册 
也就是说registerReceiver()和unregisterReceiver()方法一定要成对出现,通常我们可以重写Activity的onDestory()方法: 
  1. @Override
  2. protected void onDestroy() {
  3. this.unregisterReceiver(receiver);
  4. super.onDestroy();
  5. }

Stream/File(流/文件)回收:

主要针对各种流,文件资源等等如:

InputStream/OutputStream,SQLiteOpenHelper,SQLiteDatabase,Cursor,文件,I/O,Bitmap图片等操作等都应该记得显示关闭。
和之前介绍的Cursor道理类似,就不多说了。

Review:

Review(回顾,检查),大家都知道Code Review的重要性。而这里我说的Review和Code Review差不多,主要目的就是检查代码中存在的不合理和可以改进的地方,当然这个Review需要大家自己来做啦。

Code Review(代码检查):

Code Review主要检查代码中存在的一些不合理或可以改进优化的地方,大家可以参考之前写的Reduce,Reuse和Recycle都是侧重讲解这方面的。

UI Review(视图检查):

Android对于视图中控件的布局渲染等会消耗很多的资源和内存,所以这部分也是我们需要注意的。

减少视图层级:

减少视图层级可以有效的减少内存消耗,因为视图是一个树形结构,每次刷新和渲染都会遍历一次。
hierarchyviewer:
想要减少视图层级首先就需要知道视图层级,所以下面介绍一个SDK中自带的一个非常好用的工具hierarchyviewer。
你可以在下面的地址找到它:your sdk path\sdk\tools
大礼包!ANDROID内存优化(大汇总)

如上图大家可以看到,hierarchyviewer可以非常清楚的看到当前视图的层级结构,并且可以查看视图的执行效率(视图上的小圆点,绿色表示流畅,黄色和红色次之),所以我们可以很方便的查看哪些view可能会影响我们的性能从而去进一步优化它。

hierarchyviewer还提供另外一种列表式的查看方式,可以查看详细的屏幕画面,具体到像素级别的问题都可以通过它发现。

ViewStub标签

此标签可以使UI在特殊情况下,直观效果类似于设置View的不可见性,但是其更大的意义在于被这个标签所包裹的Views在默认状态下不会占用任何内存空间。

 

include标签

可以通过这个标签直接加载外部的xml到当前结构中,是复用UI资源的常用标签。

 

merge标签

它在优化UI结构时起到很重要的作用。目的是通过删减多余或者额外的层级,从而优化整个Android Layout的结构。

(注意:灵活运用以上3个标签可以有效减少视图层级,具体使用大家可以上网搜搜)

布局用Java代码比写在XML中快

一般情况下对于Android程序布局往往使用XML文件来编写,这样可以提高开发效率,但是考虑到代码的安全性以及执行效率,可以通过Java代码执行创建,虽然Android编译过的XML是二进制的,但是加载XML解析器的效率对于资源占用还是比较大的,Java处理效率比XML快得多,但是对于一个复杂界面的编写,可能需要一些套嵌考虑,如果你思维灵活的话,使用Java代码来布局你的Android应用程序是一个更好的方法。

重用系统资源:

1. 利用系统定义的id

比如我们有一个定义ListView的xml文件,一般的,我们会写类似下面的代码片段。

  1. <ListView
  2. android:id="@+id/mylist"
  3. android:layout_width="fill_parent"
  4. android:layout_height="fill_parent"/>

这里我们定义了一个ListView,定义它的id是"@+id/mylist"。实际上,如果没有特别的需求,就可以利用系统定义的id,类似下面的样子。

  1. <ListView
  2. android:id="@android:id/list"
  3. android:layout_width="fill_parent"
  4. android:layout_height="fill_parent"/>

在xml文件中引用系统的id,只需要加上“@android:”前缀即可。如果是在Java代码中使用系统资源,和使用自己的资源基本上是一样的。不同的是,需要使用android.R类来使用系统的资源,而不是使用应用程序指定的R类。这里如果要获取ListView可以使用android.R.id.list来获取。

2. 利用系统的图片资源

这样做的好处,一个是美工不需要重复的做一份已有的图片了,可以节约不少工时;另一个是能保证我们的应用程序的风格与系统一致。

3. 利用系统的字符串资源

如果使用系统的字符串,默认就已经支持多语言环境了。如上述代码,直接使用了@android:string/yes和@android:string/no,在简体中文环境下会显示“确定”和“取消”,在英文环境下会显示“OK”和“Cancel”。

4. 利用系统的Style

假设布局文件中有一个TextView,用来显示窗口的标题,使用中等大小字体。可以使用下面的代码片段来定义TextView的Style。

  1. <TextView
  2. android:id="@+id/title"
  3. android:layout_width="wrap_content"
  4. android:layout_height="wrap_content"
  5. android:textAppearance="?android:attr/textAppearanceMedium" />

其中android:textAppearance="?android:attr/textAppearanceMedium"就是使用系统的style。需要注意的是,使用系统的style,需要在想要使用的资源前面加“?android:”作为前缀,而不是“@android:”。

5. 利用系统的颜色定义

除了上述的各种系统资源以外,还可以使用系统定义好的颜色。在项目中最常用的,就是透明色的使用。

  1. android:background ="@android:color/transparent"

除了上面介绍的以外还有很多其他Android系统本身自带的资源,它们在应用中都可以直接使用。具体的,可以进入android-sdk的相应文件夹中去查看。例如:可以进入$android-sdk$\platforms\android-8\data\res,里面的系统资源就一览无余了。

开发者需要花一些时间去熟悉这些资源,特别是图片资源和各种Style资源,这样在开发过程中,能重用的尽量重用,而且有时候使用系统提供的效果可能会更好。

其他小tips:

1. 分辨率适配-ldpi,-mdpi, -hdpi配置不同精度资源,系统会根据设备自适应,包括drawable, layout,style等不同资源。

2.尽量使用dp(density independent pixel)开发,不用px(pixel)。

3.多用wrap_content, match_parent

4.永远不要使用AbsoluteLayout

5.使用9patch(通过~/tools/draw9patch.bat启动应用程序),png格式

6.将Acitivity中的Window的背景图设置为空。getWindow().setBackgroundDrawable(null);android的默认背景是不是为空。

7.View中设置缓存属性.setDrawingCache为true。

 

Desgin Review(设计检查):

Desgin Review主要侧重检查一下程序的设计是否合理,包括框架的设计,界面的设计,逻辑的设计(其实这些东西开发之前就应该想好了)。

框架设计:

是否定义了自己的Activity和fragment等常用控件的基类去避免进行重复的工作

是否有完善的异常处理机制,即使真的出现OOM也不会直接崩溃导致直接退出程序

界面设计:

1.在视图中加载你所需要的,而不是你所拥有。因为用户不可能同时看到所有东西。最典型的例子就是ListView中的滑动加载。

2.如果数据特别大,此时应该暗示用户去点击加载,而不是直接加载。

3.合理运用分屏,转屏等,它是个双刃剑,因为它即可以使程序更加美观功能更加完善,但也相应增加了资源开销。

 

逻辑设计:

避免子类直接去控制父类中内容,可以使用监听等方式去解决

关于这三点由于我工作经验比较少,加上一时半会也想不出来多少,如果大家有建议希望可以留言,之后我给加进去。

写在最后:

到此ANDROID内存优化上、中、下三篇全部写完了。

内存简介,Recoken(计算)请看ANDROID内存优化(大汇总——上)

Reduce(减少),Reuse(重用) 请看:ANDROID内存优化(大汇总——中)

Recycle(回收), Review(检查) 请看:ANDROID内存优化(大汇总——全)

最初写这篇文章的原因是因为我拿到一个国外大牛演讲的PPT,我看过之后感觉写的非常好,于是想按照ppt的思路将其总结一下。结果到写的时候发现困难重重,因为内存本来就是很理论的东西,很多都是靠经验的。而我的经验几乎可以忽略,写的东西完全是网上各路文章的大汇总(所以大家千万不要叫我大神,我只是大神的搬运工。。。)

虽然如此我觉得我总结和搜集的还算比较全面的,当然也有很多遗落也可能有很多错误,这个就希望大家一起帮着完善一下。

最后我把这个PPT的原件附上,里面很多高级的东西我没看懂(比如那个5R中其实是没有Review的,原文是Reorder,由于这部分我看不懂而且找不到很好的资料只能自己换了一个Review),各路大神有兴趣可以看看,如果可以的话写出来分享一下。

Putting Your App on a Memory Diet, Parts I and II_Murphy

最后小唠叨一下,我最近参加了devstore网站的一个小比赛,所以blog先停更一个月,十一之后接着写。

在这段时间里我正好也可以休息一下想想以后写点什么东西。像内存这种偏理论的东西我还是不要碰了,以后可能会多翻译一些国外大神的文章和自己做的一些小Demo吧。

不知不觉Blog也写了快半年了,越来越觉得Blog这种分享精神的重要性,因为只有分享才能收获的更多!

最后要谢谢那些关注,点赞和评论的网友们,这些真的是我能坚持下来的一个巨大动力!

参考文章:

Android内存优化http://blog.csdn.net/imain/article/details/8560986
Android 内存优化http://blog.csdn.net/awangyunke/article/details/20380719
关于android性能,内存优化http://www.cnblogs.com/zyw-205520/archive/2013/02/17/2914190.htm
Java垃圾回收原理http://www.360doc.com/content/11/0911/15/18042_147476260.shtml
JVM垃圾回收(GC)原理http://www.360doc.com/content/11/0911/16/18042_147492404.shtml