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

我一直在阅读一些关于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()中被调用时,这将恢复字段的值(因为它们在方向改变之前)吗?这也会恢复内部类的状态吗?

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

61683 次浏览

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

嵌套类:简介

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

  • 静态嵌套类:
    • 被认为是“顶级的”。
    • 不需要构造包含类的实例。
    • 不能在没有显式引用的情况下引用包含的类成员。
    • 有自己的一生。
    • 李< / ul > < / >
    • 内部嵌套类:
      • 总是要求构造包含类的实例。
      • 自动拥有包含实例的隐式引用。
      • 可以在没有引用的情况下访问容器的类成员。
      • 生命周期应该不会比容器的生命周期长。
      • 李< / ul > < / >

      垃圾收集和内部类

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

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

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

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

      解决方案:内部类

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

      活动和观点:介绍

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

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

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

      活动,视图和内部类

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

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

      这一切都归结于上下文和生命周期。有一些特定的事件(比如定位)会杀死一个活动上下文。由于如此多的类和方法需要Context,开发人员有时会试图通过获取Context引用并保留它来保存一些代码。恰好我们为了运行Activity而创建的许多对象都必须存在于Activity生命周期之外,以允许Activity做它需要做的事情。如果你的任何对象恰好有一个对活动、它的上下文或它的任何视图的引用,当它被销毁时,你就泄露了那个活动和它的整个视图树。

      解决方案:活动和视图

      • 无论如何都要避免对视图或活动进行静态引用。
      • 所有对活动上下文的引用都应该是短期的(函数的持续时间)
      • 如果你需要一个长期存在的上下文,请使用应用程序上下文(getBaseContext()getApplicationContext())。它们不隐式地保留引用。
      • 或者,您可以通过覆盖配置更改来限制活动的销毁。然而,这并不能阻止其他潜在事件破坏活动。当你可以这样做时,你可能仍然想参考上面的实践。

      可运行:介绍

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

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

      可运行对象和活动/视图

      耶!这个部分可以很短!由于Runnables在当前线程之外运行,因此它们的危险来自于长时间运行的异步操作。如果在活动或视图中将可运行对象定义为匿名内部类或嵌套内部类,则存在一些非常严重的危险。这是因为,如前所述,它< em > < / em >知道它的容器是谁。输入方向改变(或系统终止)。现在回到前面的部分来理解刚刚发生了什么。是的,你的例子很危险。

      解决方案:可运行

      • 尝试扩展Runnable,如果它不会破坏代码的逻辑的话。
      • 如果扩展的Runnables必须是嵌套类,请尽量使它们成为静态的。
      • 如果必须使用匿名可运行对象,避免在< em > < / em >对象中创建它们,该对象具有对正在使用的活动或视图的长期引用。
      • 许多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,想象一下会有多糟糕。

      结论

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

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

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

更详细的解释在这里

检查你使用的是static class InnerAdapter还是只是class InnerAdapter的常见内部类的例子有列表(ListViewRecyclerView,选项卡+页面布局(ViewPager),下拉列表和AsyncTask子类

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

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

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

例如,你为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长得多。这将导致活动的内存泄漏。 enter image description here < / p >

更详细的解释在这里