Android性能优化(1)—内存泄漏


注:本文将会使用 Memory Profiler 进行内存检测

接下来将按以下四个方面来记录和总结一下内存泄漏:

  • 什么是内存泄漏
  • 内存泄漏会导致什么后果
  • 三个栗子
  • 总结

什么是内存泄漏

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费。


内存泄漏会导致什么后果

1.频繁 GC

  • Android 系统分配给单个应用的内存资源是有限的,内存泄露将会导致其他组件可用的内存变少。当堆内存增长到一定程度时将会触发 GC ,GC 触发时所有线程都会暂停,因此 GC 的频率越高,用户将会越容易感到应用卡顿。

2. OOM

  • Android 系统为每个应用程序分配的内存是有限的,而当一个应用中产生的内存泄漏比较多时,这就难免会导致应用所需要的内存超过系统分配的内存限额,这样就会导致应用直接崩溃。

三个栗子

1. Handler 内存泄漏

  • 示例代码如下:
public class MemoryLeakageOneActivity extends AppCompatActivity {
    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //模拟界面销毁还存在未处理的消息
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {

            }
        }, 1000000000);
    }
}
  • App 操作流程如下:

      1. 从『MemoryLeakageMainActivity』跳转至 『MemoryLeakageOneActivity』
      1. 退出『MemoryLeakageOneActivity』
  • Memory Profiler 操作流程如下:

      1. 打开『Android Profiler』点击『MEMORY』
      1. 在『Memory Profiler』工具栏中点击『垃圾回收』按钮(注:建议多点两次)
      1. 在『Memory Profiler』工具栏中点击『堆转储』按钮
      1. 在『堆转储』工具栏中选择『按包名排列』
      1. 在『Package Name』列表中找到对应的包名,可进行查看哪些类还存在于堆内存中
  • Memory Profiler 界面如下:
Handler内存泄漏.png
  • 1.『垃圾回收』按钮,用于强制执行垃圾回收。

  • 2.『堆转储』按钮,用于捕获堆转储。

  • 3.『排列方式』下拉列表,根据用户需要可进行切换排列方式。

    • Arrange by class:基于类名称对所有分配进行分组。
    • Arrange by package:基于软件包名称对所有分配进行分组。
    • Arrange by callstack:将所有分配分组到其对应的调用堆栈。
    1. 定位到需要检查的包命。

根据上述『App操作流程』来看『MemoryLeakageOneActivity』 已经销毁了本应该不出现在堆内存里面。但是根据上图『Handler内存泄漏.png』来看『MemoryLeakageOneActivity』依然存在于堆内存中,因此对于本应用来说『MemoryLeakageOneActivity』 就已经产生了 内存泄漏

产生的原因:由于非静态内部类和匿名类内部类都会潜在持有它们所属的外部类的引用,但是静态内部类却不会。所以当『MemoryLeakageOneActivity』销毁时,未处理的消息持有 Handler 的引用,而 Handler 又持有它所属的外部类也就是『MemoryLeakageOneActivity』的引用。引用关系会一直保持直到消息得到处理,这样阻止了『MemoryLeakageOneActivity』被垃圾回收器回收,从而造成了内存泄漏。

如何解决 Handler 内存泄漏

  • 示例代码如下
public class MemoryLeakageOneActivity extends AppCompatActivity {
     @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //模拟界面销毁还存在未处理的消息
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {

            }
        }, 1000000000);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //移除Handler中所有消息
        mHandler.removeCallbacksAndMessages(null);
        mHandler = null;
    }
}

在『onDestroy』方法中移除 Handler 所有未处理的消息并将 Handler 置 null 。

  • mHandler.removeCallbacksAndMessages(null);
  • mHandler = null;

接下来我们再次按照上述『App操作流程』『Memory Profiler操作流程』操作一遍后会发现堆内存中已经看不到『MemoryLeakageOneActivity』了。

  • Memory Profiler 界面如下:
解决Handler内存泄漏.png

根据上图『解决Handler内存泄漏.png』来看『MemoryLeakageOneActivity』已经从堆内存中释放掉了,由此也可以看出已经解决了之前『MemoryLeakageOneActivity』所产生的内存泄漏问题。

小结:

    1. 在界面销毁时应该将 Handler 未处理的消息进行移除。
    1. 在界面销毁时若 Handler 为匿名内部类,将 Handler 置 null ,因为匿名内部类会潜在持有它所属的外部类的引用。

2. 本类实例被其他类持有所导致内存泄漏

  • 示例代码如下:
public class MemoryLeakageTwoActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //将本类的实例传给 MemoryLeakageMainActivity 类
        MemoryLeakageMainActivity.setActivity(this);
    }
}
public class MemoryLeakageMainActivity extends AppCompatActivity {
    private static Activity mActivity;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.memoryleakage_activity_main);
        startActivity(new Intent(this, MemoryLeakageTwoActivity.class));
    }

    public static void setActivity(Activity activity) {
        mActivity = activity;
    }
}

按照 『Handler内存泄漏中的「App操作流程」「Memory Profiler操作流程」』操作一遍后。

  • Memory Profiler 界面如下:
本类实例被其他类持有所导致内存泄漏.png

根据上图『本类实例被其他类持有所导致内存泄漏.png』来看『MemoryLeakageTwoActivity 』依然存在于堆内存中,因此对于本应用来说『MemoryLeakageTwoActivity 』 就已经产生了 内存泄漏

产生的原因:当『MemoryLeakageTwoActivity 』销毁时,由于『MemoryLeakageTwoActivity 』对象实例被『MemoryLeakageMainActivity』对象所引用,所以阻止了『MemoryLeakageTwoActivity 』被垃圾回收器回收,从而导致内存泄漏。

注:此处还有一个的地方需要注意,那就是 private static Activity mActivity; 由于被 static 修饰了所以导致 mActivity 所引用的实例生命周期和应用的生命周期一样长。

如何解决本类实例被其他类持有所导致内存泄漏

  • 示例代码如下:
public class MemoryLeakageMainActivity extends AppCompatActivity {
    private static SoftReference mActivity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.memoryleakage_activity_main);
        startActivity(new Intent(this, MemoryLeakageTwoActivity.class));
    }
 
    public static void setActivity(Activity activity) {
         //使用软引用方式来引用外部类的实例对象
        mActivity = new SoftReference<>(activity);
    }
}

使用 软引用 方式来引用『MemoryLeakageTwoActivity』类的实例对象。( 软引用: GC 触发时会回收软引用指向的对象)

  • mActivity 变量类型 Activity 改为 SoftReference
  • setActivity() 方法中将 mActivity = activity 改为 mActivity = new SoftReference<>(activity)

接下来继续按照 『Handler内存泄漏中的「App操作流程」「Memory Profiler操作流程」』操作一遍后。

  • Memory Profiler 界面如下:
解决本类实例被其他类持有所导致内存泄漏.png

根据上图『解决本类实例被其他类持有所导致内存泄漏.png』来看『MemoryLeakageTwoActivity』已经从堆内存中释放掉了,由此也可以看出已经解决了之前『MemoryLeakageTwoActivity』所产生的内存泄漏问题。

小结:

    1. 使用软引用方式来引用外部类的实例对象。

注:这里需要注意的地方是使用软引用时所引用的对象如果没有存在于堆内存当中那么通过软引用对象的 get() 方法进行获得软引用所引用的对象时将会为 null


3. 非静态内部类创建静态实例所导致内存泄漏

  • 应用场景如下:

在频繁的启动Activity中,为了避免重复创建相同的数据资源,我们可能会创建一个静态的数据资源的实例,来解决重复创建数据资源的问题。

  • 示例代码如下:
public class MemoryLeakageThreeActivity extends AppCompatActivity {
    private static MemoryLeakageResource mResource = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (mResource == null) {
            mResource = new MemoryLeakageResource();
        }
    }

    private class MemoryLeakageResource {

    }
}

接下来继续按照 『Handler内存泄漏中的「App操作流程」「Memory Profiler操作流程」』操作一遍后。

  • Memory Profiler 界面如下:
非静态内部类创建静态实例所导致内存泄漏.png

根据上图『非静态内部类创建静态实例所导致内存泄漏.png』来看『MemoryLeakageThreeActivity 』和『MemoryLeakageResource 』都存在于堆内存中。对于我们的 应用场景 来看应当只有『MemoryLeakageResource 』存在于堆内存中,因此对于本应用来说『MemoryLeakageThreeActivity 』 就已经产生了 内存泄漏

产生的原因:当『MemoryLeakageThreeActivity 』创建『MemoryLeakageResource 』对象实例时,由于『MemoryLeakageResource 』对象是非静态内部类因此就潜在的持有『MemoryLeakageThreeActivity 』对象的引用。又由于静态变量的生命周期和应用的生命周期一样长,所以当我们再将『MemoryLeakageResource 』对象引用赋值给一个静态变量时从而就导致了『MemoryLeakageResource 』对象无法被回收,而『MemoryLeakageResource 』对象又持有着『MemoryLeakageThreeActivity 』对象的引用。从而也就导致『MemoryLeakageThreeActivity 』对象无法被回收。

如何解决非静态内部类创建静态实例所导致内存泄漏

  • 示例代码如下:
public class MemoryLeakageThreeActivity extends AppCompatActivity {
    private static MemoryLeakageResource mResource = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (mResource == null) {
            mResource = new MemoryLeakageResource();
        }

    }
    //将内部类改为静态内部类
    private static class MemoryLeakageResource {

    }
}

将『MemoryLeakageResource』内部类改为静态内部类。
接下来继续按照 『Handler内存泄漏中的「App操作流程」「Memory Profiler操作流程」』操作一遍后。

  • Memory Profiler 界面如下:
解决非静态内部类创建静态实例所导致内存泄漏.png

根据上图『解决非静态内部类创建静态实例所导致内存泄漏.png』来看『MemoryLeakageThreeActivity 』已经从堆内存中释放掉了而『MemoryLeakageResource 』对象依然存在于堆内存,从而既满足了之前 应用场景中 提到的复用『MemoryLeakageResource 』对象又解决了之前『MemoryLeakageThreeActivity 』所产生的内存泄漏问题。

小结:

    1. 非静态内部类会潜在持有它们所属的外部类的引用,但是静态内部类却不会。
    1. 静态对象的实例和应用的生命周期一样长。

总结

    1. 当使用匿名内部类 Handler 时,应该在界面销毁的时候调用 removeCallbacksAndMessages(null) 方法将 Handler 未处理的消息进行移除,并且将 Handler 置 null。
    1. 采用软引用方式来引用外部类的实例对象。尤其是在外部类的实例对象被一个静态变量所引用这将会导致外部类的生命周期和应用的生命周期一样长。
    1. 静态对象的实例和应用的生命周期一样长。因此需要特别注意静态对象,当静态对象引用外部类的实例时,应当采用软引用的方式来引用外部类的实例
    1. 非静态内部类和匿名类内部类都会潜在持有它们所属的外部类的引用,但是静态内部类却不会。因此将内部类和匿名内部类改为静态内部类来防止内存泄漏。

注:本文只举了一部分内存泄漏的栗子以及解决方式,还有其他很多种导致内存泄漏情况例如使用了 BraodcastReceiverContentObserverFileCursorStreamBitmap 等资源应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,从而造成内存泄漏等等。简单点来说内存泄漏就是由于外部的引用从而阻止了本该被回收器回收的对象。


感谢

常见的内存泄漏原因及解决方法

你可能感兴趣的:(Android性能优化(1)—内存泄漏)