ChatGPT解决这个技术问题 Extra ChatGPT

究竟什么时候使用(匿名)内部类是安全的?

我一直在阅读一些关于 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();
    }
};

我怀疑理解这个主题与详细理解活动被销毁和重新创建时保留的内容有关。

是吗?

假设我刚刚更改了设备的方向(这是最常见的泄漏原因)。在我的 onCreate() 中调用 super.onCreate(savedInstanceState) 时,这是否会恢复字段的值(就像它们在方向更改之前一样)?这也会恢复内部类的状态吗?

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

This blog postthis blog post 有一些关于内存泄漏和内部类的有用信息。 :)
完全推荐你的帖子@AlexLockwood :) thx!

6
6 revs, 3 users 93%

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

嵌套类:简介

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

静态嵌套类:被认为是“顶级”。不需要构造包含类的实例。在没有明确引用的情况下,不得引用包含的类成员。有自己的一生。

被认为是“顶级”。

不需要构造包含类的实例。

在没有明确引用的情况下,不得引用包含的类成员。

有自己的一生。

内部嵌套类:始终需要构造包含类的实例。自动隐式引用包含的实例。可以在没有引用的情况下访问容器的类成员。生命周期应该不超过容器的生命周期。

始终需要构造包含类的实例。

自动隐式引用包含的实例。

可以在没有引用的情况下访问容器的类成员。

生命周期应该不超过容器的生命周期。

垃圾收集和内部类

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

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

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

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

解决方案:内部类

从包含对象获取临时引用。

允许包含对象成为唯一保留对内部对象的长期引用的对象。

使用已建立的模式,例如工厂。

如果内部类不需要访问包含的类成员,请考虑将其转换为静态类。

谨慎使用,无论它是否在 Activity 中。

活动与观点:简介

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

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

每当您引用 Activity(或 Activity 上下文)时,这意味着您可以沿着布局层次结构的整个链向下移动。这就是为什么有关活动或视图的内存泄漏如此之大的原因。可能会同时泄漏大量内存。

活动、观点和内部课程

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

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

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

解决方案:活动和观点

不惜一切代价避免对视图或活动进行静态引用。

所有对活动上下文的引用都应该是短暂的(函数的持续时间)

如果您需要一个长期存在的上下文,请使用应用程序上下文(getBaseContext() 或 getApplicationContext())。这些不会隐式保留引用。

或者,您可以通过覆盖配置更改来限制 Activity 的销毁。但是,这并不能阻止其他潜在事件破坏 Activity。虽然您可以这样做,但您可能仍想参考上述做法。

Runnables:简介

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

出于易用性、可读性和逻辑程序流程的考虑,许多开发人员使用匿名内部类来定义他们的 Runnable,例如您在上面创建的示例。这会产生一个与您在上面键入的示例类似的示例。 Anonymous Inner Class 基本上是一个离散的 Inner Class。您不必创建一个全新的定义并简单地覆盖适当的方法。在所有其他方面,它是一个内部类,这意味着它保持对其容器的隐式引用。

可运行文件和活动/视图

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

解决方案:Runnables

如果它不会破坏代码的逻辑,请尝试扩展 Runnable。

如果扩展的 Runnables 必须是嵌套类,请尽量使它们成为静态的。

如果必须使用 Anonymous Runnables,请避免在对正在使用的 Activity 或 View 具有长期引用的任何对象中创建它们。

许多 Runnable 可以很容易地成为 AsyncTask。考虑使用 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 中的每一个都刚刚被泄露。你对那个怎么想的?这些是内部类如何比任何类型的外部类寿命更长的两个非常常见的示例。如果那个外部类是一个 Activity,想象一下它会有多糟糕。

结论

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


非常感谢这个清晰而详细的答案。我只是不明白“许多开发人员利用闭包来定义他们的 Runnables”的意思
Java中的闭包是匿名内部类,就像您描述的Runnable一样。它是一种利用类(几乎扩展它)而不编写扩展 Runnable 的已定义类的方法。它被称为闭包,因为它是“一个封闭的类定义”,因为它在实际包含的对象中有自己的封闭内存空间。
启蒙写法!关于术语的一点评论:Java 中没有 静态内部类 这样的东西。 (Docs)。嵌套类是 staticinner,但不能同时是两者。
虽然这在技术上是正确的,但 Java 允许您在静态类中定义静态类。该术语不是为了我的利益,而是为了其他不了解技术语义的人的利益。这就是为什么首先提到它们是“顶级”的原因。 Android 开发者文档也使用这个术语,这是为关注 Android 开发的人准备的,所以我认为最好保持一致性。
很棒的帖子,是 StackOverflow 上最好的帖子之一,尤其是 Android 版。
e
ericn

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

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

更详细的解释here

检查您是使用 static class InnerAdapter 还是仅使用 class InnerAdapter 的常见内部类示例有列表(ListViewRecyclerView、标签 + 页面布局 (ViewPager)、下拉列表和 AsyncTask 子类

无论您使用 Handler + Runnable、AsyncTask、RxJava 还是其他任何东西,如果在 Activity/Fragment/View 销毁后操作完成,您将创建 Activity/Fragment/View 对象的 rouge 引用(它们是巨大的)无法垃圾收集(无法释放的内存插槽)

所以请务必取消 onDestroy() 或更早的那些长时间运行的任务,不会有内存泄漏


W
Weidian Huang

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

例如,您将 setOnClickListener() 用于 Android 按钮,大多数时候您使用匿名类,因为没有其他对象持有对它的引用,并且您不会在侦听器内部执行一些长时间的过程。一旦外部类被销毁,内部类也可以被销毁。

https://i.stack.imgur.com/rorRG.png

另一个存在内存泄漏问题的示例是 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();
  }
}

https://i.stack.imgur.com/dmyNs.png

here解释了更多细节。