Activitys, Threads, & Memory Leaks
在Android编程中,一个公认的难题是在Activity的生命周期如何协调长期运行的任务和避免有可能出现的内存泄漏问题。考虑下面一段代码,在Activity创建时启动了一个线程,在线程中无限循环。
/**
* Example illustrating how threads persist across configuration
* changes (which cause the underlying Activity instance to be
* destroyed). The Activity context also leaks because the thread
* is instantiated as an anonymous class, which holds an implicit
* reference to the outer Activity instance, therefore preventing
* it from being garbage collected.
*/
publicclass MainActivity extends Activity {
@Override
protectedvoid onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
exampleOne();
}
privatevoid exampleOne() {
newThread() {
@Override
publicvoid run() {
while(true) {
SystemClock.sleep(1000);
}
}
}.start();
}
}
当手机配置发生改变时(例如手机朝向发生改变),会导致整个Activity销毁和重新创建。我们很容易认为Andriod会为我们做好一切清理工作,回收与Activity相关联的内存和正在运行的线程。但是,事情并不和我们想象中的一样!Activity相关联的内存和正在运行的内存都不会回收,会发生泄漏,一个直接的结果就是程序的性能越来越差。
Activity是如何泄漏的
如果读过LZ的前一篇文章会发现一个非静态的匿名类会持有外部类的一个隐式引用(上述代码中的MainActivity),只要非静态的匿名类对象没有被回收,MainActivity就不会被回收,MainActivity所关联的资源和视图都不会被回收,发生比较严重的内存泄漏。要解决MainActivity的内存泄漏问题,只需把非静态的Thread匿名类定义成静态的内部类就行了(静态的内部类不会持有外部类的一个隐式引用),如下代码所示
/**
* This example avoids leaking an Activity context by declaring the
* thread as a private static inner class, but the threads still
* continue to run even across configuration changes. The DVM has a
* reference to all running threads and whether or not these threads
* are garbaged collected has nothing to do with the Activity lifecycle.
* Active threads will continue to run until the kernel destroys your
* application's process.
*/
publicclass MainActivity extends Activity {
@Override
protectedvoid onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
exampleTwo();
}
privatevoid exampleTwo() {
newMyThread().start();
}
privatestatic class MyThread extends Thread {
@Override
publicvoid run() {
while(true) {
SystemClock.sleep(1000);
}
}
}
}
现在新创建的Thread不会持有MainActivity的一个隐式引用,当手机朝向发生改变时不会阻止垃圾回收器对旧MainActivity的回收工作~
Thread是如何泄漏的
在提到的第二个问题中,一旦一个新的Activity创建,那么就有一个Thread永远得不到清理回收,发生内存泄漏。Threads在Java中是GC roots;意味着
Dalvik Virtual Machine (DVM) 会为所有活跃的threads在运行时系统中保持一个硬引用,这会导致threads一直处于运行状态,垃圾收集器将永远不可能回收它。出于这个原因,我们应当为threads添加一个结束的逻辑,如下代码所示
/**
* Same as example two, except for this time we have implemented a
* cancellation policy for our thread, ensuring that it is never
* leaked! onDestroy() is usually a good place to close your active
* threads before exiting the Activity.
*/
publicclass MainActivity extends Activity {
privateMyThread mThread;
@Override
protectedvoid onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
exampleThree();
}
privatevoid exampleThree() {
mThread = new MyThread();
mThread.start();
}
/**
* Static inner classes don't hold implicit references to their
* enclosing class, so the Activity instance won't be leaked across
* configuration changes.
*/
privatestatic class MyThread extends Thread {
privateboolean mRunning = false;
@Override
publicvoid run() {
mRunning = true;
while(mRunning) {
SystemClock.sleep(1000);
}
}
publicvoid close() {
mRunning = false;
}
}
@Override
protectedvoid onDestroy() {
super.onDestroy();
mThread.close();
}
}
在上述的代码中,当Activity结束销毁时在onDestroy()方法中结束了新创建的线程,保证了thread不会发生泄漏。但是如果你想在手机配置发生改变时保持原有的线程而不重新创建的话,你可以考虑使用fragment来保留原有的线程,以备下一次使用具体做法可以参考我之前的一篇文章
http://blog.csdn.net/tu_bingbing/article/details/9274289,关于这方面
APIdemos 中也做了相关的实现。
结论
在Android中,长时间运行的任务和Acyivity生命周期进行协调会有点困难,如果你不加以小心的话会导致内存泄漏。关于如何处理这个棘手的问题,下面有几个基本的技巧供参考
1、使用静态内部类/匿名类,不要使用非静态内部类/匿名类.非静态内部类/匿名类会隐式的持有外部类的引用,外部类就有可能发生泄漏。而静态内部类/匿名类不会隐式的持有外部类引用,外部类会以正常的方式回收,如果你想在静态内部类/匿名类中使用外部类的属性或方法时,可以显示的持有一个弱引用。
2、不要以为Java永远会帮你清理回收正在运行的threads.在上面的代码中,我们很容易误以为当Activity结束销毁时会帮我们把正在运行的thread也结束回收掉,但事情永远不是这样的!Java threads会一直存在,只有当线程运行完成或被杀死掉,线程才会被回收。所以我们应该养成为thread设置退出逻辑条件的习惯。
3、适当的考虑下是否应该使用线程.Android应用框架设计了许多的类来简化执行后台任务,我们可以使用与Activity生命周期相关联的Loaders来执行简短的后台查询任务。如果一个线程不依赖与Activity,我们还可以使用Service来执行后台任务,然后用BroadcastReceiver来向Activity报告结果。另外需要注意的是本文讨论的thread同样使用于AsyncTasks,AsyncTask同样也是由线程来实现,只不过使用了Java5.0新增并发包中的功能,但同时需要注意的是根据官方文档所说,AsyncTask适用于执行一些简短的后台任务