什么时候可以安全地使用(匿名)内部类?
我一直在阅读一些关于Android内存泄漏的文章,并从Google I / O 上观看了这个有趣的video。
尽pipe如此,我还是不完全理解这个概念,特别是当它对Activity中的用户内部类是安全的或危险的时候 。
这就是我所理解的:
如果一个内部类的实例比它的外部类生存得更久(一个Activity),就会发生内存泄漏。 – > 哪种情况可以发生?
在这个例子中,我想没有泄漏的风险,因为没有办法延长OnClickListener
的匿名类的活动时间比活动,对不对?
final Dialog dialog = new Dialog(this); dialog.setContentView(R.layout.dialog_generic); Button okButton = (Button) dialog.findViewById(R.id.dialog_button_ok); TextView titleTv = (TextView) dialog.findViewById(R.id.dialog_generic_title); // *** Handle button click okButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { dialog.dismiss(); } }); titleTv.setText("dialog title"); dialog.show();
现在,这个例子是危险的,为什么?
// We are still inside an Activity _handlerToDelayDroidMove = new Handler(); _handlerToDelayDroidMove.postDelayed(_droidPlayRunnable, 10000); private Runnable _droidPlayRunnable = new Runnable() { public void run() { _someFieldOfTheActivity.performLongCalculation(); } };
我对这个问题的理解有一个疑问,那就是详细理解一个活动被破坏和重新创build时所保留的内容。
是吗?
比方说,我只是改变了设备的方向(这是最常见的泄漏原因)。 当super.onCreate(savedInstanceState)
将在我的onCreate()
被调用,这将恢复字段的值(如他们在方向改变之前)? 这是否也会恢复内部阶层的状态?
我意识到我的问题不是很确切,但是我真的很感谢任何可以让事情更清楚的解释。
塞巴斯蒂安
你所问的是一个相当棘手的问题。 虽然你可能会认为这只是一个问题,但你实际上是一次问几个问题。 我会尽我所能知道,我不得不掩盖它,并希望其他一些人会join来掩饰我可能错过的事情。
内部类:介绍
由于我不确定在Java中使用OOP是多么的舒服,所以这会带来一些基本的东西。 内部类是当一个类定义被包含在另一个类中时。 基本上有两种types:静态和非静态。 这些真正的区别是:
- 静态内部类:
- 被认为是“顶级”。
- 不要求构造包含类的实例。
- 没有明确的引用可能不会引用包含的类成员。
- 有自己的一生。
- 非静态内部类:
- 总是要求构造一个包含类的实例。
- 自动具有对包含实例的隐式引用。
- 可以在没有参考的情况下访问容器的类成员。
- 终身不应该比集装箱寿命长。
垃圾收集和非静态内部类
垃圾收集是自动的,但试图根据是否认为它们被使用来删除对象。 垃圾收集器非常聪明,但并不完美。 它只能确定是否正在使用的东西是否有一个积极的引用对象。
这里真正的问题是,当一个非静态的内部类比它的容器长时间存活。 这是因为隐式引用了包含类。 这种情况发生的唯一方法是,如果包含类之外的对象保持对内部对象的引用,而不考虑包含对象。
这可能导致内部对象处于活动状态(通过引用),但是包含对象的引用已经从所有其他对象中移除。 因此,内部对象保持包含对象的活性,因为它始终会引用它。 这个问题是,除非它被编程,否则没有办法返回到包含对象来检查它是否还活着。
这个实现的最重要的方面是,它是否在一个活动或是一个可绘制的没有区别。 当使用非静态的内部类时,你总是必须有条不紊,并且确保它们永远不会超出容器的对象。 幸运的是,如果它不是你的代码的核心对象,泄漏可能比较小。 不幸的是,这些是最难find的一些漏洞,因为它们很可能会被忽视,直到其中许多漏洞被泄露。
解决scheme:非静态内部类
- 从包含对象中获取临时引用。
- 允许包含对象成为唯一一个保持对内部对象的长效引用。
- 使用已build立的模式,如工厂。
- 如果内部类不需要访问包含的类成员,请考虑将其转换为静态类。
- 不pipe是否在活动中,请谨慎使用。
活动和观点:介绍
活动包含许多信息以便能够运行和显示。 活动是由特征定义的,他们必须有一个View。 他们也有一定的自动处理程序。 无论您是否指定,“活动”都会隐式引用它所包含的“视图”。
为了创build视图,它必须知道在哪里创build视图,以及是否有任何子视图,以便它可以显示。 这意味着每个View都有一个对Activity的引用(通过getContext()
)。 此外,每个视图保持对其子的引用(即getChildAt()
)。 最后,每个视图保持对呈现的代表其显示的呈现的位图的引用。
每当你有一个活动(或活动上下文)的引用,这意味着你可以沿着布局层次结构中的整个链。 这就是为什么关于活动或视图的内存泄漏是如此巨大的交易。 这可能是一次性泄漏的大量内存。
活动,视图和非静态内部类
鉴于以上有关内部类的信息,这些是最常见的内存泄漏,但也是最常见的避免。 虽然可以让内部类可以直接访问Activities类成员,但是很多人愿意将它们静态化以避免潜在的问题。 活动和观点的问题比这个更深刻。
泄漏的活动,视图和活动上下文
这一切都归结于语境和生命周期。 有某些事件(如方向)会杀死活动上下文。 由于如此多的类和方法需要一个Context,开发者有时候会试图通过获取对Context的引用来保存一些代码。 恰巧我们为了运行我们的Activity而必须创build的许多对象必须存在于Activity LifeCycle之外,以便Activity能够做它需要做的事情。 如果你的任何对象碰巧有一个活动,它的上下文或它的任何视图的引用,当它被销毁时,你刚刚泄露了该活动及其整个视图树。
解决scheme:活动和观点
- 避免不惜一切代价,对视图或活动进行静态引用。
- 对活动上下文的所有引用应该是短暂的(函数的持续时间)
- 如果您需要长寿命的上下文,请使用应用程序上下文(
getBaseContext()
或getApplicationContext()
)。 这些不隐含地保留引用。 - 或者,您可以通过覆盖configuration更改来限制活动的销毁。 但是,这并不能阻止其他潜在的事件破坏活动。 虽然你可以做到这一点,你仍然可以参考上述做法。
Runnables:简介
Runnables其实并没有那么糟糕。 我的意思是,他们可能是,但实际上我们已经击中了大部分危险区域。 Runnable是一个asynchronous操作,它执行一个独立于创build线程的任务。 大多数可运行的实例都是从UI线程实例化的。 从本质上讲,使用Runnable可以创build另一个线程,只需稍微更多的pipe理。 如果你像一个标准的类一样运行Runnable,并按照上面的指导,你应该遇到一些问题。 现实是许多开发人员不这样做。
为了方便阅读和逻辑程序stream程,许多开发人员使用匿名内部类来定义他们的Runnables,比如你在上面创build的例子。 这导致了一个像上面input的例子。 一个匿名内部类基本上是一个离散的非静态内部类。 您不必创build一个全新的定义,只需重写适当的方法即可。 在所有其他方面,它是一个非静态内部类,这意味着它保持对其容器的隐式引用。
可运行和活动/视图
好极了! 这部分可以简短! 由于Runnables在当前线程之外运行的事实,这些长期运行asynchronous操作的危险。 如果runnable在Activity或View中被定义为匿名内部类或非静态内部类,则存在一些非常严重的危险。 这是因为,如前所述,它必须知道它的容器是谁。 input方向更改(或系统kill)。 现在回头看看刚才发生的事情。 是的,你的例子是相当危险的。
解决scheme:可运行的
- 尝试并扩展Runnable,如果它不破坏你的代码的逻辑。
- 尽你所能将扩展的Runnables设为静态,如果它们必须是内部类。
- 如果您必须使用匿名运行列表,请避免在任何具有对正在使用的“活动”或“视图”进行长时间引用的对象中创build它们。
- 许多Runnables可以像AsyncTasks一样简单。 考虑使用AsyncTask,因为这些默认情况下是VMpipe理的。
回答最终的问题现在回答这个post其他部分没有直接提到的问题。 你问:“内部阶级的对象什么时候能比外部阶级生存得更久?” 在我们做这件事之前,让我再次强调一下:虽然你在活动中担心这一点是正确的,但它可能导致任何地方的泄漏。 我将提供一个简单的示例(不使用活动)来演示。
下面是一个基本工厂的常见例子(缺less代码)。
public class LeakFactory {//Just so that we have some data to leak int myID = 0; // Necessary because our Leak class is non-static public Leak createLeak() { return new Leak(); } // Mass Manufactured Leak class public class Leak {//Again for a little data. int size = 1; } }
这不是一个普遍的例子,但是很简单就可以certificate。 这里的关键是构造函数…
public class SwissCheese {//Can't have swiss cheese without some holes public Leak[] myHoles; public SwissCheese() {//Gotta have a Factory to make my holes LeakFactory _holeDriller = new LeakFactory() // Now, let's get the holes and store them. myHoles = new Leak[1000]; for (int i = 0; i++; i<1000) {//Store them in the class member myHoles[i] = _holeDriller.createLeak(); } // Yay! We're done! // Buh-bye LeakFactory. I don't need you anymore... } }
现在,我们有泄漏,但没有工厂。 即使我们释放了工厂,它仍会留在内存中,因为每一个泄漏都有一个引用。 外面的class级没有数据也没有关系。 这种情况比人们想象的要多得多。 我们不需要创造者,只需要它的创作。 所以我们暂时创build一个,但无限期地使用创作。
想象一下,当我们稍微改变构造函数时会发生什么。
public class SwissCheese {//Can't have swiss cheese without some holes public Leak[] myHoles; public SwissCheese() {//Now, let's get the holes and store them. myHoles = new Leak[1000]; for (int i = 0; i++; i<1000) {//WOW! I don't even have to create a Factory... // This is SOOOO much prettier.... myHoles[i] = new LeakFactory().createLeak(); } } }
现在,这些新泄漏工厂中的每一个都刚刚泄漏。 你对那个怎么想的? 这是两个很普遍的例子,说明一个内部阶级如何能超越任何types的外部阶级。 如果这个外部类是一个活动,想象会有多糟糕。
结论
这些列出了不适当使用这些对象的主要已知危险。 总的来说,这篇文章应该涵盖了你的大部分问题,但是我知道这是一个懒惰的post,所以如果你需要澄清,请让我知道。 只要你遵循上述的做法,你将很less担心泄漏。
希望这可以帮助,
FuzzicalLogic