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

 

Android

应用程序如何避免内存泄漏以

及如何检查泄漏原因

 

 

Android

的应用程序开发使用的

Java

语言。

Java

语言的

GC

机制使得在堆上分

配内存之后无需再手动的释放内存,而是等待垃圾收集器来收集无用的对象以

回收它们占用的内存。同时在

Android

的进程管理机制中每一个单独的应用程

序在启动时都会创建一个新的

Linux

进程来运行该程序,应用程序在运行中分

配的内存也会在该应用程序退出时随着进程的销毁而释放,所以

Android

中的

内存管理给开发人员造成的负担较轻。但应用程序还需要在内存使用上注意不

要使应用程序占用大量内存,原因有如下两点:

  

1.

 

应用程序占用的内存越少,

Android

可以同时放入内存程序就越多,用

户切换这些不同的程序所消耗的时间就越少,体验就越流畅。

  

2.

 

如果应用程序在消耗光了所有的可用堆空间

(16M

48M

),那么再试图

在堆上分配新对象时就会引起

OOM(Out Of Memory Error)

异常,此时应

用程序就会崩溃退出。

  

所以在编写

Android

应用程序时,仍然需要对应用程序中内存的分配和使用多

加注意,特别是在存在后台线程、使用图片作为背景、在异步任务或者后台线

程中需要

Context

上下文对象的情况下,要注意避免出现对

Activity

View

drawable

等类的对象长期持有无用的

reference

,否则就会造成被引用的对象

无法在

GC

时回收,而是长期占用堆空间,此时就发生了内存泄漏。

  

持有

Context

引用造成的泄漏

 

下面介绍一下

Android

开发文档中

(Avoiding Memory Leak)

的一个内存泄漏的

例子,该例子说明了

Android

应用程序中会引起内存泄漏的常见原因:长期保

持了对

Context

对象的引用。在

Android

应用程序中,很多操作都用到了

Context

对象,但是大多数都是用来加载和访问资源的。这就是为什么所有的

显示控件都需要一个

Context

对象作为构造方法的参数。在

Android

应用程序

中通常可以使用两种

Context

对象:

Activity

Application

。当类或方法需

Context

对象的时候常见的作法是使用第一个作为

Context

参数。但这就意

味着

View

对象对整个

activity

保持引用,因此也就保持对

activity

内的所有

东西的引用,也就是整个

View

结构和它所有的资源都无法被及时的回收,而且

activity

的长期引用是比较隐蔽的。

  

 @Override   

 protected void onCreate(Bundle state) {   

   super.onCreate(state);   

   

   TextView label = new TextView(this);   

   label.setText("Leaks are bad");   

   

   setContentView(label);   

 }   

当屏幕方向改变时,

Android

系统默认作法是会销毁当前的

Activity

,然后创

建一个新的

Activity

,这个新的

Activity

会显示刚才的状态。在这样做的过

程中,

Android

系统会重新加载

UI

用到的资源。现在假设的应用程序中有一个

比较大的

bitmap

类型的图片,每次旋转时都重新加载图片所用的时间较多。为

了提高屏幕旋转时

Activity

的创建速度,最简单的方法是用静态变量的方法。

  

 private static Drawable sBackground;   

   

 @Override   

 protected void onCreate(Bundle state) {   

   super.onCreate(state);   

     

   TextView label = new TextView(this);   

   label.setText("Leaks are bad");   

     

   if (sBackground == null) {   

     sBackground = getDrawable(R.drawable.large_bitmap);   

   }   

   label.setBackgroundDrawable(sBackground);   

     

   setContentView(label);   

 }   

这样的代码执行起来是快速的,但同时是错误的:这样写会一直保持着对

Activity

的引用。当一个

Drawable

对象附属于一个

View

时,这个

View

就相

当于

drawable

对象的一个回调(引用)。在上面的代码片段中,就意味着

drawable

TextView

存在着引用的关系,而

TextView

自己持有了对

Activity

Context

对象)的引用,这个

Activity

又引用了相当多的东西。

  

有两种简单的方法可以避免由引用

context

对象造成的内存泄露。首先第一个

方法是避免

context

对象超出它的作用范围。上面的例子展示了静态引用的情

况,但是在类的内部,隐式的引用外部的类同样的危险。第二种方法是,使用

Application

对象。这个

context

对象会随着应用程序的存在而存在,而不依

赖于

activity

的生命周期。如果你打算对

context

对象保持一个长期的引用,

请记住这个

application

对象。通过调用

Context.getApplicationContext() 

或者

 Activity.getApplication().

方法,你可以很容易的得到这个对象。

  

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

 

Java

语言中可以在一个类的内部定义

inner class

,这在很多时候减少了工作

量,而且使代码更加紧凑,但是由于非静态的

inner class

对象中包含了产生

构造该

inner class

的包围类的引用

,

这样在

Android

环境中也会可能会因为对

包围类的长期引用而造成内存泄漏。

  

我们修改上一个例子的代码,将

sBackground

由静态成员变量改为普通成员变

量,这样就消除了该静态对象持有

Activity

的引用所造成的泄漏。之后我们在

Activity

类中定一个非静态的内部类

Inner

,并且对该

Activity

定义一个

静态变量

innerInstance

。具体代码如下:

  

 public class MemoryLeak extends Activity { 

   class Inner { 

     int someMember; 

   } 

  

   private static Inner innerInstance;  

  

   private Drawable sBackground; 

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

   @Override 

   public void onCreate(Bundle savedInstanceState) { 

     super.onCreate(savedInstanceState); 

    

     TextView label = new TextView(this); 

     label.setText("Leaks are 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

来获

取资源,或者构造

Dialog

Toast

  

2.

 

如果不能控制非静态的内部类的生命周期,尽量在

activity

中避免有非

静态的内部类。同时在

activity

中使用静态的类时如果需要引用

activity,

应该采用

WeakReference

弱引用来引用

Activity

  

后台线程操作引起的泄漏

 

Android

开发中常常需要在后台线程中执行查询或者后台操作。

Android

身也提供了一些常见的异步任务类来简化多线程编程,譬如我们可以使用

AsyncQueryHandler

来执行后台查询任务,使用

AsyncTask

来执行异步的后台

任务。这些

Async

前缀的异步任务类在执行时都会在

UI

线程之外新开了一个线

程来执行。我们在使用这些异步任务类的时,需要注意之前提到的不要将这些

异步任务类的派生类定义为

Activity

non-static inner class

以避免之前

提到的

inner class 

引起的内存泄漏。除此之外,还需要注意不要在这些异步

任务类中持有

Activity

的强引用,而应该采用

WeakReference

来保存

Activity

的引用。这样就不会由于后台线程在执行时

 Activity

对象无法及时

释放。例如在联系人列表

(ContactsListActivity)

中查询联系人的

QueryHandler

类,它作为

AsyncQueryHandler

的派生类主要用来执行查询联系

人数据库的操作,联系人较多的话查询操作用时就会较长,所以采用

AsyncQueryHander

在后台线程中执行查询。在该类的定义中就通过以下两点来

消除内存泄漏:

  

1.

 

QueryHander

定义为

static inner class

,避免

non-static inner 

class

对象持有的

ContactsListAcitivty

对象的引用

  

2.

 

通过

WeakReference

来保存

ContactsListAcitivity

的引用。

  

这样就保证了该后台查询线程不会持用

ContactsListActivity

的强引用,从而

保证了即使后台查询线程正在执行的情况下,

ContactsListAcitivity

在响应

Home

键或者旋转屏幕时也能够被

GC

回收。

  

 private static class QueryHandler extends AsyncQueryHandler { 

   protected final WeakReference<ContactsListActivity> mActivity; 

   protected boolean mLoadingJoinSuggestions = false; 

   ... 

   ... 

   @Override 

   protected void onQueryComplete(int token, Object cookie, Cursor 

cursor) { 

     final ContactsListActivity activity = mActivity.get(); 

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

   ... 

   ... 

与我们正常使用的强引用类型不同

 

,上述示例代码中

WeakReference

并不会增

ContactsListActivity

的引用计数,在使用

WeakReference

类型的

mActivity

变量时,也需要判断其

get()

返回的结果是否为

null

来判断

ContactsListActivity

对象是否还存在。在

Java

语言中,有以下四种引用类

型,

  

1.

 

Strong Reference(

强引用

)

:通常我们编写的代码都是

Strong Ref

,于

此对应的是强可达性,只有去掉强可达,对象才被回收。

  

2.

 

Soft Reference (

软引用

)

:对应软可达性,只要有足够的内存,就一直

保持对象,直到发现内存吃紧且没有

Strong Ref

时才回收对象。一般可

用来实现缓存,通过

java.lang.ref.SoftReference

类实现。

  

3.

 

Weak Reference (

弱引用

)

:比

Soft Ref

更弱,当发现不存在

Strong 

Ref

时,立刻回收对象而不必等到内存吃紧的时候。通过

java.lang.ref.WeakReference

java.util.WeakHashMap

类实现。

  

4.

 

Phantom Reference (

虚引用

)

:根本不会在内存中保持任何对象,你只

能使用

Phantom Ref

本身。一般用于在进入

finalize()

方法后进行特殊

的清理过程,通过

java.lang.ref.PhantomReference

实现。

  

关于这四种不同的类型,可以查看

[

Java

垃圾回收机制与引用类型

]

一文中的详细说

明。

  

在使用

AsyncTask

执行后台任务时,也要注意将其定义为

static inner 

class

,并且在其

doInBackground()

函数中检查

isCancelled()

是否为

true

从而可以尽快的退出后台线程。

Android

开发手册中说明如果需要在后台执行

任务时,尽量采用

AsyncTask

并按照其规范重载相应的钩子函数。但

Java

语言

中传统上使用

Runnable

Thread

来开起新的线程并执行后台任务的作法也是

可以的,只是

Android

并不提倡这样作,特别是在应用程序中通过

Thread

在后

台执行耗时较长的任务。例如下面的示例代码:

  

 new Thread() { 

 @Override 

   public void run() { 

     // do something  

   } 

 } 

或者

  

 Runnable task = new Runnable() { 

   public void run() { 

     // do something  

   } 

 }; 

 Thread t = 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

进行通信时如

果不注意,也很有可能引起内存泄漏。例如线程之间通过

Message

Runnable

对象进行通讯的例子。

  

 private Drawable sBackground; 

 private Message msg; 

  

 @Override 

 protected void onCreate(Bundle savedInstanceState) { 

   super.onCreate(savedInstanceState); 

   TextView label = new TextView(this); 

   label.setText("Leaks are bad"); 

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

   label.setBackgroundDrawable(sBackground); 

   setContentView(label); 

  

   OtherThread t = new OtherThread("other thread"); 

   t.start(); 

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

     public void 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(new Runnable() { 

       public void 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(); 

   } 

 } 

  

 private static class OtherThread extends HandlerThread { 

   OtherThread(String name) { 

   super(name); 

 } 

上述例子代码模拟了

UI

线程同另一线程

OtherThread

之间通过

Handler

进行通

讯的情景,

UI

线程会通过发送

Message

对象和

Runnable

对象来委托

OtherThread

来执行不同的任务,这时

Message

代表的任务是轻量级的任务,

其执行速度很快,这里就是打出两行

Log

。通过

Runnable

对象发送的任务是重

量级的任务,这个任务会执行

30

秒。在上述存在两处会引起内存泄漏的地方,

首先是在

Activity

msg

成员变量在将

Message

发送给

OtherThread

后,仍然

是指向这个已发送的

Message

,尽管

OtherThread

能够很快的处理这一

Message

对象任务并随后释放对该

Message

的引用。但是在

UI

线程中,在发送完该

Message

后会长时间的执行其它任务

(

这里用休眠

3

秒代表执行其它任务

)

,而

在这

3

秒过程中由于

msg

成员变量仍然是指向之前已经完成的

Message

对象。

这就使得该

Message

对象迟迟不能释放。如果

Message

带有大量的信息的话,

那么其占用的内存就一直不能被

GC

。另一处会引起内存泄漏的地方是发送给

OtherThread

的是非靜态的

Runnable inner class

,所以该

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

的中的功能代码转移到

OtherThread

Handler

对象的

handlerMessage

函数中。

UI

线程和

OtherThread

只用

Message

对象来进行通讯。

  

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

 

1.

 

ListAdapter

getView()

函数中没有使用参数中的

convertView. 

public View getView(intposition, 

ViewconvertView,ViewGroupparent) 

来向

 ListView

提供每一个

 item 

所需要的

 view

对象。初始时

ListView

会从

 BaseAdapter 

中根据当前

的屏幕布局实例化一定数量的

view

对象

同时

ListView

会将这些

view

对象缓存起来。

 

当向上滚动

 ListView

,

原先位于最上面的

 list 

item 

 view 

对象会被回收

,

然后被用来构造新出现的最下面的

 list 

item

。这个构造过程就是由

getView()

方法完成的

,getView()

的第二个

形参

 View convertView

就是被缓存起来的

listitem

view

对象

(

初始

化时缓存中没有

view 

对象则

 convertView

 null)

。由此可以看出

,

果我们不去使用

convertView,

而是每次都在

 getView()

中重新实例化一

 View

对象的话

即浪费资源也浪费时间

也会使得内存占用越来越

大,直到最后引发

GC

。对于

2.3

版本的

Android

还没有实现

concurrent 

GC

,所以在

ListView

上下滚动时就会由于虚拟机执行

GC

而卡住

ListView

的滚动,这时给用户的感觉就是界面不够流畅。

  

2.

 

Bitmap 

对象不在使用时调用

 recycle()

释放内存,有时我们会手工的操

 Bitmap 

对象

如果一个

Bitmap 

对象比较占内存

当它不在被使用

的时候

可以调用

Bitmap.recycle()

方法回收此对象的像素所占用的内

存。

  

3.

 

查询得到的

Cursor

未能及时关闭。

  

Eclipse

MAT

工具的使用

 

如果在应用程序中发生了

OOM

异常导致的错误,我们需要首先定位一下是否是

由于自身程序的原因。方法是重新打开之前出错退出的应用程序,然后在控制

台中运行:

  

 $adb shell 

然后在打开的

adb shell

终端中执行:

  

 #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

,接下来的四个字段分别为

 

VSS 

 Virtual Set Size 

虚拟耗用内存(包含共享库占用的内存)

 RSS 

 

Resident Set Size 

实际使用物理内存(包含共享库占用的内存,如果有两个

进程使用一个占用的空间为

10M

so

,那么这两个进程的

RSS

就会分别增加

10M

 PSS 

 Proportional Set Size 

实际使用的物理内存(比例分配共享库

占用的内存,如果有两个进程使用一个占用的空间为

10M

so

,那么这两个进

程的

RSS

就会分别增加

5M

 USS 

 Unique Set Size 

进程独自占用的物理内

存(不包含共享库占用的内存)在上面列表中找到之前应用程序所对应的进程

后,再重复之前引起

OOM

错误

 

的操作,如果发现该进程使用的内存不断的增加

(通常是

USS

段),那么该进程对应的应用程序就很可能存在内存泄漏。

 

在初

步定位了内存泄漏之后,可以通过

Eclipse

MAT

插件来分析一下该进程中内

存使用情况,定位一下内存泄漏的原因。具体的作法如下:

  

1.

 

将开发机通过

USB

连接至电脑,或者打开

PC

上的虚拟机,编译应用程序

的最新版本,之后

Push

到开发机上。

  

2.

 

切换到

eclipse

 DDMS

视图,在

Devices

列表中点选嫌疑应用程序的

进程,再点击

Devices

视图界面中最上方一排图标中的“Update Heap”

图标。再点击

Heap

视图中的“Cause GC”按钮,此时虚拟机会执行一次

GC

,来清除不被引用的对象。

  

3.

 

执行之前引起

OOM

错误的操作,在操作时观察

Heap

视图中的

data 

ojbect 

一行占用内存的大小是否一直增长,在操作时可以随时点击

Devices

视图界面中最上方一排图标中的“Dump HPROF file”按钮来产

生内存使用情况快照。

  

4.

 

如果应用程序所占用的空间一直增长,而且中间的

GC

操作也无法减少内

存占用,或者应用程序在操作过程中引起了

OOM

,这时就需要分析之前

HPROF

文件。

  

5.

 

hprof

视图的饼状图中查看当前内存的占用情况,通常

Resource

类会

占用大量空间,但这是正常的。点击占用了大量内存的可疑对象,再选

择“path to GC root”,查看哪些对象持有对该可疑对象的引用造成其

无法被

GC

。或者切换到

dominator tree

视图中,查看按占用内存从大

到小排序的对象列表中是否有可疑的对象,如果有的话再右键点击该对

象,选择“Path to GC root”,选择“Execulde weak/soft 

references”,查看是哪些对象持有对该可疑对象的引用。

  

6.

 

如果已经定位了是哪一个类的对象引起的内存泄漏,但是需要知道关于

这些对象更多的信息,可以使用

MAT

OQL

查询语言来查询

hprof

文件

中各个对象的其它信息来更准确的定位内存泄漏的原因。例如我们可以

OQL

界面中输入如下

OQL

语句:

  

 SELECT * FROM com.test.MemoryLeak.MemoryLeak 

再点击界面工具栏中的

!

运行按钮来执行

OQL

语句,此语句执行结果中就会列如

出所有的

MemoryLeak

类的对象,我们再对列表中的对象执行“Path to GC 

root” 来查询是哪些对象让其无法释放。如果

MemoryLeak

中还有一个成员变

mId

来标识该对象的

id

,我们可以通过如下

OQL

语句来查询各个对象的

mId

  

 SELECT s.mId FROM com.test.MemoryLeak.MemoryLeak s 

通过

OQL

语句可以有更多方法来定位内存泄漏的原因,关于

OQL

的详细文档,

可以查看

Eclipse

帮助文件中的

Memory Analyzer

一节中的说明。

  

1234567890ABCDEFGHIJKLMNabcdefghijklmn!@#$%^&&*()_+.一三五七九贰肆陆扒拾,。青玉案元夕东风夜放花千树更吹落星如雨宝马雕车香满路凤箫声动玉壶光转一夜鱼龙舞蛾儿雪柳黄金缕笑语盈盈暗香去众里寻他千百度暮然回首那人却在灯火阑珊
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值