Android常见的八种导致 APP 内存泄漏的问题

转载 2016年05月30日 13:36:30

像 Java 这样具有垃圾回收功能的语言的好处之一,就是程序员无需手动管理内存分配。这减少了段错误(segmentation fault)导致的闪退,也减少了内存泄漏导致的堆空间膨胀,让编写的代码更加安全。然而,Java 中依然有可能发生内存泄漏。所以你的安卓 APP 依然有可能浪费了大量的内存,甚至由于内存耗尽(OOM)导致闪退。

传统的内存泄漏是由忘记释放分配的内存导致的,而逻辑上的内存泄漏则是由于忘记在对象不再被使用的时候释放对其的引用导致的。如果一个对象仍然存在强引用,垃圾回收器就无法对其进行垃圾回收。在安卓平台,泄漏Context对象问题尤其严重。这是因为像Activity 这样的 Context 对象会引用大量很占用内存的对象,例如 View 层级,以及其他的资源。如果 Context 对象发生了内存泄漏,那它引用的所有对象都被泄漏了。安卓设备大多内存有限,如果发生了大量这样的内存泄漏,那内存将很快耗尽。

如果一个对象的合理生命周期没有清晰的定义,那判断逻辑上的内存泄漏将是一个见仁见智的问题。幸运的是,activity 有清晰的生命周期定义,使得我们可以很明确地判断 activity 对象是否被内存泄漏。
onDestroy()
函数将在 activity 被销毁时调用,无论是程序员主动销毁 activity,还是系统为了回收内存而将其销毁。如果 onDestroy 执行完毕之后,activity 对象仍被 heap root 强引用,那垃圾回收器就无法将其回收。所以我们可以把生命周期结束之后仍被引用的 activity 定义为被泄漏的 activity。

Activity 是非常重量级的对象,所以我们应该极力避免妨碍系统对其进行回收。然而有多种方式会让我们无意间就泄露了 activity 对象。我们把可能导致 activity 泄漏的情况分为两类,一类是使用了进程全局(process-global)的静态变量,无论 APP 处于什么状态,都会一直存在,它们持有了对 activity 的强引用进而导致内存泄漏,另一类是生命周期长于 activity 的线程,它们忘记释放对 activity 的强引用进而导致内存泄漏。下面我们就来详细分析一下这些可能导致 activity 泄漏的情况。

1.静态 Activity

泄漏 activity 最简单的方法就是在 activity 类中定义一个 static 变量,并且将其指向一个运行中的
activity 实例
。如果在 activity 的生命周期结束之前,没有清除这个引用,那它就会泄漏了。这是因为 activity(例如 MainActivity) 的类对象是静态的,一旦加载,就会在 APP 运行时一直常驻内存,因此如果类对象不卸载,其静态成员就不会被垃圾回收。

void setStaticActivity() {
  activity = this;
}

View saButton = findViewById(R.id.sa_button);
saButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
    setStaticActivity();
    nextActivity();
  }
});

2.静态 View

另一种类似的情况是对经常启动的 activity 实现一个单例模式,让其常驻内存可以使它能够快速恢复状态。然而,就像前文所述,不遵循系统定义的 activity 生命周期是非常危险的,也是没必要的,所以我们应该极力避免。

但是如果我们有一个创建起来非常耗时的 View,在同一个 activity 不同的生命周期中都保持不变呢?所以让我们为它实现一个单例模式,就像这段代码。现在一旦 activity 被销毁,那我们就应该释放大部分的内存了。

void setStaticView() {
  view = findViewById(R.id.sv_button);
}

View svButton = findViewById(R.id.sv_button);
svButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
    setStaticView();
    nextActivity();
  }
});

内存泄漏了!因为一旦 view 被加入到界面中,它就会持有 context 的强引用,也就是我们的 activity。由于我们通过一个静态成员引用了这个 view,所以我们也就引用了 activity,因此 activity 就发生了泄漏。所以一定不要把加载的 view 赋值给静态变量,如果你真的需要,那一定要确保在 activity 销毁之前将其
从 view 层级中移除。

3.内部类

现在让我们在 activity 内部定义一个类,也就是内部类。这样做的原因有很多,比如增加封装性和可读性。如果我们创建了一个内部类的对象,并且通过静态变量持有了 activity 的引用,那也会发生 activity 泄漏。

void createInnerClass() {
    class InnerClass {
    }
    inner = new InnerClass();
}

View icButton = findViewById(R.id.ic_button);
icButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createInnerClass();
        nextActivity();
    }
});

内存泄漏场景 3 - Inner Class
不幸的是,内部类能够引用外部类的成员这一优势,就是通过持有外部类的引用来实现的,而这正是 activity 泄漏的原因。

4.匿名类

类似的,匿名类同样会持有定义它们的对象的引用。因此如果在
activity 内定义了一个匿名的 AsyncTask 对象
,就有可能发生内存泄漏了。如果 activity 被销毁之后 AsyncTask 仍然在执行,那就会组织垃圾回收器回收 activity 对象,进而导致内存泄漏,直到执行结束才能回收 activity。

void startAsyncTask() {
    new AsyncTask<void, void,="" void="">() {
        @Override protected Void doInBackground(Void... params) {
            while(true);
        }
    }.execute();
}

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View aicButton = findViewById(R.id.at_button);
aicButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        startAsyncTask();
        nextActivity();
    }
});

5.Handlers

同样的,定义一个匿名的 Runnable 对象并将其提交到 Handler 上也可能导致 activity 泄漏。Runnable 对象间接地引用了定义它的 activity 对象,而它会被提交到 Handler 的 MessageQueue 中,如果它在 activity 销毁时还没有被处理,那就会导致 activity 泄漏了。

void createHandler() {
    new Handler() {
        @Override public void handleMessage(Message message) {
            super.handleMessage(message);
        }
    }.postDelayed(new Runnable() {
        @Override public void run() {
            while(true);
        }
    }, Long.MAX_VALUE >> 1);
}


View hButton = findViewById(R.id.h_button);
hButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createHandler();
        nextActivity();
    }
});

6.Threads

同样的,使用
Thread 和 TimerTask
也可能导致 activity 泄漏。

void spawnThread() {
    new Thread() {
        @Override public void run() {
            while(true);
        }
    }.start();
}

View tButton = findViewById(R.id.t_button);
tButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
      spawnThread();
      nextActivity();
  }
});

7.Timer Tasks

只要它们是通过匿名类创建的,尽管它们在单独的线程被执行,它们也会持有对 activity 的强引用,进而导致内存泄漏。

void scheduleTimer() {
    new Timer().schedule(new TimerTask() {
        @Override
        public void run() {
            while(true);
        }
    }, Long.MAX_VALUE >> 1);
}

View ttButton = findViewById(R.id.tt_button);
ttButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        scheduleTimer();
        nextActivity();
    }
});

8.Sensor Manager

最后,系统服务可以通过 context.getSystemService 获取,它们负责执行某些后台任务,或者为硬件访问提供接口。如果 context 对象想要在服务内部的事件发生时被通知,那就需要把自己注册到服务的监听器中。然而,这会让服务持有 activity 的引用,如果程序员忘记在 activity 销毁时取消注册,那就会导致 activity 泄漏了。

void registerListener() {
       SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
       Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
       sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}

View smButton = findViewById(R.id.sm_button);
smButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        registerListener();
        nextActivity();
    }
});

现在,我们展示了八种很容易不经意间就泄漏大量内存的情景。请记住,最坏的情况下,你的 APP 可能会由于大量的内存泄漏而内存耗尽,进而闪退,但它并不总是这样。相反,内存泄漏会消耗大量的内存,但却不至于内存耗尽,这时,APP 会由于内存不够分配而频繁进行垃圾回收。垃圾回收是非常耗时的操作,会导致严重的卡顿。在 activity 内部创建对象时,一定要格外小心,并且要经常测试是否存在内存泄漏。

Android 通过log看是否内存低导致的应用闪退

好多应用闪退问题都是由于内存低,LMK策略强制杀应用导致。其实我们可以通过log去看看是否是因为内存低而导致的应用闪退。 首先打开kernel.log, 如果内存太低启用了LMK,会看到有killi...
  • zimu_2080
  • zimu_2080
  • 2016年09月02日 16:01
  • 1115

android 重复进入app heapSize不断增大的问题

最近项目中碰到了一个特别奇葩的问题,上stackoverflow搜索了半天,最后终于一句话解决了,system.exit(0);没错,相信程序中都存在双击退出程序的功能,但是我用DDMS分析应用程序的...
  • ly985557461
  • ly985557461
  • 2014年11月24日 12:28
  • 1752

Android客户端内置内存工具进行崩溃定位的实践经验

前言 本宝宝苦啊,辛辛苦苦上线一个版本,上线之后,看到崩溃日志,感觉整个人都不好了. 别人家的崩溃日志是这样子的: 1 Fatal Exception: java.lang.Nul...
  • omnispace
  • omnispace
  • 2016年02月25日 18:12
  • 628

Android常见Crash原因

1.Java Crash java代码导致jvm退出,弹出“程序已经崩溃”的对话框,最终用户点击关闭后进程退出。Logcat会在“AndroidRuntime”tag下输出Java的调用栈。 2...
  • kuangshow0227
  • kuangshow0227
  • 2017年06月13日 20:32
  • 474

Android App解决卡顿慢之内存抖动及内存泄漏(发现和定位)

内存抖动是指在短时间内有大量的对象被创建或者被回收的现象,内存抖动出现原因主要是频繁(很重要)在循环里创建对象(导致大量对象在短时间内被创建,由于新对象是要占用内存空间的而且是频繁,如果一次或者两次在...
  • huang_rong12
  • huang_rong12
  • 2016年06月10日 19:01
  • 8597

android app性能优化大汇总(内存性能优化)

android app性能优化大汇总(内存性能优化) 转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持!   写在最...
  • zhaoweixing1989
  • zhaoweixing1989
  • 2016年10月21日 16:27
  • 851

Android应对Activity进程被杀死

通常在一部Android手机里同时运行着多个应用(app),每个app对应一个系统进程,当系统需要更多的资源(如内存)而空闲资源不足时,Android系统就会选择杀掉一些低优先级的进程以便释放所需资源...
  • xinzheng_wang
  • xinzheng_wang
  • 2014年04月06日 17:44
  • 16783

Android之内存泄漏以及解决办法(持更)

Android之常见内存泄漏以及解决办法文章链接知识点: 单例造成的内存泄漏原因和解决方法; Handler造成的内存泄漏原因和解决方法; 耗时线程造成的内存泄漏原因和解决方法; 非静态内部类造成的内...
  • qq_16628781
  • qq_16628781
  • 2017年03月29日 00:27
  • 1650

八个造成 Android 应用内存泄露的原因

原文链接 : Eight Ways Your Android App Can Leak Memory原文作者 : Tom Huzij译文出自 : 掘金翻译计划译者 : zhangzhaoqi校对者...
  • north1989
  • north1989
  • 2016年07月22日 22:37
  • 1364

Android代码内存优化建议-Android官方篇

转自:http://androidperformance.com/ http://developer.android.com/intl/zh-cn/training/displaying-b...
  • Hknock
  • Hknock
  • 2015年08月06日 19:58
  • 15453
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Android常见的八种导致 APP 内存泄漏的问题
举报原因:
原因补充:

(最多只允许输入30个字)