几周前,我参加了 Mobiconf,这是我在波兰参加过的最好的移动开发者会议之一。在他的多元化演讲“最佳(好)实践”中,我的朋友和同事 Jorge Barroso 提出了一种观点,听后让我反思:
如果你是一名 Android 开发者,而且你不使用 WeakReferences,那么你就有一个问题。
巧合的是,几个月前我出版了我的最后一本书《Android 高性能》,与 Diego Grancini 合著。其中最热情的一章是关于 Android 中的内存管理。在这一章中,我们谈论了移动设备中内存的工作方式、内存泄漏发生的原因,以及我们可以应用哪些技术来避免它们。自从我开始开发 Android 以来,我一直观察到一种倾向,即不自觉地避免或低优先级地处理与内存泄漏和内存管理有关的一切。如果功能标准得到满足,为什么还要费心呢?我们总是急于开发新功能,宁愿在下一个迭代演示中展示一些视觉效果,而不愿关注那些人们不会一眼看到的东西。
这是一个不错的观点,无可挽回地导致了 技术债务 的产生。我甚至可以补充说,技术债务在现实世界中也会产生一些我们无法通过单元测试衡量的影响:失望、开发人员之间的摩擦、低质量的软件发布以及动力的丧失。之所以这些影响难以衡量,是因为它们通常发生在长期内。这有点像政治家的情况:如果我只负责8年,为什么我要担心12年后会发生什么?只不过在软件开发中,一切都发生得更快。
写关于在软件开发中采用适当的心态的文章可能需要几卷,而且已经有很多书籍和文章可以供你探索。然而,简要解释不同类型的内存引用、它们的含义以及如何在 Android 中应用它们将是一个更简短的任务,这就是我想在本文中做的事情。
首先:Java 中的引用是什么?
引用是一个被注释的对象的方向,因此你可以访问它。
Java 默认有 4 种引用类型:强引用、软引用、弱引用和虚引用。有人认为只有强引用和弱引用两种类型,而弱引用可以呈现 2 种弱度。我们倾向于用植物学家的分类方法对生活中的一切进行分类。不管哪种方法对你来说更好,但首先你需要了解它们。然后你可以找出自己的分类方法。
每种引用类型的含义是什么?
强引用:强引用是 Java 中的普通引用。每次我们创建一个新对象时,都会默认创建一个强引用。例如,当我们执行以下操作时:
MyObject object = new MyObject();
一个新的对象 MyObject 被创建,它的强引用被存储在 object 中。到目前为止很容易理解,你还跟得上吗?好的,现在更有趣的事情来了。这个 object 是强可达的,也就是说,它可以通过一系列强引用到达。这将防止垃圾回收器将其拾取并销毁,这正是我们最想要的。但现在,让我们看一个例子,这可能会对我们产生反作用。
public class MainActivity extends Activity {
private AsyncTask task;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
task = new AsyncTask() {
@Override
protected Object doInBackground(Object[] params) {
// do something
return null;
}
};
}
}
请花几分钟时间,试着找出可能有问题的方法。
如果你找不出来,不用担心,多花点时间也没关系。
这个 AsyncTask 将与 onCreate() 方法一起创建和执行。但是,我们在这里遇到了一个问题:内部类在其整个生命周期中需要访问外部类。
当 Activity 被销毁时会发生什么?AsyncTask 持有对 Activity 的引用,而 GC 无法收集 Activity。这就是我们所说的内存泄漏。
旁注:在我之前的生活中,我曾经进行过对未来候选人的面试,我会问他们如何创建内存泄漏,而不是问关于什么是内存泄漏的理论方面。那总是更有趣!
内存泄漏实际上不仅发生在 Activity 被销毁时,而且当系统由于配置更改或需要更多内存等原因而强制销毁 Activity 时也会发生。如果 AsyncTask 很复杂(即保留对 Activity 中的 View 等引用),它甚至可能导致崩溃,因为 View 引用为空。
那么,如何才能避免这个问题再次发生呢?让我们解释一下另一种引用类型:
弱引用:弱引用是一种不足以使对象保持在内存中的引用。如果我们试图确定对象是否具有强引用,并且它恰好是通过弱引用实现的,那么该对象将被垃圾回收。为了更好地理解,最好放弃理论,通过实际示例来说明如何将先前的代码改为使用 WeakReference 并避免内存泄漏:
public class MainActivity extends Activity {
private WeakReference<MainActivity> reference;
private AsyncTask task;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
reference = new WeakReference<>(this);
task = new AsyncTask() {
@Override
protected Object doInBackground(Object[] params) {
// do something with reference.get()
return null;
}
};
}
}
现在有一个主要的区别:内部类中的 Activity 现在是这样引用的:
reference.get()
会发生什么呢?当 Activity 停止存在时,由于它是通过 WeakReference 保存的,因此它可以被收集。因此不会发生内存泄漏。
旁注:现在你有了对 WeakReference 的更好理解,你会发现 WeakHashMap 类非常有用。它的工作方式与 HashMap 完全相同,除了键(键而不是值)是使用 WeakReference 引用的。这使它们非常适合实现诸如缓存之类的实体。我们还提到了另外几个引用类型,让我们看看它们的用途,以及我们如何受益:
SoftReference:将 SoftReference 视为更强的 WeakReference。虽然 WeakReference 会立即被回收,但 SoftReference 会请求 GC 保留在内存中,除非没有其他选择。Garbage Collector 算法非常精彩,你可以深入研究数小时而不感到疲倦。但基本上,它们说:“我将始终回收 WeakReference。如果对象是 SoftReference,则根据生态系统条件决定该怎么做。”这使得 SoftReference 在实现缓存时非常有用:只要内存足够,我们就不必担心手动删除对象。如果你想看一个实际例子,可以查看使用 SoftReference 实现的此缓存示例。
PhantomReference:噢,幽灵引用!我认为在生产环境中使用它们的次数不足五次。仅通过 PhantomReference 引用的对象可以在垃圾回收器希望的任何时候进行收集。没有进一步的解释,也没有“回电话”。这使得它难以描述。为什么我们想使用这种东西?难道其他的不够问题吗?为什么我选择成为一名程序员?PhantomReference 可以用来检测对象何时从内存中移除。坦白地说,在我的整个职业生涯中,我只有两次使用 PhantomReference 的经历。所以,如果现在难以理解它们,不要感到压力。
评论(0)