Android性能优化(内存泄露第一篇)
首先我们关注一个内存泄露的场景,相信大家都知道在Android中非静态的内部类或匿名内部类都很有可能造成Context泄露。主要原因就是在某些情况下,Context的生命周期已经走完,但是这些类的生命还未到尽头,而他们又持有Context的引用,导致GC时无法回收该回收的内存空间从而导致类存泄露。
上面这段话应该不难理解,下面就用一些简单的例子说明这个问题。
一、普通内部类或匿名类造成内存泄露
public class SecondActivity extends Activity {
private static final String TAG = "WeakReferenceTest";
private ImageView ivTest;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_2);
ivTest = (ImageView) findViewById(R.id.image);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
ivTest.setImageBitmap(bitmap);
// 匿名内部类会持有外部类的引用
final Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000 * 100);
Log.i(TAG, "This log is from SecondActivity!");
}catch (InterruptedException e){
}
}
});
Button button = (Button) findViewById(R.id.btn_2);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
thread.start();
finish();
}
});
}
}
上面的代码中,有一个匿名的Runnable类让其所在线程sleep 100秒,在这个Activity中有一个ImageView并为其设置了一张图片。我们连续的进行打开->关闭Activity这项操作,发现越到后面卡顿越严重。看下面两张图,这是某两个时刻的内存使用情况(一前一后):
first_time_capture.png
second_time_capture.png
可以发现,在连续进行上述同一操作的时候,程序内存增大了很多!再看看Dalvikvm(4.4以上系统可能是ART)打印的日志:
dalvikvm_log_1.png
GC操作显示当前活动对象占用的内存越来越多,最后直至程序崩溃!这里可以肯定,我们上面写的代码确实造成了内存泄露。就是这个匿名内部类,它持有外部Activity的引用,当我们点击Button开启了线程的同时结束了当前Activvity,此时GC正要回收此Activity占用的内存空间,发现还有对象持有它的引用所以无法进行内存回收;当我们多次进行打开->关闭Activity操作的时候,就导致了内存泄露,最后程序也崩了。
问题来了,如何避免。其实这里相信大家都知道,将其声明为静态的就行,如下:
private static class MyRunnable implements Runnable{
@Override
public void run() {
try {
Thread.sleep(1000 * 100);
Log.i(TAG, "This log is from SecondActivity!");
}catch (InterruptedException e){
}
}
}
// 使用
final Thread thread = new Thread(new MyRunnable());
Button button = (Button) findViewById(R.id.btn_2);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
thread.start();
finish();
}
});
修改后Dalvikvm打印日志如下图:
dalvikvm_log_2.png
程序的内存不在一直飙升,而是稳定在一个范围内。这里的主要原因就在于内部类和静态内部类的区别:
- 静态内部类不同于普通内部类,它不会持有外部类的引用;而普通内部类或匿名类则相反
- 普通内部类或匿名类因为持有外部类的引用,所以可以访问外部类的资源属性成员变量等;静态内部类不行
- 因为普通内部类或匿名类依赖外部类,所以必须先创建外部类,再创建普通内部类或匿名类;而静态内部类随时都可以在其他外部类中随时创建
所以上面的代码中,由于使用的是静态内部类,当外部类Activity需要被GC回收内存时,Activity的引用数为0,所以能被正常回收。
二、Handler造成Context泄露
先看代码:
public class SecondActivity extends Activity {
private static final String TAG = "WeakReferenceTest";
private ImageView ivTest;
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Log.i(TAG, msg.obj.toString());
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_2);
ivTest = (ImageView) findViewById(R.id.image);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
ivTest.setImageBitmap(bitmap);
Message msg = mHandler.obtainMessage();
msg.obj = "This is a message!";
mHandler.sendMessageDelayed(msg, 1000 * 10);
finish();
}
}
当我们写下这段代码的时候,IDE会提示一个警告如下:
提示Handler类应该是静态的,否则可能会发生泄露。
其实这里发生泄露和上面说的普通/匿名内部类是类似的。根据Android的消息机制,每个Message对象都保存着处理其Handler的引用,而在Activity中实例化一个非静态的Handler类,此类又会持有Activity的引用;当消息没处理完或者需要延迟处理就结束了当前Activity,此时Activity引用数不为0,就会造成Context泄露。问题就是这样,对策是不是也同样出来了,将Handler类声明为静态内部类,代码如下:
static class MyHandler extends Handler{
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
}
警告确实没有了,但是问题又来了。一般情况下,我们使用Handler就是为了配合Thread进行耗时操作然后更新UI,但是这里的Handler类是静态内部类,不能访问外部类的成员变量,怎么破!接下来,就该WeakReference派上用场了!
Google对WeakReference介绍不多,下面是官方文档中的介绍(以下”入队”指将该引用加入引用队列(Reference Queen)):
弱引用(WeakReference)是三种引用中间的一种。一旦GC判定一个对象时弱引用可到达,会发生以下情况:
- 有一组引用ref,这组引用包含以下元素:
指向该对象的所有弱引用
所有弱引用指向的软引用/强引用可到达对象
- 所有在这组ref中的引用会被自动清除
- 所以之前被ref引用的对象都可以被析构(回收)
- 在未来的某个时候,ref中所有的引用会根据自己的相应的引用队列(如果有)入队
弱引用在Map中很有用,如果一个弱引用没有被外部任何地方引用,它就会自动被移除。SoftReference和WeakReference的区别就在于对象被回收、引用入队的时间点不同:- 如果一个对象是软引用可到达,那么这个对象会尽可能晚的被回收,这个引用同样会尽可能晚的入队。比如当VM内存不足时这种情形。
- 如果一个对象被判定是弱引用可到达,那么这个对象会尽快被回收,这个引用也会尽快入队。
- 弱引用不能阻挡GC对对象进行回收,由GC决定引用的对象何时回收并且将对象从内存移除
- 使用get()方法获取其引用的对象
介绍完了弱引用,看看我们修改后的代码:
static class MyHandler extends Handler{
private final WeakReference<Context> mWeakReference;
public MyHandler(Context context){
mWeakReference = new WeakReference<Context>(context);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Activity mActivity;
if ((mActivity = (Activity)mWeakReference.get()) != null){
// Activity operation
// ...
}
}
}
这样我们就可以在静态内部类中使用操作Activity。
除了弱引用(WeakReference)和上面稍微提到的软引用(SoftReference),还有强引用(StrongReference
)和虚引用 (PhantomReference)。
软引用(SoftReference)
一旦GC判定一个对象时弱引用可到达,会发生以下情况:
- 有一组引用ref,这组引用包含以下元素:
指向该对象的所有弱引用
所有软引用指向的强引用可到达的对象
- 所有在这组ref中的引用会被自动清除
- 在同一时间或是未来的某一时间,ref中所有的引用会根据自己的相应的引用队列(如果有)入队
- 系统会延迟清除软引用指向的对象,该软引用也会延迟入队,但是再系统抛出OutOfMemoryError异常的时候所有的软引用可到达的对象会被回收。当系统需要回收内存来满足分配,软引用可到达的对象会才会被回收,软引用入队。简单来说就是软引用阻止GC回收其指向的对象的能力相对弱引用强。
软引用上面说到了当内存不足时才会回收这些软引用指向的对象,所以挺适合做缓存用。但是Google可不推荐这么做,因为很多原因限制了它灵活的处理缓存相关的事情。所以关于SoftReference官方文档提到这样一句:Most applications should use an android.util.LruCache instead of soft references. LruCache has an effective eviction policy and lets the user tune how much memory is allotted. 所以要做缓存还是得用LruCache。
强引用(StrongReference)
我们使用的最多的就是强引用,比如一句简单的赋值代码:
Button button = new Button(this); // 创建一个Button对象,并将这个对象的引用存到button中。
虚引用 (PhantomReference)
虚引用是几类引用中最弱的一种,当一个对象被判定是虚引用可到达时,该引用就会被加入到引用队列(也就是当一个对象被回收之后),但是它的指向不会被清除。虚引用适合在一个对象回收前做一些清理操作,因为它比finalize()方法更灵活。
关于Java中的弱引用,这篇文章(译文)关于WeakReference写的很好,推荐。
参考
[Android最佳性能实践][1]
[http://developer.android.com/reference][2]
[1]:http://blog.csdn.net/guolin_blog/article/details/42238633/
[2]:http://developer.android.com/reference