使用(匿名)内部类究竟何时泄漏安全?

2022-08-31 04:52:14

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

不过,我并不完全理解这个概念,特别是当在活动内使用内部类是安全或危险的。

这是我所理解的:

如果内部类的实例存活时间长于其外部类(活动),则会发生内存泄漏。-> 在什么情况下会发生这种情况?

在这个例子中,我认为没有泄漏的风险,因为匿名类扩展不会比活动存活更长时间,对吧?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)onCreate()

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


答案 1

你问的是一个非常棘手的问题。虽然你可能认为这只是一个问题,但你实际上是同时问几个问题。我会尽我所能,我知道我必须覆盖它,并希望其他人会加入进来,覆盖我可能错过的东西。

嵌套类:简介

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

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

垃圾回收和内部类

垃圾回收是自动的,但会根据它是否认为对象正在被使用来尝试删除对象。垃圾收集器非常聪明,但并非完美无瑕。它只能确定是否正在使用某些内容,以确定是否存在对该对象的活动引用。

这里真正的问题是当一个内部类保持活动时间比它的容器更长时。这是因为对包含类的隐式引用。发生这种情况的唯一方法是,如果包含类外部的对象保留对内部对象的引用,而不考虑包含的对象。

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

这种认识最重要的方面是,无论是在活动中还是在可绘制中,它都没有区别。使用内部类时,您始终必须有条不紊,并确保它们永远不会超过容器的对象。幸运的是,如果它不是代码的核心对象,那么相比之下,泄漏可能很小。不幸的是,这些是一些最难找到的泄漏,因为它们很可能被忽视,直到其中许多泄漏为止。

解决方案:内部类

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

活动和观点: 简介

活动包含大量信息,以便能够运行和显示。活动由它们必须具有视图的特征定义。它们还具有某些自动处理程序。无论是否指定它,活动都有一个对它所包含的视图的隐式引用。

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

每当您具有对活动(或活动上下文)的引用时,这意味着您可以沿着布局层次结构向下跟踪整个链。这就是为什么有关活动或视图的内存泄漏是一个如此巨大的问题。它可能是一次泄漏的大量内存。

活动、视图和内部类

鉴于上面有关内部类的信息,这些是最常见的内存泄漏,但也是最常避免的。虽然希望内部类能够直接访问 Activities 类成员,但许多人愿意将它们设置为静态以避免潜在问题。活动和视图的问题远不止于此。

泄露的活动、视图和活动上下文

这一切都归结为背景和生命周期。某些事件(如方向)会终止活动上下文。由于如此多的类和方法需要 Context,开发人员有时会尝试通过获取对 Context 的引用并保留它来保存一些代码。碰巧的是,我们必须创建的许多对象来运行我们的活动必须存在于活动生命周期之外,以便允许活动执行它需要做的事情。如果任何对象在被销毁时碰巧引用了某个活动、其上下文或其任何视图,那么您刚刚泄露了该活动及其整个视图树。

解决方案:活动和视图

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

运行:简介

Runnables实际上并没有那么糟糕。我的意思是,他们可能会,但实际上我们已经击中了大部分危险区域。Runnable 是一种异步操作,它独立于创建任务的线程执行任务。大多数可运行对象都是从 UI 线程实例化的。从本质上讲,使用Runnable正在创建另一个线程,只是管理得稍微多一些。如果您像标准类一样对Runnable进行分类并遵循上述准则,那么您应该会遇到一些问题。现实情况是,许多开发人员不这样做。

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

运行和活动/视图

耶!本节可以很短!由于 Runnables 在当前线程之外运行,因此这些操作的危险来自长时间运行的异步操作。如果在活动或视图中将可运行对象定义为匿名内部类或嵌套内部类,则存在一些非常严重的危险。这是因为,如前所述,它必须知道它的容器是谁。输入方向更改(或系统终止)。现在只需参考前面的部分,了解刚刚发生的事情。是的,你的例子非常危险。

解决方案:运行

  • 尝试扩展 Runnable,如果它不破坏代码的逻辑。
  • 如果扩展的 Runnables 必须是嵌套类,请尽最大努力使它们保持静态。
  • 如果必须使用匿名 Runnables,请避免在对正在使用的活动或视图具有长期引用的任何对象中创建它们。
  • 许多 Runnables 可以很容易地成为 AsyncTasks。请考虑使用 AsyncTask,因为默认情况下这些任务是 VM 托管的。

回答最后一个问题现在回答本文其他部分未直接解决的问题。你问“一个内在类的对象什么时候能比它的外类存活更长时间?在我们开始之前,让我再次强调:虽然你在“活动”中担心这一点是正确的,但它可能会在任何地方导致泄漏。我将提供一个简单的示例(不使用活动)来演示。

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

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

现在,我们有泄漏,但没有工厂。即使我们发布了工厂,它也将保留在内存中,因为每个泄漏都有对它的引用。外部类没有数据甚至无关紧要。这种情况发生的频率远远超过人们想象的。我们不需要造物主,只需要它的创造。因此,我们暂时创建一个,但无限期地使用这些创建。

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

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中的每一个都刚刚被泄露。对此,你怎么看?这是两个非常常见的例子,说明内部类如何比任何类型的外部类更持久。如果那个外在的类是一个活动,想象一下它会有多糟糕。

结论

这些列出了不恰当地使用这些对象的主要已知危险。总的来说,这篇文章应该涵盖了你的大部分问题,但我理解这是一个loooong帖子,所以如果你需要澄清,请告诉我。只要你遵循上述做法,你就很少担心泄漏。


答案 2

您在1篇文章中有2个问题:

  1. 使用内部类而不将其声明为 是不安全的。它不仅限于Android,还适用于整个Java世界。static

更详细的解释在这里

用于检查是否正在使用或仅使用列表(或、选项卡 + 页面布局 ()、下拉列表和 AsyncTask 子类的常见内部类的示例static class InnerAdapterclass InnerAdapterListViewRecyclerViewViewPager

  1. 无论你是使用 Handler + Runnable、AsyncTask、RxJava 还是其他任何东西,如果操作在 Activity/Fragment/View 被销毁后完成,你就会创建一个 Activity/Fragment/View 对象(巨大的)的 rouge 引用,该对象无法垃圾回收(无法释放的内存插槽)

因此,请确保在或更早的时间内取消这些长时间运行的任务,并且不会有内存泄漏onDestroy()


推荐