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
一节中的说明。