Uso compartido de tecnología Android | Algunos ejemplos de fugas de memoria y soluciones en Android

Una breve introducción a las fugas de memoria y la paliza de memoria

Fuga de memoria :

La fuga de memoria, es un tipo de fuga de recursos, la razón principal es que el programa de computadora maneja mal la configuración de la memoria y pierde el control de una sección del espacio de memoria asignado, lo que hace que el programa continúe ocupando el espacio de memoria que ya no se usa, o los objetos almacenados en la memoria no pueden ser penetrados, se accede a ella ejecutando código, lo que consume recursos de memoria.

En pocas palabras, una fuga de memoria es la incapacidad de recuperar correctamente la memoria que ya no está en uso

Ejemplo:

Tenga en cuenta que los siguientes ejemplos son ficticios

La aplicación de este ejemplo es una pequeña pieza de software simple que controla el funcionamiento del ascensor.
Esta parte del software se ejecuta cuando un pasajero presiona un botón para un piso en el ascensor.

Cuando se presiona el botón:

La memoria es necesaria para recordar
el piso de destino Guarde el número del piso de destino en la memoria
¿Ha llegado el ascensor al piso de destino?
En caso afirmativo, no hay nada que hacer: el programa se completa ; de
lo contrario:
espere hasta que el ascensor se detenga
y llegue al piso especificado .
Libere la memoria que acaba de usar para recordar el piso de destino.

Hay una pérdida de memoria en este programa: si se presiona el botón de este piso en el piso donde se encuentra el ascensor (es decir, el paso 4 del programa anterior), el programa activará la condición de juicio y finalizará la operación, pero la memoria sigue ocupada y no liberada. Cuanto más sucede esto, más memoria se pierde.

Este pequeño error no tiene un impacto inmediato. Porque las personas no suelen presionar el mismo botón de piso en el piso donde se encuentra el ascensor. Y en circunstancias normales, el ascensor debería tener suficiente memoria para hacer frente a cientos o miles de situaciones similares. Sin embargo, todavía es posible que el ascensor acabe con toda la memoria. Esto puede llevar meses o años, por lo que el problema pasa desapercibido con pruebas simples.

而这个例子导致的后果会是不那么令人愉快。至少,电梯不会再理会前往其他楼层的要求。更严重的是,如果程序需要存储器去开启电梯门,那可能有人被困电梯内,因为电梯没有足够的存储器去开启电梯门。

存储器泄漏只会在程序运行的时间内持续。例如:关闭电梯的电源时,程序终止运行。当电源再度开启,程序会再次运行而存储器会重置,而这种缓慢的泄漏则会从头开始再次发生。

内存抖动

源自Android文档中的Memory churn一词,中文翻译为内存抖动。 指快速频繁的创建对象从而产生的性能问题。

引用Android文档原文:

垃圾回收事件通常不会影响应用的性能。不过,如果在短时间内发生许多垃圾回收事件,就可能会快速耗尽帧时间。系统花在垃圾回收上的时间越多,能够花在呈现或流式传输音频等其他任务上的时间就越少。

通常,“内存抖动”可能会导致出现大量的垃圾回收事件。实际上,内存抖动可以说明在给定时间内出现的已分配临时对象的数量。

例如,您可以在 for 循环中分配多个临时对象。或者,您也可以在视图的 onDraw() 函数中创建新的 PaintBitmap 对象。在这两种情况下,应用都会快速创建大量对象。这些操作可以快速消耗新生代 (young generation) 区域中的所有可用内存,从而迫使垃圾回收事件发生。

内存泄漏(Memory leak)的产生和避免方式

Java内存泄漏的根本原因是长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏。

尽管短生命周期对象已经不再需要,但因为长生命周期依旧持有它的引用,故不能被回收而导致内存泄漏。

几种引起内存泄漏的问题:

静态集合类引起的内存泄漏

HashMapArrayList等集合以静态形式声明时,这些静态对象的生命周期与应用程序一致。他们所引用的对象也无法被释放,因为它们也被集合引用着。

private static HashMap<String, Object> a = new HashMap();
public static void main(String args[]) {
  for (int i = 0; i < 1000; i++) {
    Object tO = new Object();
    a.put("0", tO);
    tO = null;  
  }
}
复制代码

如果仅仅释放引用本身(tO = null),ArrayList依然在引用该对象,GC无法回收

监听器

在Java应用中,通常会用到很多监听器,一般通过addXXXXListener()实现。但释放对象时通常会忘记删除监听器,从而增加内存泄漏的风险。

各种连接

如数据库连接、网络连接(Socket)和I/O连接。忘记显式调用close()方法引起的内存泄漏

内部类和外部模块的引用

内部类的引用是很容易被遗忘的一种,一旦没有释放可能会导致一系列后续对象无法释放。此外还要小心外部模块不经意的引用,内部类是否提供相应的操作去除外部引用。

单例模式

由于单例的静态特性,使其生命周期与应用的生命周期一样长,一旦使用不恰当极易造成内存泄漏。如果单利持有外部引用,需要注意提供释放方式,否则当外部对象无法被正常回收时,会进而导致内存泄漏。

常见的内存泄漏处理方式:

集合类泄漏

如集合的使用范围超过逻辑代码的范围,需要格外注意删除机制是否完善可靠。比如由静态属性static指向的集合。

单利泄漏

以下为简单逻辑代码,只为举例说明内存泄漏问题,不保证单利模式的可靠性

public class AppManager {
  private static AppManager instance;
  private Context context;

  private AppManager(Context context) {
    this.context = context;
  }

  public static AppManager getInstance(Context context) {
    if (instance == null) {
      instance = new AppManager(context);
    }
    return instance;
  }
}
复制代码

AppManager创建时需要传入一个Context,这个Context的生命周期长短至关重要。

  1. 如果传入的是ApplicationContext,因为Application的生命周期等同于应用的生命周期,所以没有任何问题
  2. 如果传入的是ActivityContext,则需要考虑这个Activity是否在整个生命周期都不会被回收了,如果不是,则会造成内存泄漏

非静态内部类创建静态实例造成的内存泄漏

public class MyActivity extends AppCompatActivity {
  private static MyInnerClass mInnerClass = null;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...

    if (mInnerClass == null) {
      mInnerClass = new MyInnerClass();
    }
  }

  class MyInnerClass {
    ...
  }
}
复制代码

内部类持有外部类引用,而static声明的对象声明周期通常会比Activity长。即使关闭这个页面,由于mInnerClass为静态的,并且持有MyActivity的引用,导致无法回收此页面从而引起内存泄漏

应该将该内部类单独封装为一个单例来使用。

匿名内部类/异步线程

public class MyActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...

    new Thread(new Runnable() {
      @Override
      public void run() {
        ...
      }
    }).start();
  }
}
复制代码

Runnable都使用了匿名内部类,将持有MyActivity的引用。如果任务在Activity销毁前未完成,将导致Activity的内存无法被回收,从而造成内存泄漏

解决方法:将Runnable独立出来或使用静态内部类,可以避免因持有外部对象导致的内存泄漏

Handler造成的内存泄漏

public class SampleActivity extends AppCompatActivity {
  private final Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      ...
    }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    ...

    mHandler.postDelayed(new Runnable() {
      @Override
      public void run() {
        ...
      }
    }, 300000);

    finish();
  }
}
复制代码

Handler属于TLS(Thread Local Storage)变量,生命周期与Activity是不一致的,容易导致持有的对象无法正确被释放

当Android应用程序启动时,该应用程序的主线程会自动创建一个Looper对象和与之关联的MessageQueue。

当主线程中实例化一个Handler对象后,它就会自动与主线程Looper的MessageQueue关联起来。所有发送到MessageQueue的Messag都会持有Handler的引用,所以Looper会据此回调Handle的handleMessage()方法来处理消息。只要MessageQueue中有未处理的Message,Looper就会不断的从中取出并交给Handler处理。

另外,主线程的Looper对象会伴随该应用程序的整个生命周期。

在Java中,非静态内部类和匿名类内部类都会潜在持有它们所属的外部类的引用,但是静态内部类却不会。

当该 Activity 被 finish() 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,所以此时 finish() 掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指 SampleActivity)。

解决方法:在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,见如下代码:

public class SampleActivity extends AppCompatActivity {

  private static class MyHandler extends Handler {
    private final WeakReference<SampleActivity> mActivity;

    public MyHandler(SampleActivity activity) {
      mActivity = new WeakReference<SampleActivity>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
      SampleActivity activity = mActivity.get();
      if (activity != null) {
        ...
      }
    }
  }
  private final MyHandler mHandler = new MyHandler(this);

  private static final Runnable mRunnable = new Runnable() {
    @Override
    public void run() { ... }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    ...

    mHandler.postDelayed(mRunnable, 300000);
    finish();
  }
}
复制代码
避免不必要的静态成员变量

对于BroadcastReceiver、ContentObserver、File、Cursor、Stream、Bitmap等资源的使用,应在Activity销毁前及时关闭或注销

不使用WebView对象时,应调用destroy()方法销毁

在这里插入图片描述

Supongo que te gusta

Origin juejin.im/post/7085540378916421669
Recomendado
Clasificación