首页 文章

什么时候使用(匿名)内部类可以安全泄漏?

提问于
浏览
278

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

尽管如此,我还是不完全理解这个概念,尤其是当用户 inner classes inside an Activity 安全或危险时 .

这就是我的理解:

如果内部类的实例比其外部类(活动)存活的时间更长,则会发生内存泄漏 . - > In which situations can this happen?

在这个例子中,我认为没有泄漏的风险,因为延伸 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 回答

  • 583

    你问的是一个非常棘手的问题 . 虽然您可能认为这只是一个问题,但实际上您实际上是在问几个问题 . 我会尽我所能来覆盖它,并希望其他一些人能够加入我的想法 .

    Nested Classes: Introduction

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

    • 静态嵌套类:

    • 被视为"top-level" .

    • 不要求构造包含类的实例 .

    • 如果没有显式引用,则不能引用包含的类成员 .

    • 有自己的一生 .

    • 内嵌套类:

    • 始终要求构造包含类的实例 .

    • 自动具有对包含实例的隐式引用 .

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

    • Lifetime应该不会超过容器的寿命 .

    Garbage Collection and Inner Classes

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

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

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

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

    Solutions: Inner Classes

    • 从包含对象获取临时引用 .

    • 允许包含对象是唯一一个保持对内部对象的长期引用的对象 .

    • 使用已 Build 的模式,例如Factory .

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

    • 谨慎使用,无论其是否在活动中 .

    Activities and Views: Introduction

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

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

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

    Activities, Views and Inner Classes

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

    Leaked Activities, Views and Activity Contexts

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

    Solutions: Activities and Views

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

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

    • 如果需要长期使用的Context,请使用应用程序上下文( getBaseContext()getApplicationContext() ) . 这些不会隐含地保留引用 .

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

    Runnables: Introduction

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

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

    Runnables and Activities/Views

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

    Solutions: Runnables

    • 尝试并扩展Runnable,如果它不破坏代码的逻辑 .

    • 如果必须是嵌套类,请尽量使扩展的Runnables为静态 .

    • 如果必须使用Anonymous Runnables,请避免在 any 对象中创建它们,该对象具有对正在使用的活动或视图的长期引用 .

    • 许多Runnables可以很容易地成为AsyncTasks . 考虑使用AsyncTask,因为默认情况下这些是VM Managed .

    Answering the Final Question 现在回答本文其他部分未直接提到的问题 . 你问过"When can an object of an inner class survive longer than its outer class?"在我们谈到这个之前,让我再强调一下:虽然你在活动中担心这个问题是正确的,但它可能导致任何地方的泄漏 . 我将提供一个简单的例子(不使用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刚刚被泄露 . 你对那个怎么想的?这是两个非常常见的例子,说明内部类如何能够比任何类型的外部类寿命更长 . 如果那个外部类是一个活动,想象它会有多糟糕 .

    Conclusion

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

相关问题