几周前我有幸参加了在波兰举行的国际移动会议,这是移动开发者最好的会议之一。在“最佳实践”系列演讲中,我的朋友兼同事Jorge Barroso 的一个观点引起了我的注意:
如果你是一个Android开发者,但是你没有使用弱引用,那么你会有麻烦。
刚好,几个月前我和Diego Grancini合作出版了我的上一本书,“Android High Performance”。其中最热门的一章就是讨论Android中的内存管理。在这一章中,我们讨论了在移动设备上内存是怎么工作的,内存泄漏是怎么发生的,内存泄漏这个问题为什么如此重要以及我们需要采取什么技术来避免。自从我从事Android开发以来,我注意到一种倾向,凡是和内存泄漏或者内存管理有关的事情,开发者总是不由自主的回避或者降低其优先级。如果功能需求已经满足,为什么要自寻烦恼?我们总是急于开发新的功能,我们宁愿在我们的下一个Sprint演示中呈现一些视觉效果,而不是关心那些人们不会第一眼就发现的问题。
一个不错的观点就是,这将不可避免的导致技术债务。我甚至还补充一点,技术债务在现实世界也会产生一些影响,这些影响我们无法通过单元测试来衡量:失望、开发者之间扯皮、软件交付质量低下以及工作激情消失。这个影响很难衡量的原因是它们常常发生在未来的某一个时间点。它的发生有一点像政客:如果我仅仅当政8年,我为什么要为第12年发生的事情烦恼?与之不同的是软件开发在飞速发展。
如果要写适用于软件开发的思维模式,需要很长的篇幅,并且已经有很多书和文章可供您探索,然而简单的介绍内存引用的不同类型,它们各自的含义以及怎样把它们应用到Android开发中则容易的多,这就是我在本文中要做得事情。
首先:Java中什么是引用?
引用指向一个已经声明的对象,你可以访问它。
Java默认有4种引用:强引用,软引用,弱引用和虚引用。也有一些人认为只有两种引用,强引用和弱引用,并且弱引用可以表现为两种形式。在生活中,我们倾向于将一切东西像植物学家一样进行分类。不管哪一种分类更适合你,首先你需要理解他们。然后你才能提出你自己的分类方式。
每一种引用的含义是什么?
强引用:强引用是Java中最常见的引用。每当我们创建一个新的对象,默认就创建了一个强引用。比如,但我们写下如下代码:
MyObject object = new MyObject();
当一个MyObject类型的对象被创建,object就持有一个它的强引用。到目前为止,还是比较简单的,你还能跟上吗?那么,接下来会发生更多有趣的事情。这个Object是强可达的-也就是说它可以通过一个强引用链到达。这将阻止垃圾回收器回收并且销毁它,我们最希望的是垃圾回收器及时的回收并销毁它。但是现在让我们一起来看一个例子,这将我和我们想要的不一样。
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
new MyAsyncTask().execute();
}
private class MyAsyncTask extends AsyncTask {
@Override
protected Object doInBackground(Object[] params) {
return doSomeStuff();
}
private Object doSomeStuff() {
//do something to get result
return new MyObject();
}
}
}
花几分钟时间,尽可能找出任何容易出现问题的路径。
不用担心,如果找不到就多花一点时间。
现在呢?
AsyncTask将在Activity的OnCreate()函数中创建并执行。但是这里会有一个问题,内部类在他的整个生命周期中都需要访问外部类。
当Activity被销毁的时候会发生什么事情?AsyncTask持有一个Activity的引用,从而导致Activity不能被垃圾回收器回收。这就是所谓的内存泄漏。
旁记:我以前在面试候选人时,我总是问他们怎样制造一个内存泄漏,而不是问他们关于内存泄漏的理论知识。通常都很有意思。
这里的内存泄漏实际上不仅仅在Activity自身销毁时发生,同样,由于系统配置发生变化或者系统需要更多的内存等而被强制销毁时也会发生。如果这个AsyncTask比较复杂(比如持有Activity里的Views的引用等),这还会引发程序崩溃,因为view的引用是null。
那么怎样才能防止这个问题再次发生? 让我们解释另一种类型的引用:
弱引用:弱引用是指不够强大到让系统保存在内存中的引用。如果我们尝试确定一个对象是否被强引用,而刚好是通过WeakRerences方式引用,那么这该对象将被回收。为了理解,最好是消化掉理论知识,我们通过一个实际的样例来展示怎样通过使用WeakReference来避免上一个例子中的内存泄漏:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new MyAsyncTask(this).execute();
}
private static class MyAsyncTask extends AsyncTask {
private WeakReference<MainActivity> mainActivity;
public MyAsyncTask(MainActivity mainActivity) {
this.mainActivity = new WeakReference<>(mainActivity);
}
@Override
protected Object doInBackground(Object[] params) {
return doSomeStuff();
}
private Object doSomeStuff() {
//do something to get result
return new Object();
}
@Override
protected void onPostExecute(Object object) {
super.onPostExecute(object);
if (mainActivity.get() != null){
//adapt contents
}
}
}
}
注意主要的变化:内部类是通过下面的方式引用Activity的:
private WeakReference<MainActivity> mainActivity;
当Activity需要被销毁时会发生什么事情?由于它是通过弱引用的方式持有,它可以被回收。所以不会有内存泄漏发生。
旁记:现在希望你已经更好的理解了什么是弱引用,你会发现一个非常有用的类 WeakHashMap。 它就是一个HashMap,除了它的keys(注意是key, 不是values)是通过弱引用的方式被引用的。这使得它对于实现高速缓存之类的实体是非常有用的。
我们已经提到了更多的引用。 让我们看看在什么情况下它们是有用的,以及我们如何能够从中受益:
软引用:把软引用看做一个较强的弱引用。软引用会要求垃圾回收将其保留在内存中,如果没有其他选项时才将其回收,而弱引用是被立即回收。垃圾回收算法真的是令人兴奋的东西,所以有时你会花数小时去研究而不知疲倦。但是基本规则就是:”我总是要回收弱引用。如果一个对象是软引用,我会根据系统环境来决定做什么“。这使得软引用对于实现缓存非常有用:只要内存是充裕的,我们不用担心手动移除对象。如果你想查看一个实际的例子,你可以查看这个通过软引用实现的缓存样例。
虚引用:啊哈,虚引用!我想我一只手就能数过来在生产环境中我所见到的虚引用被使用的情形。一个对象如果仅仅被通过虚引用的方式引用,那么它会被垃圾回收器随心所欲的回收。没有更多的解释,没有“召回”。这使得它难以描述。为什么我们会喜欢使用这样一个东西?难道其他的还不够麻烦?为什么我选择成为一名程序员?虚引用可以被用来精准的检测一个对象是否被从内存中删除。我记得我的整个职业生涯中一共使用了两次虚引用。所以如果你现在感觉虚引用很难理解,请不要沮丧。
希望本文能够理清一点点你以前对引用的认识。在任何学习过程中,你也许想开始实践实践,把弄一下自己的代码并看看你如何改进它。首先你需要检查你的代码中是否存在内存泄漏的问题,进而使用你从这些课程中学习到的知识去消除这些令人难受的内存泄漏。如果喜欢本文或者你觉得本文对你有所帮助,请随时分享并/或者留下评论。这是对业余写作者最大的鼓励。
原文链接:Finally understanding how references work in Android and Java