aoi学院

Aisaka's Blog, School of Aoi, Aisaka University

Android-性能优化-内存泄漏

Android内存泄漏原因

从Java上看,主要有3个原因:

  1. listener:设置监听,在一个类中设置成员变量并把另一个类作为listener给set进去:mListener = setListener(listener);,但在不需要时没有set为null,导致一直持有该对象强引用无法被回收。外部的类没有被回收,所以内部的listener也没有被回收。

  2. inner-class:内部类持有外部的引用,例如持有Acticity的this,解决方法是对应的需要什么就传什么,避免持有外部引用无法被销毁。

  3. thread:无论是内部类的thread还是外部的thread,根本原因还是由1、2造成,例如等待网络回调等,导致持有对象无法被销毁。

从Android上看,主要有2个原因:

  1. 全局进程(process-global)的static变量。这个无视应用的状态,持有Activity的强引用。

  2. 活在Activity生命周期之外的线程。没有清空对Activity的强引用。


Android内存泄漏的可能的八种情况

以下情况需关注是否会内存泄漏:Static Activities、Static Views、Inner Classes、Anonymous Classes、Handler、Threads、TimerTask、Sensor Manager

一般内存泄漏:忘记释放分配的内存,例如cursor

逻辑内存泄漏:当应用不再需要这个对象或结束生命周期,仍未释放该对象的所有引用

在Android中,导致潜在内存泄漏的陷阱:

  1. 全局进程的static变量,这个无视应用的状态,持有Activity的强引用

  2. 活在Activity生命周期外的线程,没有清空对Activity的强引用

Static Activities

在类中定义了静态Activity变量,把当前运行的Activity实例赋值于这个静态变量。这个静态变量在Activity生命周期结束后没有被清空就导致了内存泄漏,因为static变量是贯穿这个应用的生命周期的,所以被泄漏的Activity就会一直存在于应用的进程中,不会被垃圾回收器回收。

1
2
3
4
5
6
7
8
9
10
11
12
static Activity activity;
void setStaticActivity() {
activity = this;
}
View sButton = findViewById(R.id.button);
sButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setStaticActivity();
nextActivity();
}
});

解决办法:弱引用不会阻止对象的内存释放,所以即使有弱引用的存在,该对象也可以被回收。

1
2
3
4
private static WeakReference<MainActivity> activityReference;
void setStaticActivity() {
activityReference = new WeakReference<MainActivity>(this);
}

Static Views

常发生在单例模式中,如果Activity经常被用到,那么在内存中保存一个实例是很实用的。但脱离Activity生命周期的操作是很危险的。

特殊情况:如果一个View初始化耗费大量资源,而且在一个Activity生命周期内保持不变,那么可以把它变成static加载到视图树上(View Hierarchy)。但在Activity被onDestroy()时,应当将这个static view置null。(虽然但是,不建议使用static view)

1
2
3
4
5
6
7
8
9
10
11
12
static View view;
void setStaticView() {
view = findViewById(R.id.button);
}
View sButton = findViewById(R.id.button);
sButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setStaticView();
nextActivity();
}
});

解决办法:弱引用是个有效的解决方法,然而还有另一种方法是在生命周期结束时清除引用,Activity#onDestroy()方法就很适合把引用置空。

1
2
3
4
5
6
7
8
9
10
11
private static View view;
@Override
public void onDestroy() {
super.onDestroy();
if (view != null) {
unsetStaticView();
}
}
void unsetStaticView() {
view = null;
}

Inner Classes

Activity中有个内部类,这样做可以提高可读性和封装性。但如果这个内部类持有一个静态变量的引用,便很容易内存泄漏(除非销毁时置null)。内部类的优势之一就是可以访问外部类,不幸的是,导致内存泄漏的原因,就是内部类持有外部类实例的强引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static Object inner;
void createInnerClass() {
class InnerClass {

}
inner = new InnerClass();
}
View icButton = findViewById(R.id.ic_button);
icButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
createInnerClass();
nextActivity();
}
});

解决办法:开发者必须注意少用非静态内部类,因为非静态内部类持有外部类的隐式引用,容易导致意料之外的泄漏。然而内部类可以访问外部类的私有变量,只要我们注意引用的生命周期,就可以避免意外的发生。避免静态变量。

1
2
3
4
5
6
7
private Object inner;
void createInnerClass() {
class InnerClass {

}
inner = new InnerClass();
}

Anonymous Classes

如果匿名类也维护了外部类的引用,在Activity中定义了匿名的AsyncTask,就容易发生内存泄漏。当异步任务在后台执行耗时任务期间,Activity被销毁了(用户退出、系统回收),这个被AsyncTask持有的Activity实例就不会被GC回收,直到异步任务结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void startAsyncTask() {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
while(true);
}
}.execute();
}
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View aicButton = findViewById(R.id.at_button);
aicButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startAsyncTask();
nextActivity();
}
});

解决办法:匿名类和内部类一样,只要不跨越生命周期是完全没问题的。但是,这些类是用于产生后台线程的,这些Java线程是全局的,而且持有创建者的引用(即匿名类的引用),而匿名类又持有外部类的引用。线程是可能长时间运行的,所以一直持有Activity的引用导致当销毁时无法回收。我们不能通过移除静态成员变量解决,因为线程是于应用生命周期相关的。为了避免泄漏,必须舍弃简洁偷懒的写法,把子类声明为静态内部类。(静态内部类不持有外部类的引用,打破了链式引用。)

1
2
3
4
5
6
7
8
9
private static class NimbleTask extends AsyncTask<Void, VoiVoid> {
@Override
protected Void doInBackground(Void... params) {
while(true);
}
}
void startAsyncTask() {
new NimbleTask().execute();
}

Handler

定义了匿名的Runnable,用匿名类Handler执行。Runnable内部类会持有外部类的隐式引用,被传递到Handler的消息队列MessageQueue中,在Message消息没有被处理之前,Activity销毁导致了内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void createHandler() {
new Handler() {
@Override
public void handleMessage(Message message) {
super.handleMessage(message);
}
}.postDelayed(new Runnable() {
@Override
public void run() {
while(true);
}
}, Long.MAX_VALUE >> 1);
}
View hButton = findViewById(R.id.h_button);
hButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
createHandler();
nextActivity();
}
});

解决办法:匿名类和内部类一样,只要不跨越生命周期是完全没问题的。但是,这些类是用于产生后台线程的,这些Java线程是全局的,而且持有创建者的引用(即匿名类的引用),而匿名类又持有外部类的引用。线程是可能长时间运行的,所以一直持有Activity的引用导致当销毁时无法回收。我们不能通过移除静态成员变量解决,因为线程是于应用生命周期相关的。为了避免泄漏,必须舍弃简洁偷懒的写法,把子类声明为静态内部类。(静态内部类不持有外部类的引用,打破了链式引用。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static class NimbleHandler extends Handler {
@Override
public void handleMessage(Message message) {
super.handleMessage(message);
}
}
private static class NimbleRunnable implements Runnable {
@Override
public void run() {
while(true);
}
}
void createHandler() {
new NimbleHandler().postDelayed(new NimbleRunnable(), Long.MAX_VALUE >> 1);
}

Threads

通过Thread和TimerTask来展现内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void spawnThread() {
new Thread() {
@Override public void run() {
while(true);
}
}.start();
}
View tButton = findViewById(R.id.t_button);
tButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
spawnThread();
nextActivity();
}
});

解决办法:使用静态内部类,不持有外部类的引用,打破了链式引用。但是,如果坚持使用匿名类,只要在生命周期结束时中断线程就可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private Thread thread;
@Override
public void onDestroy() {
super.onDestroy();
if (thread != null) {
thread.interrupt();
}
}
void spawnThread() {
thread = new Thread() {
@Override
public void run() {
while (!isInterrupted()) {
}
}
};
thread.start();
}

TimerTask

只要是匿名类的实例,不管是不是在工作线程,都会持有Activity的引用,导致内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void scheduleTimer() {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
while(true);
}
}, Long.MAX_VALUE >> 1);
}
View ttButton = findViewById(R.id.tt_button);
ttButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
scheduleTimer();
nextActivity();
}
});

解决办法:匿名类和内部类一样,只要不跨越生命周期是完全没问题的。但是,这些类是用于产生后台线程的,这些Java线程是全局的,而且持有创建者的引用(即匿名类的引用),而匿名类又持有外部类的引用。线程是可能长时间运行的,所以一直持有Activity的引用导致当销毁时无法回收。我们不能通过移除静态成员变量解决,因为线程是于应用生命周期相关的。为了避免泄漏,必须舍弃简洁偷懒的写法,把子类声明为静态内部类。(静态内部类不持有外部类的引用,打破了链式引用。)

1
2
3
4
5
6
7
8
9
private static class NimbleTimerTask extends TimerTask {
@Override
public void run() {
while(true);
}
}
void scheduleTimer() {
new Timer().schedule(new NimbleTimerTask(), Long.MAX_VALUE >> 1);
}

Sensor Manager

通过Context.getSystemService(int name)可以获取系统服务。这些服务工作在各自的进程中,帮助应用处理后台任务,处理硬件交互。如果需要使用这些服务,可以注册监听器,这会导致服务持有了Context的引用,如果在Activity销毁的时候没有注销这些监听器,会导致内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
void registerListener() {
SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}
View smButton = findViewById(R.id.sm_button);
smButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
registerListener();
nextActivity();
}
});

解决办法:在Activity结束时注销监听器

1
2
3
4
5
6
7
8
9
10
11
12
private SensorManager sensorManager;
private Sensor sensor;
@Override
public void onDestroy() {
super.onDestroy();
if (sensor != null) {
unregisterListener();
}
}
void unregisterListener() {
sensorManager.unregisterListener(this, sensor);
}