写在最前:
本文的思路主要借鉴了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的对象。此算法最致命的是无法处理循环引用的问题。
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不过出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
此算法结合了 “标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象 “压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
实施垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么原因JDK5.0中的收集器没有使用这种算法的。
基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。
finalize():
每一个对象都有一个finalize方法,这个方法是从Object类继承来的。
当垃圾回收确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
Java 技术允许使用finalize方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。一旦垃圾回收器准备好释放对象占用的空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
简单的说finalize方法是在垃圾收集器删除对象之前对这个对象调用的
System.gc():
我们可以调用System.gc方法,建议虚拟机进行垃圾回收工作(注意,是建议,但虚拟机会不会这样干,我们也无法预知!)
下面来看一个例子来了解finalize()和System.gc()的使用:
- public class TestGC {
- public TestGC() {}
- //当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
- protected void finalize() {
- System.out.println("我已经被垃圾回收器回收了...");
- }
- public static void main(String [] args) {
- TestGC gc = new TestGC();
- gc = null;
- // 建议虚拟机进行垃圾回收工作
- System.gc();
- }
- }
如上面的例子所示,大家可以猜猜重写的finalize方法会不会执行?
答案是:不一定!
因为无论是设置gc的引用为null还是调用System.gc()方法都只是"建议"垃圾回收器进行垃圾回收,但是最终所有权还在垃圾回收器手中,它会不会进行回收我们无法预知!
垃圾回收面试题:
最后通过网上找到的3道面试题来结束垃圾回收的内容。
面试题一:
- 1.fobj = new Object ( ) ;
- 2.fobj. Method ( ) ;
- 3.fobj = new Object ( ) ;
- 4.fobj. Method ( ) ;
问:这段代码中,第几行的fobj 符合垃圾收集器的收集标准?
答:第3行。因为第3行的fobj被赋了新值,产生了一个新的对象,即换了一块新的内存空间,也相当于为第1行中的fobj赋了null值。这种类型的题是最简单的。
面试题二:
- 1.Object sobj = new Object ( ) ;
- 2.Object sobj = null ;
- 3.Object sobj = new Object ( ) ;
- 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.Object aobj = new Object ( ) ;
- 2.Object bobj = new Object ( ) ;
- 3.Object cobj = new Object ( ) ;
- 4.aobj = bobj;
- 5.aobj = cobj;
- 6.cobj = null;
- 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 ),所以线程很容易造成内存泄露。
如下面代码所示:
- Thread t = new Thread() {
- public void run() {
- while (true) {
- try {
- Thread.sleep(1000);
- System.out.println("thread is running...");
- } catch (InterruptedException e) {
- }
- }
- }
- };
- t.start();
- t = null;
- System.gc();
如上在线程t中每间隔一秒输出一段话,然后将线程设置为null并且调用System.gc方法。
最后的结果是线程并不会被回收,它会一直的运行下去。
因为运行中的线程是称之为垃圾回收根(GC Roots)对象的一种,不会被垃圾回收。当垃圾回收器判断一个对象是否可达,总是使用垃圾回收根对象作为参考点。
Cursor(游标)回收:
Cursor是Android查询数据后得到的一个管理数据集合的类,在使用结束以后。应该保证Cursor占用的内存被及时的释放掉,而不是等待GC来处理。并且Android明显是倾向于编程者手动的将Cursor close掉,因为在源代码中我们发现,如果等到垃圾回收器来回收时,会给用户以错误提示。
所以我们使用Cursor的方式一般如下:
- Cursor cursor = null;
- try {
- cursor = mContext.getContentResolver().query(uri,null, null,null,null);
- if(cursor != null) {
- cursor.moveToFirst();
- //do something
- }
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
有一种情况下,我们不能直接将Cursor关闭掉,这就是在CursorAdapter中应用的情况,但是注意,CursorAdapter在Acivity结束时并没有自动的将Cursor关闭掉,因此,你需要在onDestroy函数中,手动关闭。
- @Override
- protected void onDestroy() {
- if (mAdapter != null && mAdapter.getCurosr() != null) {
- mAdapter.getCursor().close();
- }
- super.onDestroy();
- }
Receiver(接收器)回收
也就是说registerReceiver()和unregisterReceiver()方法一定要成对出现,通常我们可以重写Activity的onDestory()方法:
- @Override
- protected void onDestroy() {
- this.unregisterReceiver(receiver);
- super.onDestroy();
- }
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可以非常清楚的看到当前视图的层级结构,并且可以查看视图的执行效率(视图上的小圆点,绿色表示流畅,黄色和红色次之),所以我们可以很方便的查看哪些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文件,一般的,我们会写类似下面的代码片段。
- <ListView
- android:id="@+id/mylist"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"/>
这里我们定义了一个ListView,定义它的id是"@+id/mylist"。实际上,如果没有特别的需求,就可以利用系统定义的id,类似下面的样子。
- <ListView
- android:id="@android:id/list"
- android:layout_width="fill_parent"
- 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。
- <TextView
- android:id="@+id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textAppearance="?android:attr/textAppearanceMedium" />
其中android:textAppearance="?android:attr/textAppearanceMedium"就是使用系统的style。需要注意的是,使用系统的style,需要在想要使用的资源前面加“?android:”作为前缀,而不是“@android:”。
5. 利用系统的颜色定义
除了上述的各种系统资源以外,还可以使用系统定义好的颜色。在项目中最常用的,就是透明色的使用。
- 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