Android内存泄漏及分析

内存泄漏的定义

大家都知道,Java是有垃圾回收机制的,这使得Java程序员比C++程序员轻松了许多,存储申请了,不用心心念念要加一句释放,Java虚拟机会派出一些回收线程兢兢业业不定时地回收那些不再被需要的内存空间(注意回收的不是对象本身,而是对象占据的内存空间)。

什么叫不再被需要的内存空间

Java没有指针,全凭引用来和对象进行关联,通过引用来操作对象。如果一个对象没有与任何引用关联,那么这个对象也就不太可能被使用到了,回收器便是把这些“无任何引用的对象”作为目标,回收了它们占据的内存空间

如何分辨为对象无引用

1.引用计数法
直接计数,简单高效,Python便是采用该方法。但是如果出现两个对象相互引用,即使它们都无法被外界访问到,计数器不为0它们也始终不会被回收。为了解决该问题,Java采用的是可达性分析法

2.可达性分析法
这个方法设置了一系列的“GC Roots”对象作为索引起点,如果一个对象与起点对象之间均无可达路径,那么这个不可达的对象就会成为回收对象。这种方法处理两个对象相互引用的问题,如果两个对象均没有外部引用,会被判断为不可达对象进而被回收

两种方法图示如下:

这里写图片描述

不幸的是,虽然垃圾回收器会帮我们干掉大部分无用的内存空间,但是对于还保持着引用,但逻辑上已经不会再用到的对象,垃圾回收器不会回收它们。这些对象积累在内存中,直到程序结束,就是我们所说的“内存泄漏”。

进一步了解垃圾回收和引用请移步JVM垃圾回收(深入理解Java虚拟机学习笔记)

内存泄漏的源头

  • 自身编码引起:由项目开发人员自身的编码造成。

  • 第三方代码引起:第三方非开源的SDK和开源的第三方框架。

  • 系统原因:由 Android 系统自身造成的泄漏,如像WebView 、InputMethodManager等引起的问题,还有某些第三方 ROM 存在的问题。

Android中常见的内存泄漏种类

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

如果我们有一个创建起来非常耗时的 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();
    }
});

不幸的是,内部类能够引用外部类的成员这一优势,就是通过持有外部类的引用来实现的,而这正是 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.Handler

同样的,定义一个匿名的 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();
    }
});

这里提一下解决方法:可以自己写一个继承Handler的静态子类,如果需要引用Activity请使用软引用或者虚引用

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

6.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();
    }
});


同样的,对于使用了BraodcastReceiverContentObserverFile,游标 CursorStreamBitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

总结

如上,我们展示了几种很容易不经意间就泄漏大量内存的情景,真正总结下来其实就三点:

  • 静态变量引用Activity

  • 内部类或者匿名内部类的生命周期长于外部类

  • 使用资源没有关闭

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

内存泄漏分析

使用Android Monitor + MAT

Android Monitor是Android Studio自带的内存分析工具。

MAT(Memory Analyzer Tool)是基于Eclipse的内存分析工具

第一步:强制GC,生成Java Heap文件

我们都知道Java有一个非常强大的垃圾回收机制,会帮我回收无引用的对象,这些无引用的对象不在我们内存泄漏分析的范畴,Android Studio有一个Android Monitor帮助我们进行强制GC,获取Java Heap文件。

这里写图片描述

强制GC:点击Initate GC(1)按钮,建议点击后等待几秒后再次点击,尝试多次,让GC更加充分。然后点击Dump Java Heap(2)按钮,然后等到一段时间,生成有点慢。

生成的Java Heap文件会在新建窗口打开:

这里写图片描述

第二步:分析内存泄漏的Activity

这里写图片描述

点击Analyzer TasksPerform Analysis(1)按钮,然后等待几秒十几秒不等,即可找出内存泄漏的Activity(2)。

那么我们就可以知道内存泄漏的Activity,因为这个例子比较简单,其实在(3)就已经可以看到问题所在,如果比较复杂的问题Android Studio并不够直观,不够MAT方便,如果Android Studio无法解决我们的问题,就建议使用MAT来分析,所以下一步我们就生成标准的hprof文件,通过MAT来找出泄漏的根源。

第三步:转换成标准的hprof文件

刚才生成的Heap文件不是标准的Java Heap,所以MAT无法打开,我们需要转换成标准的Java Heap文件,这个工具Android Studio就有提供,叫做Captures,右击选中的hprof,Export to standard .hprof选择保存的位置,即可生成一个标准的hprof文件:

这里写图片描述

第四步:MAT打开hprof文件

MAT的下载地址

这里写图片描述

MAT的使用方式和eclipse一样,这里就不多说了,打开刚才生成的hprof文件。点击(1)按钮打开Histogram。(2)这里是支持正则表达式,我们直接输入Activity名称,点击enter键即可。

当然也可以利用QQL,点击下图中标记的QQL图标,然后输入select * from instanceof android.app.Activity

这里写图片描述

然后我们搜索到了目标的Activity:

这里写图片描述

右击搜索出来的类名,选择Merge Shortest Paths to GC Rootsexclude all phantom/weak/soft etc. references,来到这一步,就可以看到内存泄漏的原因,我们就需要根据内存泄漏的信息集合我们的代码去分析原因:

这里写图片描述

第五步:根据内存泄漏信息和代码分析原因

使用Handler案例分析,给出的信息是Thread和android.os.Message,这个Thread和Message配合通常是在Handler使用,结合代码,所以我猜测是Handler导致内存泄漏问题,查看代码,直接就在函数中定义了一个final的Handler用来定时任务,在Activity的onDestroy后,这个Handler还在不断地工作,导致Activity无法正常回收:

// 导致内存泄漏的代码
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
textView = (TextView) findViewById(R.id.text);
final Handler handler = new Handler();
handler.post(new Runnable() {
 @Override
 public void run() {
   textView.setText(String.valueOf(timer++));
   handler.postDelayed(this, 1000);
  }
 });
}

修改代码,避免内存泄漏:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
textView = (TextView) findViewById(R.id.text);
handler.post(new Runnable() {
 @Override
 public void run() {
  textView.setText(String.valueOf(timer++));
  if (handler != null) {
   handler.postDelayed(this, 1000);
  }
 }
});
}
private Handler handler = new Handler();
@Override
protected void onDestroy() {
 super.onDestroy();
 // 避免Handler导致内存泄漏
 handler.removeCallbacksAndMessages(null);
 handler = null;
}

重新测试,确保问题已经解决。

以上用例代码:https://github.com/taoweiji/DemoAndroidMemoryLeak

使用DDMS + MAT

DDMS最早是Eclipse开发Android的ADT插件的一部分,现在Android Studio中也有,打开方式如下:

这里写图片描述

运行程序,然后进入 DDMS管理界面,如下:

这里写图片描述

点击工具栏上的 这里写图片描述来更新统计信息。

然后点击右侧的 Cause GC 按钮或工具栏上的这里写图片描述即可查看当前的堆情况,如下:

这里写图片描述

点击工具栏上的这里写图片描述按钮,将内存信息保存成文件,再转换成标准的hprof格式,利用MAT查看。

静态代码分析工具 —— Lint

Lint 是 Android Studio 自带的工具,使用姿势很简单 Analyze -> Inspect Code 然后选择想要扫面的区域即可:

这里写图片描述

这里写图片描述

这里写图片描述

这里只是抛砖引玉的介绍 Lint ,实际上玩法还有很多,大家可以自行拓展学习。除了 Lint 外,还有像 FindBugsCheckstyle 等静态代码分析工具也是很不错的。

严苛模式 —— StrictMode

Android官方文档–StrictMode

StrictMode 是 Android 系统提供的 API ,在开发环境下引入可以更早的暴露发现问题。

以官网的示例代码为栗子,一般 StrictMode 只在测试环境下启用,到了生产环境就会进行关闭,通常我们都会借助 BuildConfig.DEBUG 来实现:

这里写图片描述

启用 StrictMode 后,在过滤日志的地方加上 StrictMode 的过滤 Tag ,如果手机连接着电脑进行开发,定期观察一下 StrictMode 这个 Tag 下的日志,一般你看到一大堆红色告警的 Log,就需要好好排查一下是否跟内存泄漏有关了:

这里写图片描述

LeakCanary

LeakCanary是Square公司出品的内存分析工具。

LeakCanary 和 StrictMode 一样,需要在项目代码中集成,不过代码也非常简单,如下的官方示例:

In your build.gradle:

 dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.4'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
 }

In your Application class:

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}

详细使用除了官方的README也可以参考:LeakCanary 中文使用说明

我对使用LeakCanary有以下两点感受:

  • 当内存泄漏发生时,LeakCanary 会弹窗提示并生成对应的堆存储信息记录,这让我们对隐蔽的内存泄漏问题有了更加直观的感觉,但从实际使用来看,LeakCanary 的每个提示也并非是真正存在内存泄漏问题,要想确定是否存在问题我们还需要借助 MAT 来进行最后的确定。

  • Android 系统本身就存在一些问题导致应用内存泄漏,LeakCanary 的 AndroidExcludedRefs 类帮助我们处理了不少这类问题。

参考:
1.常见的八种导致 APP 内存泄漏的问题
2.Android内存泄漏的简单检查与分析方法
3.Android 内存泄漏总结
4.Android 应用内存泄漏的定位、分析与解决策略
5.利用Android Studio、MAT对Android进行内存泄漏检测
6.Android Studio +MAT 分析内存泄漏实战
7.内存分析工具 MAT 的使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值