我的问题是:如果用户导航或关闭应用程序时会发生什么情况,而我仍然引用用户刚刚在我的AsyncTask中关闭的Activity?
我的猜测是,一旦用户导航就应该销毁它,但是当我出于某种原因在设备上测试它时,我仍然可以在Activity上调用方法,即使它已经消失了!这里发生了什么?
解决方法
内存泄漏
只要像AsyncTask这样的应用程序的某些部分仍然拥有对Activity的引用,它就不会被销毁.它会一直存在,直到AsyncTask完成或以其他方式释放它的引用.这可能会导致非常糟糕的后果,例如您的应用程序崩溃,但最糟糕的后果是您没有注意到的:您的应用程序可能会继续引用应该在很久以前发布的活动以及每次用户执行任何泄漏活动的内存在设备上可能会变得越来越充实,直到看似无处不在Android杀死你的应用程序以消耗太多内存.内存泄漏是我在Stack Overflow上的Android问题中看到的最常见和最严重的错误
解决方案
但是,避免内存泄漏非常简单:您的AsyncTask永远不应该引用Activity,Service或任何其他UI组件.
而是使用侦听器模式并始终使用WeakReference.永远不要强烈引用AsyncTask之外的东西.
几个例子
引用AsyncTask中的视图
正确实现的使用ImageView的AsyncTask可能如下所示:
public class ExampleTask extends AsyncTask<Void,Void,Bitmap> { private final WeakReference<ImageView> mImageViewReference; public ExampleTask(ImageView imageView) { mImageViewReference = new WeakReference<>(imageView); } @Override protected Bitmap doInBackground(Void... params) { ... } @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); final ImageView imageView = mImageViewReference.get(); if (imageView != null) { imageView.setImageBitmap(bitmap); } } }
这完全说明了WeakReference的功能. WeakReferences允许他们引用的Object被垃圾收集.所以在这个例子中,我们在AsyncTask的构造函数中为ImageView创建一个WeakReference.然后在onPostExecute()中可能会在10秒后调用,当ImageView不再存在时,我们在WeakReference上调用get()来查看ImageView是否存在.只要get()返回的ImageView不为null,那么ImageView就不会被垃圾收集,因此我们可以毫无顾虑地使用它!在此期间,用户应该退出应用程序,然后ImageView立即有资格进行垃圾收集,如果AsyncTask稍后完成,则会看到ImageView已经消失.没有内存泄漏,没有问题.
使用听众
public class ExampleTask extends AsyncTask<Void,Bitmap> { public interface Listener { void onResult(Bitmap image); } private final WeakReference<Listener> mListenerReference; public ExampleTask(Listener listener) { mListenerReference = new WeakReference<>(listener); } @Override protected Bitmap doInBackground(Void... params) { ... } @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); final Listener listener = mListenerReference.get(); if (listener != null) { listener.onResult(bitmap); } } }
这看起来很相似,因为它实际上非常相似.您可以在Activity或Fragment中使用它:
public class ExampleActivty extends AppCompatActivity implements ExampleTask.Listener { private ImageView mImageView; ... @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... new ExampleTask(this).execute(); } @Override public void onResult(Bitmap image) { mImageView.setImageBitmap(image); } }
或者您可以像这样使用它:
public class ExampleFragment extends Fragment { private ImageView mImageView; private final ExampleTask.Listener mListener = new ExampleTask.Listener() { @Override public void onResult(Bitmap image) { mImageView.setImageBitmap(image); } }; @Override public void onViewCreated(View view,Bundle savedInstanceState) { super.onViewCreated(view,savedInstanceState); new ExampleTask(mListener).execute(); } ... }
WeakReference及其在使用侦听器时的后果
然而,你必须要注意另一件事.仅对侦听器具有WeakReference的结果.想象一下,你实现了这样的监听器接口:
private static class ExampleListener implements ExampleTask.Listener { private final ImageView mImageView; private ExampleListener(ImageView imageView) { mImageView = imageView; } @Override public void onResult(Bitmap image) { mImageView.setImageBitmap(image); } } public void doSomething() { final ExampleListener listener = new ExampleListener(someImageView); new ExampleTask(listener).execute(); }
相当不寻常的方式 – 我知道 – 但类似的东西可能会在你不知情的情况下偷偷进入你的代码,后果可能很难调试.你有没有注意到上面的例子可能有什么问题?尝试搞清楚,否则继续阅读下面.
问题很简单:您创建一个ExampleListener的实例,其中包含对ImageView的引用.然后将其传递给ExampleTask并启动任务.然后doSomething()方法完成,因此所有局部变量都有资格进行垃圾回收.传递给ExampleTask的ExampleListener实例没有强引用,只有一个WeakReference.因此,ExampleListener将被垃圾收集,当ExampleTask完成时,不会发生任何事情.如果ExampleTask执行得足够快,垃圾收集器可能还没有收集ExampleListener实例,所以它可能在某些时候工作或根本不工作.像这样的调试问题可能是一场噩梦.因此,故事的寓意是:始终注意您的强弱参考以及何时对象有资格进行垃圾回收.
嵌套类和使用静态
另一件可能是大多数内存泄漏的原因我在Stack Overflow上看到的人以错误的方式使用嵌套类.请查看以下示例,并尝试在以下示例中找出导致内存泄漏的原因:
public class ExampleActivty extends AppCompatActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... final ImageView imageView = (ImageView) findViewById(R.id.image); new ExampleTask(imageView).execute(); } public class ExampleTask extends AsyncTask<Void,Bitmap> { private final WeakReference<ImageView> mListenerReference; public ExampleTask(ImageView imageView) { mListenerReference = new WeakReference<>(imageView); } @Override protected Bitmap doInBackground(Void... params) { ... } @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); final ImageView imageView = mListenerReference.get(); if (imageView != null) { imageView.setImageAlpha(bitmap); } } } }
你看到了吗?这是另一个完全相同问题的例子,它只是看起来不同:
public class ExampleActivty extends AppCompatActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... final ImageView imageView = (ImageView) findViewById(R.id.image); final Thread thread = new Thread() { @Override public void run() { ... final Bitmap image = doStuff(); imageView.post(new Runnable() { @Override public void run() { imageView.setImageBitmap(image); } }); } }; thread.start(); } }
你弄清楚问题是什么吗?我每天都会看到人们在不知道自己做错了什么的情况下不经意地实施上述内容.问题是Java如何基于Java的基本特征工作的结果 – 没有任何借口,实现上述内容的人要么喝醉了,要么对Java一无所知.让我们简化问题:
想象一下,你有一个这样的嵌套类:
public class A { private String mSomeText; public class B { public void doIt() { System.out.println(mSomeText); } } }
当你这样做时,你可以从类B内部访问类A的成员.这就是doIt()如何打印mSomeText,它可以访问A甚至私有的所有成员.
您可以这样做的原因是,如果您嵌套类,那么Java隐式地创建了对B内部的A的引用.这是因为该引用而没有其他任何东西可以访问B内部的所有A成员.但是在如果您不知道自己在做什么,内存泄漏的上下文会再次造成问题.考虑第一个例子(我将剥离与示例无关的所有部分):
public class ExampleActivty extends AppCompatActivity { public class ExampleTask extends AsyncTask<Void,Bitmap> { ... } }
所以我们将AsyncTask作为Activity中的嵌套类.由于嵌套类不是静态的,我们可以访问ExampleTask中的ExampleActivity的成员.这里没有关系,ExampleTask实际上并不访问Activity中的任何成员,因为它是一个非静态嵌套类,Java隐式地创建了一个对ExampleTask内部Activity的引用,因此看似没有明显的原因我们有内存泄漏.我们该如何解决这个问题?其实很简单.我们只需要添加一个单词,这是静态的:
public class ExampleActivty extends AppCompatActivity { public static class ExampleTask extends AsyncTask<Void,Bitmap> { ... } }
在一个简单的嵌套类中,这个缺少的关键字就是内存泄漏和完全正常的代码之间的区别.真的尝试在这里理解这个问题,因为它是Java工作的核心,理解这一点至关重要.
至于Thread的另一个例子?完全相同的问题,像这样的匿名类也只是非静态嵌套类,并立即发生内存泄漏.然而它实际上差了百万倍.从各个角度来看,Thread的例子都是糟糕的代码.不惜一切代价避免.
所以我希望这些例子可以帮助您理解问题以及如何编写没有内存泄漏的代码.如果您有任何其他问题,请随时提出.