Android 中关于activity引起的内存溢出


Android应用程序如何避免内存泄漏以及如何检查泄漏原因



Android的应用程序开发使用的Java语言。Java语言的GC机制使得在堆上分配内存之后无需再手动的释放内存,而是等待垃圾收集器来收集无用的对象以回收它们占用的内存。同时在Android的进程管理机制中每一个单独的应用程序在启动时都会创建一个新的Linux进程来运行该程序,应用程序在运行中分配的内存也会在该应用程序退出时随着进程的销毁而释放,所以Android中的内存管理给开发人员造成的负担较轻。但应用程序还需要在内存使用上注意不要使应用程序占用大量内存,原因有如下两点:

  1. 应用程序占用的内存越少,Android可以同时放入内存程序就越多,用户切换这些不同的程序所消耗的时间就越少,体验就越流畅。

  2. 如果应用程序在消耗光了所有的可用堆空间(16M48M),那么再试图在堆上分配新对象时就会引起OOM(OutOf Memory Error)异常,此时应用程序就会崩溃退出。

所以在编写Android应用程序时,仍然需要对应用程序中内存的分配和使用多加注意,特别是在存在后台线程、使用图片作为背景、在异步任务或者后台线程中需要Context上下文对象的情况下,要注意避免出现对ActivityViewdrawable等类的对象长期持有无用的reference,否则就会造成被引用的对象无法在GC时回收,而是长期占用堆空间,此时就发生了内存泄漏。

持有Context引用造成的泄漏

下面介绍一下Android开发文档中(AvoidingMemoryLeak)的一个内存泄漏的例子,该例子说明了Android应用程序中会引起内存泄漏的常见原因:长期保持了对Context对象的引用。在Android应用程序中,很多操作都用到了Context对象,但是大多数都是用来加载和访问资源的。这就是为什么所有的显示控件都需要一个Context对象作为构造方法的参数。在Android应用程序中通常可以使用两种Context对象:ActivityApplication。当类或方法需要Context对象的时候常见的作法是使用第一个作为Context参数。但这就意味着View对象对整个activity保持引用,因此也就保持对activity内的所有东西的引用,也就是整个View结构和它所有的资源都无法被及时的回收,而且对activity的长期引用是比较隐蔽的。

@Override

protectedvoid onCreate(Bundle state) {

super.onCreate(state);

TextViewlabel = new TextView(this);

label.setText("Leaksare bad");

setContentView(label);

}

当屏幕方向改变时,Android系统默认作法是会销毁当前的Activity,然后创建一个新的Activity,这个新的Activity会显示刚才的状态。在这样做的过程中,Android系统会重新加载UI用到的资源。现在假设的应用程序中有一个比较大的bitmap类型的图片,每次旋转时都重新加载图片所用的时间较多。为了提高屏幕旋转时Activity的创建速度,最简单的方法是用静态变量的方法。

privatestatic Drawable sBackground;

@Override

protectedvoid onCreate(Bundle state) {

super.onCreate(state);

TextViewlabel = new TextView(this);

label.setText("Leaksare bad");

if(sBackground == null) {

sBackground= getDrawable(R.drawable.large_bitmap);

}

label.setBackgroundDrawable(sBackground);

setContentView(label);

}

这样的代码执行起来是快速的,但同时是错误的:这样写会一直保持着对Activity的引用。当一个Drawable对象附属于一个View时,这个View就相当于drawable对象的一个回调(引用)。在上面的代码片段中,就意味着drawableTextView存在着引用的关系,而TextView自己持有了对ActivityContext对象)的引用,这个Activity又引用了相当多的东西。

有两种简单的方法可以避免由引用context对象造成的内存泄露。首先第一个方法是避免context对象超出它的作用范围。上面的例子展示了静态引用的情况,但是在类的内部,隐式的引用外部的类同样的危险。第二种方法是,使用Application对象。这个context对象会随着应用程序的存在而存在,而不依赖于activity的生命周期。如果你打算对context对象保持一个长期的引用,请记住这个application对象。通过调用Context.getApplicationContext()或者Activity.getApplication().方法,你可以很容易的得到这个对象。

非静态内部类引起的内存泄漏

Java语言中可以在一个类的内部定义innerclass,这在很多时候减少了工作量,而且使代码更加紧凑,但是由于非静态的innerclass对象中包含了产生构造该innerclass的包围类的引用,这样在Android环境中也会可能会因为对包围类的长期引用而造成内存泄漏。

我们修改上一个例子的代码,将sBackground由静态成员变量改为普通成员变量,这样就消除了该静态对象持有Activity的引用所造成的泄漏。之后我们在该Activity类中定一个非静态的内部类Inner,并且对该Activity定义一个静态变量innerInstance。具体代码如下:

publicclass MemoryLeak extends Activity {

classInner {

intsomeMember;

}

privatestatic Inner innerInstance;

privateDrawable sBackground;

/**Called when the activity is first created. */

@Override

publicvoid onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

TextViewlabel = new TextView(this);

label.setText("Leaksare bad");

sBackground= getResources().getDrawable(R.drawable.pic1);

if(innerInstance == null) {

innerInstance= new Inner();

}

label.setBackgroundDrawable(sBackground);

setContentView(label);

}

}

上面例子代码在旋屏时同样会引起内存泄漏,原因是innerInstance在第一次构造时会持有当时Activity的引用。当旋屏引起Activity重新被构造,新的Activity对象执行onCreate()函数时,作为Activity类的静态成员变量innerInstance此时不为null,所以并不会被重新构造,但它还保留着之前Activity对象的引用,造成了该Activity对象无法被释放。在上面的例子中也就意味着至少一个Drawable对象占用的内存没有被及时回收。对于上述两种常见的内存泄漏,我们在开发中需要记住以下2点:

  1. activity的持久引用,对activity的引用应该和activity本身有相同的生命周期,尽量使用application代替activity作为Context来获取资源,或者构造DialogToast

  2. 如果不能控制非静态的内部类的生命周期,尽量在activity中避免有非静态的内部类。同时在activity中使用静态的类时如果需要引用activity,应该采用WeakReference弱引用来引用Activity

后台线程操作引起的泄漏

Android开发中常常需要在后台线程中执行查询或者后台操作。Android本身也提供了一些常见的异步任务类来简化多线程编程,譬如我们可以使用AsyncQueryHandler来执行后台查询任务,使用AsyncTask来执行异步的后台任务。这些Async前缀的异步任务类在执行时都会在UI线程之外新开了一个线程来执行。我们在使用这些异步任务类的时,需要注意之前提到的不要将这些异步任务类的派生类定义为Activitynon-staticinner class以避免之前提到的innerclass引起的内存泄漏。除此之外,还需要注意不要在这些异步任务类中持有Activity的强引用,而应该采用WeakReference来保存Activity的引用。这样就不会由于后台线程在执行时Activity对象无法及时释放。例如在联系人列表(ContactsListActivity)中查询联系人的QueryHandler类,它作为AsyncQueryHandler的派生类主要用来执行查询联系人数据库的操作,联系人较多的话查询操作用时就会较长,所以采用AsyncQueryHander在后台线程中执行查询。在该类的定义中就通过以下两点来消除内存泄漏:

  1. QueryHander定义为staticinner class,避免non-staticinner class对象持有的ContactsListAcitivty对象的引用

  2. 通过WeakReference来保存ContactsListAcitivity的引用。

这样就保证了该后台查询线程不会持用ContactsListActivity的强引用,从而保证了即使后台查询线程正在执行的情况下,ContactsListAcitivity在响应Home键或者旋转屏幕时也能够被GC回收。

privatestatic class QueryHandler extends AsyncQueryHandler {

protectedfinal WeakReference<ContactsListActivity> mActivity;

protectedboolean mLoadingJoinSuggestions = false;

...

...

@Override

protectedvoid onQueryComplete(int token, Object cookie, Cursor cursor) {

finalContactsListActivity activity = mActivity.get();

if(activity != null && !activity.isFinishing()) {

...

...

与我们正常使用的强引用类型不同,上述示例代码中WeakReference并不会增加ContactsListActivity的引用计数,在使用WeakReference类型的mActivity变量时,也需要判断其get()返回的结果是否为null来判断ContactsListActivity对象是否还存在。在Java语言中,有以下四种引用类型,

  1. StrongReference(强引用):通常我们编写的代码都是StrongRef,于此对应的是强可达性,只有去掉强可达,对象才被回收。

  2. SoftReference (软引用):对应软可达性,只要有足够的内存,就一直保持对象,直到发现内存吃紧且没有StrongRef时才回收对象。一般可用来实现缓存,通过java.lang.ref.SoftReference类实现。

  3. WeakReference (弱引用):比SoftRef更弱,当发现不存在StrongRef时,立刻回收对象而不必等到内存吃紧的时候。通过java.lang.ref.WeakReferencejava.util.WeakHashMap类实现。

  4. PhantomReference (虚引用):根本不会在内存中保持任何对象,你只能使用PhantomRef本身。一般用于在进入finalize()方法后进行特殊的清理过程,通过java.lang.ref.PhantomReference实现。

关于这四种不同的类型,可以查看[Java垃圾回收机制与引用类型]一文中的详细说明。

在使用AsyncTask执行后台任务时,也要注意将其定义为staticinnerclass,并且在其doInBackground()函数中检查isCancelled()是否为true,从而可以尽快的退出后台线程。Android开发手册中说明如果需要在后台执行任务时,尽量采用AsyncTask并按照其规范重载相应的钩子函数。但Java语言中传统上使用RunnableThread来开起新的线程并执行后台任务的作法也是可以的,只是Android并不提倡这样作,特别是在应用程序中通过Thread在后台执行耗时较长的任务。例如下面的示例代码:

newThread() {

@Override

publicvoid run() {

//do something

}

}

或者

Runnabletask = new Runnable() {

publicvoid run() {

//do something

}

};

Threadt = new Thread(task);

t.start();

上述直接使用Thread类创建线程来执行后台任务有很大的局限性,由于Android要求UI界面的更新必须是在UI主线程中进行,所以如果后台线程中想要操作UI界面中的元素(例如更新进度条)就必须通过Handler来发送消息,反而增加了代码的复杂性。如果使用标准的AsyncTask,则只需要实现onProgressUpdate()钩子函数,就可以在后台任务执行的同时动态的更新UI界面。由于是onProgressUpdate()钩子函数是在UI主线程中执行,在该钩子函数中可以安全的操用UI界面。而且AsyncTask在可以在onCancelled()onPostExecute()钩子函数中操用UI界面元素以通知用户后台任务的执行结果,上述直接采用Thread类的方法仍然必须通过Handler才能更新用户界面。

线程之间通过Handler通信引起的内存泄漏

Android中线程之间进行通信时最常用的作法是通过接收消息的目标线程所持有Handler对象来创建Message对象,然后再向目标线程发送该Message。在目标线程中Handler在执行handleMessage()时会根据相应Message来执行相应不同功能。另外一种作法是通过Handler对象向目标线程直接发送Runnable对象来执行该Runnable对象中不同的功能代码。在通过Handler进行通信时如果不注意,也很有可能引起内存泄漏。例如线程之间通过MessageRunnable对象进行通讯的例子。

privateDrawable sBackground;

privateMessage msg;

@Override

protectedvoid onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

TextViewlabel = new TextView(this);

label.setText("Leaksare bad");

sBackground= getResources().getDrawable(R.drawable.pic1);

label.setBackgroundDrawable(sBackground);

setContentView(label);

OtherThreadt = new OtherThread("other thread");

t.start();

Handlerh = new Handler(t.getLooper()) {

publicvoid handleMessage(Message msg) {

Log.d(TAG,"msg is " + msg.toString());

Log.d(TAG,"Thread " + Thread.currentThread().getId()

+" handled message");

}

};

try{

msg= h.obtainMessage(101);

h.sendMessage(msg);

TimeUnit.SECONDS.sleep(3);

h.post(newRunnable() {

publicvoid run() {

Log.d(TAG,"hahaha");

try{

TimeUnit.SECONDS.sleep(30);

}catch (InterruptedException e) {

//TODO Auto-generated catch block

e.printStackTrace();

}

Log.d(TAG,"Thread " + Thread.currentThread().getId()

+" waken up");

}

});

}catch (InterruptedException e) {

//TODO Auto-generated catch block

e.printStackTrace();

}

}

privatestatic class OtherThread extends HandlerThread {

OtherThread(Stringname) {

super(name);

}

}

上述例子代码模拟了UI线程同另一线程OtherThread之间通过Handler进行通讯的情景,UI线程会通过发送Message对象和Runnable对象来委托OtherThread来执行不同的任务,这时Message代表的任务是轻量级的任务,其执行速度很快,这里就是打出两行Log。通过Runnable对象发送的任务是重量级的任务,这个任务会执行30秒。在上述存在两处会引起内存泄漏的地方,首先是在Activitymsg成员变量在将Message发送给OtherThread后,仍然是指向这个已发送的Message,尽管OtherThread能够很快的处理这一Message对象任务并随后释放对该Message的引用。但是在UI线程中,在发送完该Message后会长时间的执行其它任务(这里用休眠3秒代表执行其它任务),而在这3秒过程中由于msg成员变量仍然是指向之前已经完成的Message对象。这就使得该Message对象迟迟不能释放。如果Message带有大量的信息的话,那么其占用的内存就一直不能被GC。另一处会引起内存泄漏的地方是发送给OtherThread的是非靜态的Runnableinnerclass,所以该Runnable对象就持有当前Activity的引用。由于该Runnable执行的任务比较重,所以在OtherThread完成前Activity是无法被GC的。如果OtherThread在执行Runnable任务时用户旋转了手机屏幕,那么旋屏后已经无用的Activity还会存在将近30秒,也就是Activity中所有占用内存在30秒中无法被GC。为了修正上述内存泄漏的问题,需要对例子中的代码做如下修改:

  1. sendMessage完成之后显示的将msg成员变量置为null

msg= h.obtainMessage(101);

h.sendMessage(msg);

msg= null;

  1. Runnable对象改为static类型的内部类,或者不用Runnable对象来进行线程间通讯而是将Runnable的中的功能代码转移到OtherThreadHandler对象的handlerMessage函数中。UI线程和OtherThread只用Message对象来进行通讯。

其它常见的引起内存泄漏的原因

  1. ListAdaptergetView()函数中没有使用参数中的convertView.public View getView(intposition, ViewconvertView,ViewGroupparent) 来向ListView提供每一个item所需要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的view对象,同时ListView会将这些view对象缓存起来。当向上滚动 ListView,原先位于最上面的listitem view对象会被回收,然后被用来构造新出现的最下面的listitem。这个构造过程就是由getView()方法完成的,getView()的第二个形参ViewconvertView就是被缓存起来的listitemview对象(初始化时缓存中没有view对象则convertViewnull)。由此可以看出,如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大,直到最后引发GC。对于2.3版本的Android还没有实现concurrentGC,所以在ListView上下滚动时就会由于虚拟机执行GC而卡住ListView的滚动,这时给用户的感觉就是界面不够流畅。

  2. Bitmap对象不在使用时调用recycle()释放内存,有时我们会手工的操作Bitmap对象,如果一个Bitmap对象比较占内存,当它不在被使用的时候,可以调用Bitmap.recycle()方法回收此对象的像素所占用的内存。

  3. 查询得到的Cursor未能及时关闭。

EclipseMAT工具的使用

如果在应用程序中发生了OOM异常导致的错误,我们需要首先定位一下是否是由于自身程序的原因。方法是重新打开之前出错退出的应用程序,然后在控制台中运行:

$adbshell

然后在打开的adbshell终端中执行:

#procrank

procrank就会输出类似下面的列表:

PID Vss Rss Pss Uss cmdline

935 68544K 42160K 19570K 15840K system_server

1002 37600K 35124K 14912K 12804K oms.home

1221 33828K 33828K 12259K 9440K com.android.phone

2537 31916K 31916K 11510K 9324K com.android.Browser

2956 28768K 28768K 9034K 7152K com.hiapk.market

...

854 268K 268K 89K 84K /system/bin/servicemanager

859 444K 444K 86K 28K /system/bin/sdm

920 268K 268K 85K 80K /system/bin/cbmon

883 404K 404K 84K 28K /system/bin/sdm

857 276K 276K 81K 76K /system/bin/debuggerd

其中最左一例是当前正在运行所有进程的PID,接下来的四个字段分别为VSSVirtualSet Size 虚拟耗用内存(包含共享库占用的内存)RSSResidentSet Size实际使用物理内存(包含共享库占用的内存,如果有两个进程使用一个占用的空间为10Mso,那么这两个进程的RSS就会分别增加10MPSSProportionalSet Size实际使用的物理内存(比例分配共享库占用的内存,如果有两个进程使用一个占用的空间为10Mso,那么这两个进程的RSS就会分别增加5MUSSUniqueSet Size 进程独自占用的物理内存(不包含共享库占用的内存)在上面列表中找到之前应用程序所对应的进程后,再重复之前引起OOM错误的操作,如果发现该进程使用的内存不断的增加(通常是USS段),那么该进程对应的应用程序就很可能存在内存泄漏。在初步定位了内存泄漏之后,可以通过EclipseMAT插件来分析一下该进程中内存使用情况,定位一下内存泄漏的原因。具体的作法如下:

  1. 将开发机通过USB连接至电脑,或者打开PC上的虚拟机,编译应用程序的最新版本,之后Push到开发机上。

  2. 切换到eclipseDDMS视图,在Devices列表中点选嫌疑应用程序的进程,再点击Devices视图界面中最上方一排图标中的“UpdateHeap”图标。再点击Heap视图中的“CauseGC”按钮,此时虚拟机会执行一次GC,来清除不被引用的对象。

  3. 执行之前引起OOM错误的操作,在操作时观察Heap视图中的dataojbect 一行占用内存的大小是否一直增长,在操作时可以随时点击Devices视图界面中最上方一排图标中的“DumpHPROF file”按钮来产生内存使用情况快照。

  4. 如果应用程序所占用的空间一直增长,而且中间的GC操作也无法减少内存占用,或者应用程序在操作过程中引起了OOM,这时就需要分析之前的HPROF文件。

  5. hprof视图的饼状图中查看当前内存的占用情况,通常Resource类会占用大量空间,但这是正常的。点击占用了大量内存的可疑对象,再选择“pathto GC root”,查看哪些对象持有对该可疑对象的引用造成其无法被GC。或者切换到dominatortree视图中,查看按占用内存从大到小排序的对象列表中是否有可疑的对象,如果有的话再右键点击该对象,选择“Pathto GC root”,选择“Execuldeweak/soft references”,查看是哪些对象持有对该可疑对象的引用。

  6. 如果已经定位了是哪一个类的对象引起的内存泄漏,但是需要知道关于这些对象更多的信息,可以使用MATOQL查询语言来查询hprof文件中各个对象的其它信息来更准确的定位内存泄漏的原因。例如我们可以在OQL界面中输入如下OQL语句:

SELECT* FROM com.test.MemoryLeak.MemoryLeak

再点击界面工具栏中的!运行按钮来执行OQL语句,此语句执行结果中就会列如出所有的MemoryLeak类的对象,我们再对列表中的对象执行“Pathto GC root”来查询是哪些对象让其无法释放。如果MemoryLeak中还有一个成员变量mId来标识该对象的id,我们可以通过如下OQL语句来查询各个对象的mId

SELECTs.mId FROM com.test.MemoryLeak.MemoryLeak s

通过OQL语句可以有更多方法来定位内存泄漏的原因,关于OQL的详细文档,可以查看Eclipse帮助文件中的MemoryAnalyzer一节中的说明。 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值