你所问的是一个相当棘手的问题。虽然你可能认为这只是一个问题,但实际上你同时在提出几个问题。我会尽我所能用我所知道的知识来覆盖它,并希望其他人加入其中来补充我可能会遗漏的内容。
嵌套类:介绍
由于我不确定你对Java中的面向对象编程(OOP)有多熟悉,因此我将涉及一些基础知识。嵌套类是指一个类定义包含在另一个类中。基本上有两种类型:静态嵌套类和内部类。它们之间的真正区别是:
静态嵌套类:
- 被视为“顶级”。
- 不需要构造包含类的实例。
- 不能没有显式引用访问包含类成员。
- 有自己的生命周期。
内部嵌套类:
- 总是需要构造包含类的实例。
- 自动具有对包含实例的隐式引用。
- 可以访问容器的类成员而不需要引用。
- 生命周期应该不长于容器的生命周期。
垃圾回收和内部类
垃圾回收是自动的,但尝试根据它是否认为对象正在使用来删除对象。垃圾回收器非常聪明,但并不完美。它只能通过是否有对对象的活动引用来确定某些东西是否正在被使用。
真正的问题在于内部类的生存时间比其容器长。这是由于对包含类的隐式引用。唯一可能发生这种情况的方法是,如果包含类外的对象保留了对内部对象的引用,而不考虑包含对象。
这可能会导致内部对象仍然存活(通过引用),但对包含对象的引用已从所有其他对象中删除。因此,内部对象使包含对象保持活动状态,因为它将始终引用它。这个问题的问题在于,除非编程,否则没有办法返回到包含对象以检查它是否仍然存在。
这个认识最重要的方面是,无论它是在Activity中还是作为可绘制物体,你都必须在使用内部类时进行方法研究,并确保它们永远不会超过容器对象的寿命。幸运的是,如果它不是您代码的核心对象,则泄漏可能相对较小。不幸的是,这些是最难找到的泄漏之一,因为它们很可能会被忽视,直到许多泄漏出现。
解决方案:内部类
- 从包含对象中获取临时引用。
- 允许包含对象是唯一保存内部对象长期引用的对象。
- 使用已有的模式,例如工厂模式。
- 如果内部类不需要访问包含类成员,请考虑将其转换为静态类。
- 无论是否在Activity中,都要谨慎使用。
活动和视图:介绍
活动包含大量信息以便能够运行和显示。活动的特征是必须具有视图。它们还具有某些自动处理程序。无论您是否指定,活动都隐式引用其包含的视图。
为了创建视图,它必须知道在哪里创建以及是否有任何子项,以便可以显示。这意味着每个视图都有对活动的引用(通过getContext()
)。此外,每个视图都保留对其子项的引用(即getChildAt()
)。最后,每个视图都保留对表示其显示的呈现位图的引用。
每当您有对活动(或活动上下文)的引用时,这意味着您可以沿着整个布局层次结构链进行遍历。这就是为什么涉及活动或视图的内存泄漏是如此重要的原因。它可能一次性泄漏大量内存。
活动、视图和内部类
根据上面关于内部类的信息,这些是最常见的内存泄漏,但也是最常避免的。虽然希望内部类直接访问活动类成员,但许多人愿意将它们设为静态以避免潜在问题。活动和视图的问题比这深得多。
泄漏的活动、视图和活动上下文
这归结于上下文和生命周期。有某些事件(例如方向)会杀死活动上下文。由于许多类和方法需要上下文,开发人员有时会尝试通过获取对上下文的引用并保留它来节省一些代码。碰巧我们必须创建的许多对象以运行我们的活动必须存在于活动生命周期之外,以使活动能够执行其所需操作。如果您的任何对象在被销毁时仍具有对活动、其上下文或任何视图的引用,则刚刚泄漏了该活动及其整个视图树。
解决方案:活动和视图
- 尽可能避免对View或Activity做静态引用。
- 所有与Activity上下文的引用应短暂存在(仅在函数执行期间)。
- 如果需要长时间的上下文,请使用Application Context (
getBaseContext()
或 getApplicationContext()
),它们不会隐式保留引用。
- 或者,您可以通过重写 Configuration Changes 来限制 Activity 的销毁。但这不能阻止其他潜在事件摧毁 Activity。虽然您可以 这样做,但您仍然可能希望参考以上做法。
Runnables: 入门介绍
其实Runnables并没有那么糟。我是说,它们可能糟糕,但我们已经遇到了大部分的危险区域。 Runnable是一个异步操作,它执行与创建它的线程无关的任务。大多数 Runnables 是从 UI 线程实例化的。本质上,使用Runnable就是创建了另一个线程,只不过稍微管理得更好一些。如果像标准类一样对待 Runnable 并遵循上述指南,则应该遇到很少的问题。现实情况是,许多开发人员并没有这样做。
为了方便起见、易读性和逻辑程序流程,许多开发人员使用匿名内部类来定义他们的 Runnables,例如您上面创建的示例。这会导致像您键入的那个示例一样的情况。匿名内部类基本上就是离散的内部类。您只需要重写适当的方法而不必创建一个全新的定义。在其他方面它也是一个内部类,这意味着它保留了对其容器的隐式引用。
Runnables和Activities/Views
耶!这一节很短!由于 Runnables 运行在当前线程之外,因此这些的危险就在于长时间运行的异步操作。如果将 Runnable 定义为 Activity 或 View 中的匿名内部类或嵌套内部类,则存在一些非常严重的风险。这是因为,如先前所述,它已经知道它的容器是谁。进入方向更改(或系统杀死)。现在只需回到前面的章节以理解刚刚发生了什么。是的,你的示例非常危险。
解决方案: Runnables
- 尽可能扩展Runnable,如果不破坏代码逻辑。
- 尽最大努力使扩展的 Runnables 静态化,如果它们必须是嵌套类。
- 如果必须使用匿名Runnables,请避免在正在使用的任何对象中创建它们长期引用 Activity 或 View。
- 许多 Runnable 可以很容易地成为 AsyncTask。考虑使用 AsyncTask,因为它们默认由 VM 管理。
回答最后一个问题
现在,要回答这些问题,这些问题并没有被本文的其他部分直接提到。你问,“内部类的对象何时可以比它的外部类存活更长?” 在我们回答这个问题之前,让我再次强调:虽然你在Activity中担心这个问题是正确的,但它可能会导致泄漏。我将提供一个简单的示例(不使用Activity)来说明。
下面是一个基本工厂的常见示例(缺少代码)。
public class LeakFactory
{
int myID = 0;
public Leak createLeak()
{
return new Leak();
}
public class Leak
{
int size = 1;
}
}
这个例子并不常见,但足够简单以示范。关键在于构造函数...
public class SwissCheese
{
public Leak[] myHoles;
public SwissCheese()
{
LeakFactory _holeDriller = new LeakFactory()
myHoles = new Leak[1000];
for (int i = 0; i++; i<1000)
{
myHoles[i] = _holeDriller.createLeak();
}
}
}
现在,我们有泄漏(Leaks),但没有工厂。即使我们发布了工厂,它也会保留在内存中,因为每个泄漏都有一个对它的引用。即使外部类没有数据也无所谓。这种情况比人们想象的要频繁得多。我们不需要创建者,只需要它的创造物。因此,我们暂时创建一个,但无限期地使用创造物。
想象一下当我们稍微改变构造函数时会发生什么。
public class SwissCheese
{
public Leak[] myHoles;
public SwissCheese()
{
myHoles = new Leak[1000];
for (int i = 0; i++; i<1000)
{
myHoles[i] = new LeakFactory().createLeak();
}
}
}
现在,所有这些新的LeakFactory都已经泄漏了。你对此有何看法?这些都是内部类如何比任何类型的外部类更长寿的两个非常普遍的例子。如果该外部类是一个Activity,想象一下会更糟糕。
结论:
这些列出了不适当使用这些对象的主要危险。总的来说,本文应该已经回答了大部分您的问题,但我知道这篇文章很长,如果您需要澄清,请告诉我。只要您遵循上述做法,就不必担心内存泄漏的问题。