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

时间:2012-06-02 18:26:22

标签: java android memory-leaks inner-classes

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

尽管如此,我还是不完全理解这个概念,尤其是当在一个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)中调用onCreate()时,是否会恢复字段的值(因为它们在方向更改之前)?这还会恢复内部阶级的状态吗?

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

3 个答案:

答案 0 :(得分:613)

你问的是一个非常棘手的问题。虽然您可能认为这只是一个问题,但实际上您实际上是在问几个问题。我将尽我所能来完善我所需要的知识,并希望其他一些人能够加入我所能想到的内容。

嵌套课程:简介

由于我不确定你在Java中使用OOP有多舒服,这将有几个基础。嵌套类是在另一个类中包含类定义的时候。基本上有两种类型:静态嵌套类和内部类。这些之间的真正区别是:

  • 静态嵌套类:
    • 被视为"顶级"。
    • 不要求构造包含类的实例。
    • 如果没有明确的引用,则不能引用包含的类成员。
    • 有自己的一生。
  • 内部嵌套类:
    • 始终要求构造包含类的实例。
    • 自动拥有对包含实例的隐式引用。
    • 可以在没有参考的情况下访问容器的类成员。
    • 的生命周期不应超过容器的生命周期。

垃圾收集和内部类

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

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

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

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

解决方案:内部课程

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

活动和观点:简介

活动包含大量可以运行和显示的信息。活动由他们必须拥有View的特征定义。他们还有一些自动处理程序。无论您是否指定它,Activity都会隐式引用它包含的View。

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

每当您引用Activity(或Activity Context)时,这意味着您可以沿着布局层次结构的整个链。这就是为什么有关活动或视图的内存泄漏是如此巨大的原因。它可以是一次的内存泄露。

活动,观点和内部课程

鉴于上面有关内部类的信息,这些是最常见的内存泄漏,但也是最常见的内存泄漏。虽然希望内部类可以直接访问Activities类成员,但是许多人愿意将它们设置为静态以避免潜在的问题。活动和观点的问题比这更深刻。

泄露的活动,观点和活动背景

这一切都归结为Context和LifeCycle。某些事件(例如方向)会杀死活动上下文。由于许多类和方法需要Context,因此开发人员有时会尝试通过获取对Context的引用并保留它来保存一些代码。事实上,我们必须创建的许多用于运行Activity的对象必须存在于Activity LifeCycle之外,以便允许Activity执行它需要做的事情。如果你的任何对象恰好引用了Activity,它的Context或它被销毁的任何Views,你刚刚泄露了该Activity及其整个View树。

解决方案:活动和观点

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

Runnables:简介

Runnables实际上并没有那么糟糕。我的意思是,他们可以,但实际上我们已经击中了大部分危险区域。 Runnable是一个异步操作,它执行与创建它的线程无关的任务。大多数runnable都是从UI线程实例化的。从本质上讲,使用Runnable创建另一个线程,只是稍微管理一下。如果您将Runnable归类为标准类并遵循上面的指导原则,那么您应该遇到一些问题。现实情况是许多开发人员不这样做。

许多开发人员使用匿名内部类来定义他们的Runnables,例如您在上面创建的示例,这是一种简单,可读性和逻辑程序流程。这会产生一个类似上面输入的示例。匿名内部类基本上是一个离散的内部类。您不必创建一个全新的定义,只需覆盖适当的方法即可。在所有其他方面,它是一个内部类,这意味着它保持对其容器的隐式引用。

可运行和活动/视图

耶!这部分可以简短!由于Runnables在当前线程之外运行,因此这些问题的危险在于长时间运行的异步操作。如果runnable在Activity或View中定义为匿名内部类或嵌套内部类,则存在一些非常严重的危险。这是因为,如前所述, 来了解其容器是谁。输入方向更改(或系统终止)。现在回顾前面几节,了解刚刚发生的事情。是的,你的例子非常危险。

解决方案:Runnables

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

回答最终问题 现在回答本帖其他部分未直接解决的问题。你问过"内部阶级的对象什么时候能够存活的时间长于外部阶级?"在我们谈到这一点之前,让我再强调一下:尽管你在“活动”中担心这一点是正确的,但它可能导致任何地方的泄漏。我将提供一个简单的例子(不使用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中的每一个都刚被泄露。你对那个怎么想的?这是两个非常常见的例子,说明内部类如何能够比任何类型的外部类寿命更长。如果那个外类是一个活动,想象它会有多糟糕。

<强>结论

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

答案 1 :(得分:0)

您在1个帖子中有2个问题:

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

更详细的说明here

用于检查是使用static class InnerAdapter还是仅使用class InnerAdapter的常见内部类的示例是列表(ListViewRecyclerView,标签+页面布局({{1} }),下拉列表和 AsyncTask子类

  1. 无论使用Handler + Runnable,AsyncTask,RxJava还是其他任何东西,如果操作在Activity / Fragment / View被销毁后完成,您都将创建Activity / Fragment / View对象的红色引用(巨大)(无法清除)(无法释放的内存插槽)

因此,请确保取消ViewPager或更早版本中运行时间较长的任务,并且不会出现内存泄漏

答案 2 :(得分:0)

只要您知道内部(匿名)类的生命周期短或与外部类完全相同,就可以安全地使用它们。

例如,您通常将setOnClickListener()用于Android按钮,因为大多数时候您都使用匿名类,因为没有其他对象持有对该引用的引用,并且您不会在侦听器内进行长时间的处理。一旦外部类被破坏,内部类也可以被破坏。

enter image description here

另一个有内存泄漏问题的示例是Android LocationCallback作为打击示例。

public class MainActivity extends AppCompatActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    initLocationLibraries();
  }

  private void initLocationLibraries() {
    mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
    mSettingsClient = LocationServices.getSettingsClient(this);

    mLocationCallback = new LocationCallback() {
        @Override
        public void onLocationResult(LocationResult locationResult) {
            super.onLocationResult(locationResult);
            // location is received
            mCurrentLocation = locationResult.getLastLocation();
            updateLocationUI();
        }
    };

    mRequestingLocationUpdates = false;

    mLocationRequest = new LocationRequest();
    mLocationRequest.setInterval(UPDATE_INTERVAL_IN_MILLISECONDS);
    mLocationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS);
    mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);

    LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder();
    builder.addLocationRequest(mLocationRequest);
    mLocationSettingsRequest = builder.build();
  }
}

现在,不仅Activity拥有LocationCallback的引用,Android GMS服务也拥有它。 GMS服务的生命周期比活动更长。它将导致活动的内存泄漏。 enter image description here

here解释了更多详细信息。