何时使用(匿名)内部类是安全的?

348

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

然而,我仍然不完全理解这个概念,特别是在使用Activity内部类时安全或危险的情况。

这是我理解的:

如果一个内部类的实例生存时间比其外部类(即Activity)更长,就会发生内存泄漏。 -> 在哪些情况下会出现这种情况?

在这个例子中,我认为没有泄漏的风险,因为匿名类扩展OnClickListener没有比Activity存在更长的寿命,对吗?

    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()中被调用时,它会恢复字段的值(就像在方向改变之前一样)吗?它还会恢复内部类的状态吗?

我意识到我的问题不是很精确,但我真的很感激任何能让事情更清晰的解释。


15
这篇博客文章和这篇博客文章讨论了内部类和线程对Android应用程序中内存泄漏的影响,对此有很好的阐述。 :) - Alex Lockwood
非常推荐你的帖子 @AlexLockwood :) 谢谢! - Andy
3个回答

696
你所问的是一个相当棘手的问题。虽然你可能认为这只是一个问题,但实际上你同时在提出几个问题。我会尽我所能用我所知道的知识来覆盖它,并希望其他人加入其中来补充我可能会遗漏的内容。
嵌套类:介绍
由于我不确定你对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
{//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...
    }
}

现在,我们有泄漏(Leaks),但没有工厂。即使我们发布了工厂,它也会保留在内存中,因为每个泄漏都有一个对它的引用。即使外部类没有数据也无所谓。这种情况比人们想象的要频繁得多。我们不需要创建者,只需要它的创造物。因此,我们暂时创建一个,但无限期地使用创造物。
想象一下当我们稍微改变构造函数时会发生什么。
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();
        }
    }
}

现在,所有这些新的LeakFactory都已经泄漏了。你对此有何看法?这些都是内部类如何比任何类型的外部类更长寿的两个非常普遍的例子。如果该外部类是一个Activity,想象一下会更糟糕。
结论:
这些列出了不适当使用这些对象的主要危险。总的来说,本文应该已经回答了大部分您的问题,但我知道这篇文章很长,如果您需要澄清,请告诉我。只要您遵循上述做法,就不必担心内存泄漏的问题。

4
非常感谢您提供清晰详细的答案。我只是不理解您所说的“许多开发人员利用闭包来定义他们的Runnables”的意思。 许多开发人员使用闭包来定义他们的可运行代码块。 - Sébastien
1
为了更清晰,编辑了答案以删除对闭包的引用。 - Fuzzical Logic
26
启迪性的文章!关于术语的一点说明:在Java中不存在“static inner class”。(文档)。嵌套类要么是“static”,要么是“inner”,但不能同时是两者。 - jenzz
2
尽管从技术角度来看这是正确的,但Java允许您在静态类中定义静态类。这种术语不是为了我的利益,而是为了那些不理解技术语义的人们的利益。这就是为什么首先提到它们是“顶级”的原因。Android开发者文档也使用这个术语,这是为了那些正在研究Android开发的人们,因此我认为保持一致性更好。 - Fuzzical Logic
13
很棒的帖子,在StackOverflow中算是最好的之一,尤其适用于Android。 - StackOverflowed
显示剩余24条评论

2
您在一篇文章中提出了两个问题:
1. 使用内部类时,如果不将其声明为static,则永远不安全。这种情况不仅限于Android,而是适用于整个Java世界。
详细的解释可以在这里找到。
常见的内部类包括列表(ListViewRecyclerView)、选项卡+页面布局(ViewPager)、下拉菜单和AsyncTask子类,请检查您是否使用了static class InnerAdapter 或者只有 class InnerAdapter
2. 无论您使用Handler + Runnable、AsyncTask、RxJava还是其他任何东西,如果操作在Activity / Fragment / View销毁之后完成,则会创建一个巨大的Activity / Fragment / View对象的引用(它们本身就很大),这些引用不能被垃圾回收(无法释放的内存槽)。
因此,请确保在onDestroy()或更早的阶段取消那些长时间运行的任务,这样就不会出现内存泄漏。

2
只要你知道内部(匿名)类的生命周期与外部类相同或更短,就可以安全地使用它们。例如,在 Android 按钮中使用 setOnClickListener() 时,大多数情况下会使用匿名类,因为没有其他对象持有对它的引用,并且你不会在监听器中执行一些长时间的过程。一旦外部类被销毁,内部类也会被销毁。

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服务的生命周期比Activity长得多。这将导致Activity的内存泄漏。 enter image description here 更多细节在此处解释

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接