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<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 = new SoftReference<>(activity);
    }
}

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

  • mActivity 变量类型 Activity 改为 SoftReference<Activity>
  • 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销毁时及时关闭或者注销,否则这些资源将不会被回收,从而造成内存泄漏等等。简单点来说内存泄漏就是由于外部的引用从而阻止了本该被回收器回收的对象。


感谢

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容