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

尽管如此,我仍然不完全理解这个概念,特别是当它对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();
    }
};

我对这样一个事实表示怀疑,即理解这个主题必须详细理解当一个活动被破坏和重新创建时保留了什么。

是吗?

假设我刚刚改变了设备的方向(这是泄漏的最常见原因)。当super.onCreate(savedInstanceState)将在我的onCreate()中被调用,这将恢复字段的值(因为它们在方向变化之前)?这也会恢复内部类的状态吗?

我知道我的问题不是很精确,但我真的很感激任何能让事情更清楚的解释。


当前回答

你在一个帖子里有两个问题:

使用内部类而不将其声明为静态是不安全的。它不仅局限于Android,而且适用于整个Java世界。

这里有更详细的解释

检查你是否使用静态类InnerAdapter或仅仅使用类InnerAdapter的常见内部类的例子是列表(ListView或RecyclerView,选项卡+页面布局(ViewPager),下拉和AsyncTask子类

不管你是使用Handler + Runnable, AsyncTask, RxJava还是其他什么,如果操作在Activity/ Fragment/ View被销毁后完成,你会创建一个不能垃圾收集的Activity/ Fragment/ View对象的rouge引用(这是巨大的)(内存插槽不能被释放)

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

其他回答

你问的是一个相当棘手的问题。虽然你可能认为这只是一个问题,但实际上你同时问了几个问题。我会尽我最大的努力去覆盖它,希望其他一些人能加入进来,覆盖我可能错过的东西。

嵌套类:简介

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

Static Nested Classes: Are considered "top-level". Do not require an instance of the containing class to be constructed. May not reference the containing class members without an explicit reference. Have their own lifetime. Inner Nested Classes: Always require an instance of the containing class to be constructed. Automatically have an implicit reference to the containing instance. May access the container's class members without the reference. Lifetime is supposed to be no longer than that of the container.

垃圾收集和内部类

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

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

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

这种实现最重要的方面是,它是在活动中还是在可绘制对象中都没有区别。在使用内部类时,您必须始终保持有条不紊,并确保它们永远不会比容器的对象更长寿。幸运的是,如果它不是代码的核心对象,泄漏可能相对较小。不幸的是,这些都是最难发现的漏洞,因为它们很可能会被忽视,直到其中许多漏洞泄露。

解决方案:内部类

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

活动和观点:介绍

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

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

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

活动,视图和内部类

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

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

It all comes down to the Context and the LifeCycle. There are certain events (such as orientation) that will kill an Activity Context. Since so many classes and methods require a Context, developers will sometimes try to save some code by grabbing a reference to a Context and holding onto it. It just so happens that many of the objects we have to create to run our Activity have to exist outside of the Activity LifeCycle in order to allow the Activity to do what it needs to do. If any of your objects happen to have a reference to an Activity, its Context, or any of its Views when it is destroyed, you have just leaked that Activity and its entire View tree.

解决方案:活动和视图

Avoid, at all costs, making a Static reference to a View or Activity. All references to Activity Contexts should be short lived (the duration of the function) If you need a long-lived Context, use the Application Context (getBaseContext() or getApplicationContext()). These do not keep references implicitly. Alternatively, you may limit the destruction of an Activity by overriding Configuration Changes. However, this does not stop other potential events from destroying the Activity. While you can do this, you may still want to refer to the above practices.

可运行:介绍

Runnables are actually not that bad. I mean, they could be, but really we've already hit most of the danger zones. A Runnable is an asynchronous operation that performs a task independant from the thread it was created on. Most runnables are instantiated from the UI thread. In essence, using a Runnable is creating another thread, just slightly more managed. If you class a Runnable like a standard class and follow the guidelines above, you should run into few problem. The reality is that many developers do not do this.

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

可运行对象和活动/视图

Yay! This section can be short! Due to the fact that Runnables run outside of the current thread, the danger with these comes to long running asynchronous operations. If the runnable is defined in an Activity or View as an Anonymous Inner Class OR nested Inner Class, there are some very serious dangers. This is because, as previously stated, it has to know who its container is. Enter the orientation change (or system kill). Now just refer back to the previous sections to understand what just happened. Yes, your example is quite dangerous.

解决方案:Runnables

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

回答最后一个问题 现在来回答这篇文章其他部分没有直接提到的问题。你问“内部类的对象什么时候能比外部类存活得更久?”在我们讨论这个问题之前,让我再次强调:尽管您在Activities中对此感到担忧是正确的,但它可能在任何地方导致泄漏。我将提供一个简单的示例(不使用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...
    }
}

现在我们有泄密,但没有工厂。即使我们发布了工厂,它仍将保留在内存中,因为每个泄漏都有对它的引用。外层类没有数据也没关系。这种情况发生得比人们想象的要频繁得多。我们不需要创造者,只需要它的创造物。因此,我们临时创建一个,但无限期地使用创建的对象。

想象一下稍微改变构造函数会发生什么。

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都被泄露了。你觉得怎么样?这是内部类如何比任何类型的外部类更长寿的两个非常常见的例子。如果这个外部类是一个Activity,想象一下会有多糟糕。

结论

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

你在一个帖子里有两个问题:

使用内部类而不将其声明为静态是不安全的。它不仅局限于Android,而且适用于整个Java世界。

这里有更详细的解释

检查你是否使用静态类InnerAdapter或仅仅使用类InnerAdapter的常见内部类的例子是列表(ListView或RecyclerView,选项卡+页面布局(ViewPager),下拉和AsyncTask子类

不管你是使用Handler + Runnable, AsyncTask, RxJava还是其他什么,如果操作在Activity/ Fragment/ View被销毁后完成,你会创建一个不能垃圾收集的Activity/ Fragment/ View对象的rouge引用(这是巨大的)(内存插槽不能被释放)

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

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

例如,你为Android按钮使用setOnClickListener(),大多数时候你使用匿名类,因为没有其他对象持有对它的引用,你不会在监听器内部做一些很长的过程。一旦外部类被破坏,内部类也可以被破坏。

另一个有内存泄漏问题的例子是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长得多。这将导致活动的内存泄漏。

更多细节在这里解释。