什么时候使用(匿名)内部类泄漏安全?


324

我一直在阅读有关Android内存泄漏的文章,并观看了来自Google I / O的有关该主题的有趣视频。

不过,我还是不完全理解这个概念,尤其是当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();
    }
};

我对以下事实感到怀疑:了解此主题与详细了解破坏和重新创建活动时保留的内容有关。

是吗?

假设我只是改变了设备的方向(这是导致泄漏的最常见原因)。什么时候super.onCreate(savedInstanceState)将在my中被调用,这onCreate()将恢复字段的值(如方向更改之前的值)吗?这还会恢复内部类的状态吗?

我意识到我的问题不是很精确,但是我非常感谢任何可以使事情变得更清楚的解释。


14
此博客文章此博客文章都具有一些有关内存泄漏和内部类的良好信息。:)
Alex Lockwood 2014年

Answers:


651

您要问的是一个非常棘手的问题。尽管您可能认为这只是一个问题,但实际上您一次要问几个问题。我将尽我所能,使我做到最好,并且希望其他一些人会加入进来,以弥补我可能会错过的内容。

嵌套类:简介

由于我不确定您对Java中的OOP有多满意,因此这将涉及两个基础知识。嵌套类是指一个类定义包含在另一个类中。基本上有两种类型:静态嵌套类和内部类。这些之间的真正区别是:

  • 静态嵌套类:
    • 被视为“顶级”。
    • 不需要构造包含类的实例。
    • 如果没有显式引用,则不能引用包含的类成员。
    • 有自己的一生。
  • 内部嵌套类:
    • 始终要求构造包含类的实例。
    • 自动具有对包含实例的隐式引用。
    • 可能会在没有参考的情况下访问容器的类成员。
    • 寿命应该比该容器不再。

垃圾收集和内部舱位

垃圾收集是自动的,但是会根据是否认为正在使用来尝试删除对象。垃圾收集器非常聪明,但并非完美无缺。它只能通过对对象的有效引用来确定是否正在使用某些对象。

真正的问题是,内部类的生存期比其容器更长。这是因为对包含类的隐式引用。发生这种情况的唯一方法是,如果包含类之外的对象保留对内部对象的引用,而不考虑包含对象。

这可能导致内部对象处于活动状态(通过引用),但已从所有其他对象中删除了对包含对象的引用。因此,内部对象将使包含对象保持活动状态,因为它将始终对其进行引用。这样做的问题是,除非对其进行编程,否则无法返回到包含对象以检查它是否还处于活动状态。

实现这一点最重要的方面是,无论是处于活动状态还是可绘制状态都没有区别。使用内部类时,您始终必须有条理,并确保它们永远不会超出容器的对象。幸运的是,如果它不是代码的核心对象,则相比而言泄漏可能很小。不幸的是,这些是最难发现的一些泄漏,因为在其中许多泄漏之前它们可能不会被注意到。

解决方案:内部类

  • 从包含对象获得临时引用。
  • 允许包含对象成为保留对内部对象的长期引用的唯一对象。
  • 使用已建立的模式,例如工厂。
  • 如果内部类不需要访问包含的类成员,请考虑将其转换为静态类。
  • 请谨慎使用,无论它是否在活动中。

活动和观点:简介

活动包含许多可以运行和显示的信息。活动由必须具有视图的特征定义。他们还具有某些自动处理程序。不管您是否指定它,活动都会隐式引用它包含的视图。

为了创建视图,它必须知道在何处创建视图以及它是否具有任何子代以便可以显示。这意味着每个View都有对Activity的引用(通过getContext())。此外,每个视图都保留对其子级(即getChildAt())的引用。最后,每个视图都会保留对表示其显示的渲染位图的引用。

只要有对活动(或活动上下文)的引用,这意味着您可以沿布局层次结构遵循整个IREIRE链。这就是为什么有关“活动”或“视图”的内存泄漏如此之大的原因。一次可能会泄漏大量内存。

活动,观点和内部课程

鉴于以上有关内部类的信息,这些是最常见的内存泄漏,也是最通常避免的泄漏。虽然希望内部类可以直接访问Activity类的成员,但许多人还是愿意将它们设为静态以避免潜在的问题。活动和视图的问题要深得多。

活动,视图和活动上下文泄漏

一切都取决于上下文和生命周期。有某些事件(例如方向)将杀死活动上下文。由于有这么多的类和方法需要Context,因此开发人员有时会尝试通过获取对Context的引用并保留它来保存一些代码。碰巧的是,我们为了运行Activity而必须创建的许多对象必须存在于Activity LifeCycle之外,以允许Activity进行所需的操作。如果销毁某个对象时碰巧引用了一个Activity,其Context或任何View,则您刚刚泄漏了该Activity及其整个View树。

解决方案:活动和视图

  • 不惜一切代价避免静态引用视图或活动。
  • 所有对活动上下文的引用都应该是短暂的(函数的持续时间)
  • 如果需要长期使用的上下文,请使用应用程序上下文(getBaseContext()getApplicationContext())。这些不会隐式保留引用。
  • 或者,您可以通过覆盖配置更改来限制活动的销毁。但是,这并不能阻止其他潜在事件破坏活动。虽然可以执行此操作,但您可能仍要参考上述做法。

Runnables:简介

可运行对象实际上还不错。我的意思是,他们可以是,但实际上我们已经触及了大多数危险区域。Runnable是异步操作,它执行与创建线程无关的任务。大多数可运行对象都是从UI线程实例化的。从本质上讲,使用Runnable可以创建另一个线程,但该线程要稍微多一些管理。如果像标准类一样对Runnable进行分类并遵循上述准则,则应该遇到一些问题。现实是许多开发人员不这样做。

出于易用性,可读性和逻辑程序流,许多开发人员使用匿名内部类来定义其Runnable,例如您在上面创建的示例。这样就得到了与您在上面键入的示例类似的示例。匿名内部类基本上是离散的内部类。您只需要创建一个全新的定义,而不必重写适当的方法。在所有其他方面,它是一个内部类,这意味着它对容器保持隐式引用。

可运行项目和活动/视图

好极了!这部分可能很短!由于Runnable在当前线程之外运行,因此长期运行异步操作会带来危险。如果在“活动”或“视图”中将可运行对象定义为匿名内部类或嵌套内部类,则存在一些非常严重的危险。如前所述,这是因为必须知道其容器是谁。输入方向更改(或系统终止)。现在,请参考上一节以了解发生了什么。是的,您的例子非常危险。

解决方案:可运行

  • 如果不破坏代码逻辑,请尝试扩展Runnable。
  • 如果扩展的Runnable必须是嵌套类,请尽最大努力使它们成为静态的。
  • 如果必须使用Anonymous Runnable,请避免在任何位置创建它们具有长期使用中的Activity或View引用的对象中。
  • 许多Runnable就像AsyncTasks一样容易。考虑使用AsyncTask,因为默认情况下这些是VM管理的。

回答最终问题 现在回答本文中其他部分未直接解决的问题。您问“内部类的对象何时能比其外部类生存更长的时间?” 在开始讨论之前,让我再次强调:尽管您正确担心“活动”中的此问题,但它可能会在任何地方引起泄漏。我将提供一个简单的示例(不使用Activity)来进行演示。

下面是一个基本工厂的常见示例(缺少代码)。

public class LeakFactory
{//Just so that we have some data to leak
    int myID = 0;
// Necessary because our Leak class is an Inner class
    public Leak createLeak()
    {
        return new Leak();
    }

// Mass Manufactured Leak class
    public class Leak
    {//Again for a little data.
       int size = 1;
    }
}

这不是一个常见的示例,但足以演示。这里的关键是构造函数...

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...
    }
}

现在,我们有泄漏,但没有工厂。即使我们发布了Factory,它也会保留在内存中,因为每个Leak都有对其的引用。外部类没有数据甚至都没有关系。这种情况发生的频率比人们想象的要多得多。我们不需要创作者,只需它的创作。因此,我们暂时创建一个,但是无限期地使用这些创建。

想象一下,当我们稍微改变构造函数时会发生什么。

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();
        }
    }
}

现在,这些新的LeakFactories中的每一个都已经泄漏。你对那个怎么想的?这是两个非常常见的示例,说明内部类如何能胜过任何类型的外部类。如果那个外部类是一个活动,请想象它会变得更糟。

结论

这些列出了不适当使用这些对象的主要已知危险。总的来说,该帖子应该涵盖了您的大部分问题,但是我知道这是一个冗长的帖子,因此,如果您需要澄清,请告诉我。只要遵循上述做法,您就不会担心泄漏。


3
非常感谢您提供清晰,详细的答案。我只是不明白你所说的“许多开发商利用闭包来定义他们的Runnable”的意思
塞巴斯蒂安

1
Java中的闭包是匿名内部类,就像您描述的Runnable。它是一种利用类(几乎对其进行扩展)而无需编写扩展Runnable的已定义类的方法。之所以称为闭包,是因为它是“封闭的类定义”,因为它在实际的包含对象中具有自己的封闭内存空间。
模糊逻辑

26
启发性的写作!关于术语的一句话:Java中没有静态内部类。(文档)。嵌套类可以是staticinner,但不能同时使用。
jenzz 2013年

2
尽管从技术上讲这是正确的,但是Java允许您在静态类中定义静态类。该术语不是为了我的利益,而是为了其他不了解技术语义的人的利益。这就是为什么首先提到它们是“顶级”的原因。Android开发人员文档也使用此术语,这适用于正在研究Android开发的人们,因此我认为最好保持一致性。
模糊逻辑2013年

13
很棒的帖子,StackOverflow上最好的帖子之一,尤其是Android版。
StackOverflowed
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.