Android性能优化之从卡顿和ANR来彻底理解内存泄露原理和优化

移动开发 Android
JAVA程序,因为有垃圾回收机制,应该没有内存泄露。我们已经知道了,如果某个对象,从根节点可到达,也就是存在从根节点到该对象的引用链,那么该对象是不会被 GC 回收的。

[[415143]]

前言

  • JAVA程序,因为有垃圾回收机制,应该没有内存泄露。我们已经知道了,如果某个对象,从根节点可到达,也就是存在从根节点到该对象的引用链,那么该对象是不会被 GC 回收的。
  • 如果说这个对象已经不会再被使用到了,是无用的,我们依然持有他的引用的话,就会造成内存泄漏,例如 一个长期在后台运行的线程持有 Activity 的引用,这个时 候 Activity 执行了 onDestroy 方法,那么这个 Activity 就是从根节点可到达并且无用的对象, 这个 Activity 对象就是泄漏的对象,给这个对象分配的内存将无法被回收。
  • 如果我们的java运行很久,而这种内存泄露不断的发生,最后就没内存可用了。
  • 当然java的,内存泄漏和C/C++是不一样的。如果java程序完全结束后,它所有的对象就都不可达了,系统就可以对他们进行垃圾回收,它的内存泄露仅仅限于它本身,而不会影响整个系统的。C/C++的内存泄露就比较糟糕了,它的内存泄露是系统级,即使该C/C++程序退出,它的泄露的内存也无法被系统回收,永远不可用了,除非重启机器。
  • 我们这篇文章就开始对Android内存泄露进行总结;

一、Android内存泄露介绍

1、什么是内存泄露?

  • 内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
  • 内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃;
  • Android的一个应用程序的内存泄露对别的应用程序影响不大。为了能够使得Android应用程序安全且快速的运行,Android的每个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,它是由Zygote服务进程孵化出来的,也就是说每个应用程序都是在属于自己的进程中运行的。
  • Android为不同类型的进程分配了不同的内存使用上限,如果程序在运行过程中出现了内存泄漏的而造成应用进程使用的内存超过了这个上限,则会被系统视为内存泄漏,从而被kill掉,这使得仅仅自己的进程被kill掉,而不会影响其他进程(如果是system_process等系统进程出问题的话,则会引起系统重启)。

2、内存泄露的危害

  • 用户对单次的内存泄漏并没有什么感知,但是当泄漏积累到内存都被消耗完,就会导致卡顿,甚至崩溃;
  • gc回收频繁 造成应用卡顿ANR:
  • 当内存不足的时候,gc会主动回收没用的内存.但是,内存回收也是需要时间的.
  • 内存回收和gc回收垃圾资源之间高频率交替的执行.就会产生内存抖动.
  • 很多数据就会污染内存堆,马上就会有许多GCs启动,由于这一额外的内存压力,也会产生突然增加的运算造成卡顿现象,
  • 任何线程的任何操作都会需要暂停,等待GC操作完成之后,其他操作才能够继续运行,所以垃圾回收运行的次数越少,对性能的影响就越少;

3、内存泄漏的原因

①内存空间使用完毕后没有被回收,就会导致内存泄漏。虽然Java有垃圾回收机制,但是Java中任然存在很多造成内存泄漏的代码逻辑,垃圾回收器会回收掉大部分的内存空间,但是有一些内存空间还保持着引用,但是在逻辑上已经不会再用到的对象,这时候垃圾回收器就很无能为力,不能回收它们,比如:

  • 忘记释放分配的内存;
  • 应用不需要这个对象了,但是却没有释放这个对象的引用;
  • 强引用持有的对象,垃圾回收器是无法回收这个对象;
  • 持有对象生命周期过长,导致无法回收;

②Android(Java)平台的内存泄漏是指没用的对象资源与GC Roots之间保持可达路径,导致系统无法进行回收;

③那么从栈中弹出的对象将不会被当作垃圾回收,即使程序不再使用栈中的这些队象,他们也不会回收,因为栈中仍然保存这对象的引用,俗称过期引用,这个内存泄露很隐蔽;

二、检测内存泄露检测工具

①Memory Monitor

位于 Android Monitor 中,该工具可以:

  • 方便的显示内存使用和 GC 情况
  • 快速定位卡顿是否和 GC 有关
  • 快速定位 Crash 是否和内存占用过高有关
  • 快速定位潜在的内存泄露问题(内存占用一直在增长)
  • 但是不能准确的定位问题

②Allocation Tracker

该工具用途:

  • 可以定位代码中分配的对象类型、大小、时间、线程、堆栈等信息
  • 可以定位内存抖动问题
  • 配合 Heap Viewer 定位内存泄露问题(可以找出来泄露的对象是在哪创建的等等)
  • 使用方法:在 Memory Monitor 中有个 Start Allocation Tracking 按钮即可开始跟踪 在点击停止跟踪后会显示统计结果。

③Heap Viewer

该工具用于:

  • 显示内存快照信息
  • 每次 GC 后收集一次信息
  • 查找内存泄露的利器
  • 使用方法:在 Memory Monitor 中有个 Dump Java Heap 按钮,点击即可,在统计报告左上角选按 package 分类。配合 Memory Monitor 的 initiate GC(执行 GC)按钮,可检测内存泄露等情况。

④LeakCanary

  1. dependencies { 
  2.   debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.3' 
  3.   releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3' 
  4.   // Optional, if you use support library fragments: 
  5.   debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.3' 

直接在Application中使用,然后运行APP就会自动检测,检测到会在另一个APP上通知,显示详情

  1. public class ExampleApplication extends Application { 
  2.   @Override public void onCreate() { 
  3.     super.onCreate(); 
  4.     if (LeakCanary.isInAnalyzerProcess(this)) { 
  5.       // This process is dedicated to LeakCanary for heap analysis. 
  6.       // You should not init your app in this process. 
  7.       return
  8.     } 
  9.     LeakCanary.install(this); 
  10.     // Normal app init code... 
  11.   } 

三、常见的内存泄露场景详解

1.单例导致内存泄露

单例模式在Android开发中会经常用到,但是如果使用不当就会导致内存泄露。因为单例的静态特性使得它的生命周期同应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有它的引用,那么在整个应用程序的生命周期它都不能正常被回收,从而导致内存泄露。

  1. public class AppSettings { 
  2.     private static volatile AppSettings singleton; 
  3.     private Context mContext; 
  4.     private AppSettings(Context context) { 
  5.         this.mContext = context; 
  6.     } 
  7.     public static AppSettings getInstance(Context context) { 
  8.         if (singleton == null) { 
  9.             synchronized (AppSettings.class) { 
  10.                 if (singleton == null) { 
  11.                     singleton = new AppSettings(context); 
  12.                 } 
  13.             } 
  14.         } 
  15.         return singleton; 
  16.     } 

像上面代码中这样的单例,如果我们在调用getInstance(Context context)方法的时候传入的context参数是Activity、Service等上下文,就会导致内存泄露。以Activity为例,当我们启动一个Activity,并调用getInstance(Context context)方法去获取AppSettings的单例,传入Activity.this作为context,这样AppSettings类的单例sInstance就持有了Activity的引用,当我们退出Activity时,该Activity就没有用了,但是因为sIntance作为静态单例(在应用程序的整个生命周期中存在)会继续持有这个Activity的引用,导致这个Activity对象无法被回收释放,这就造成了内存泄露。

为了避免这样单例导致内存泄露,我们可以将context参数改为全局的上下文:

  1. private AppSettings(Context context) { 
  2.         this.mContext = context.getApplicationContext(); 

2.静态变量导致内存泄漏

静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放。比如下面这样的情况,在Activity中为了避免重复的创建info,将sInfo作为静态变量:

  1. public class MainActivity2 extends AppCompatActivity { 
  2.     public static Info sInfo; 
  3.     @Override 
  4.     protected void onCreate(Bundle savedInstanceState) { 
  5.         super.onCreate(savedInstanceState); 
  6.         sInfo = new Info(this); 
  7.     } 
  8.     class Info { 
  9.         private Context mContext; 
  10.         public Info(Context context) { 
  11.             this.mContext = context; 
  12.         } 
  13.     } 

Info作为Activity的静态成员,并且持有Activity的引用,但是sInfo作为静态变量,生命周期肯定比Activity长。所以当Activity退出后,sInfo仍然引用了Activity,Activity不能被回收,这就导致了内存泄露。

在Android开发中,静态持有很多时候都有可能因为其使用的生命周期不一致而导致内存泄露,所以我们在新建静态持有的变量的时候需要多考虑一下各个成员之间的引用关系,并且尽量少地使用静态持有的变量,以避免发生内存泄露。当然,我们也可以在适当的时候讲静态量重置为null,使其不再持有引用,这样也可以避免内存泄露。

3.非静态内部类导致内存泄露

非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。非静态内部类导致的内存泄露在Android开发中有一种典型的场景就是使用Handler,很多开发者在使用Handler是这样写的:

  1. public class MainActivity2 extends AppCompatActivity { 
  2.     @Override 
  3.     protected void onCreate(Bundle savedInstanceState) { 
  4.         super.onCreate(savedInstanceState); 
  5.         start(); 
  6.     } 
  7.     private void start() { 
  8.         Message message = Message.obtain(); 
  9.         message.what = 1; 
  10.         mHandler.sendMessage(message); 
  11.     } 
  12.     private Handler mHandler = new Handler() { 
  13.         @Override 
  14.         public void handleMessage(Message msg) { 
  15.             super.handleMessage(msg); 
  16.             if (msg.what == 1) { 
  17.                 //doNothing 
  18.             } 
  19.         } 
  20.     }; 

也许有人会说,mHandler并未作为静态变量持有Activity引用,生命周期可能不会比Activity长,应该不一定会导致内存泄露呢,显然不是这样的!熟悉Handler消息机制的都知道,mHandler会作为成员变量保存在发送的消息msg中,即msg持有mHandler的引用,而mHandler是Activity的非静态内部类实例,即mHandler持有Activity的引用,那么我们就可以理解为msg间接持有Activity的引用。msg被发送后先放到消息队列MessageQueue中,然后等待Looper的轮询处理(MessageQueue和Looper都是与线程相关联的,MessageQueue是Looper引用的成员变量,而Looper是保存在ThreadLocal中的)。那么当Activity退出后,msg可能仍然存在于消息对列MessageQueue中未处理或者正在处理,那么这样就会导致Activity无法被回收,以致发生Activity的内存泄露。

通常在Android开发中如果要使用内部类,但又要规避内存泄露,一般都会采用静态内部类+弱引用的方式。

  1. MyHandler mHandler; 
  2. public static class MyHandler extends Handler { 
  3.         private WeakReference<Activity> mActivityWeakReference; 
  4.         public MyHandler(Activity activity) { 
  5.             mActivityWeakReference = new WeakReference<>(activity); 
  6.         } 
  7.         @Override 
  8.         public void handleMessage(Message msg) { 
  9.             super.handleMessage(msg); 
  10.         } 

mHandler通过弱引用的方式持有Activity,当GC执行垃圾回收时,遇到Activity就会回收并释放所占据的内存单元。这样就不会发生内存泄露了。上面的做法确实避免了Activity导致的内存泄露,发送的msg不再已经没有持有Activity的引用了,但是msg还是有可能存在消息队列MessageQueue中,所以更好的是在Activity销毁时就将mHandler的回调和发送的消息给移除掉。

  1. @Override 
  2.     protected void onDestroy() { 
  3.         super.onDestroy(); 
  4.         mHandler.removeCallbacksAndMessages(null); 
  5.  } 

非静态内部类造成内存泄露还有一种情况就是使用Thread或者AsyncTask。要避免内存泄露的话还是需要像上面Handler一样使用静态内部类+弱应用的方式(代码就不列了,参考上面Hanlder的正确写法)。

4.未取消注册或回调导致内存泄露

比如我们在Activity中注册广播,如果在Activity销毁后不取消注册,那么这个刚播会一直存在系统中,同上面所说的非静态内部类一样持有Activity引用,导致内存泄露。因此注册广播后在Activity销毁后一定要取消注册。在注册观察则模式的时候,如果不及时取消也会造成内存泄露。比如使用Retrofit+RxJava注册网络请求的观察者回调,同样作为匿名内部类持有外部引用,所以需要记得在不用或者销毁的时候取消注册。

5.Timer和TimerTask导致内存泄露

Timer和TimerTask在Android中通常会被用来做一些计时或循环任务,比如实现无限轮播的ViewPager:

  1. private void stopTimer(){ 
  2.         if(mTimer!=null){ 
  3.             mTimer.cancel(); 
  4.             mTimer.purge(); 
  5.             mTimer = null
  6.         } 
  7.         if(mTimerTask!=null){ 
  8.             mTimerTask.cancel(); 
  9.             mTimerTask = null
  10.         } 
  11.     } 
  12.     @Override 
  13.     protected void onDestroy() { 
  14.         super.onDestroy(); 
  15.         stopTimer(); 
  16.     } 

当我们Activity销毁的时,有可能Timer还在继续等待执行TimerTask,它持有Activity的引用不能被回收,因此当我们Activity销毁的时候要立即cancel掉Timer和TimerTask,以避免发生内存泄漏。

6.集合中的对象未清理造成内存泄露

这个比较好理解,如果一个对象放入到ArrayList、HashMap等集合中,这个集合就会持有该对象的引用。当我们不再需要这个对象时,也并没有将它从集合中移除,这样只要集合还在使用(而此对象已经无用了),这个对象就造成了内存泄露。并且如果集合被静态引用的话,集合里面那些没有用的对象更会造成内存泄露了。所以在使用集合时要及时将不用的对象从集合remove,或者clear集合,以避免内存泄漏。

7.资源未关闭或释放导致内存泄露

在使用IO、File流或者Sqlite、Cursor等资源时要及时关闭。这些资源在进行读写操作时通常都使用了缓冲,如果不及时关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。因此我们在不需要使用它们的时候就及时关闭,以便缓冲能及时得到释放,从而避免内存泄露。

8.属性动画造成内存泄露

动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放。因此同样要在Activity销毁的时候cancel掉属性动画,避免发生内存泄漏。

9.WebView造成内存泄露

关于WebView的内存泄露,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存。另外在查阅WebView内存泄露相关资料时看到这种情况:Webview下面的Callback持有Activity引用,造成Webview内存无法释放,即使是调用了Webview.destory()等方法都无法解决问题(Android5.1之后)。最终的解决方案是:在销毁WebView之前需要先将WebView从父容器中移除,然后再销毁WebView。

总结

  • 对于生命周期比Activity长的对象(单例),要避免直接引用Activity的context,可以考虑使用ApplicationContext,静态变量不使用时及时置空;
  • Handler持有的引用最好使用弱引用,在Activity被释放的时候要记得清空Message,取消Handler对象的Runnable;
  • 非静态内部类、非静态匿名内部类会自动持有外部类的引用,为避免内存泄露,可以考虑把内部类声明为静态的;
  • 广播接收器、EventBus等的使用过程中,注册/反注册应该成对使用,但凡有注册的都应该有反注册;
  • 不再使用的资源对象Cursor、File、Bitmap等要记住正确关闭;

集合里面的东西有加入就应该对应有相应的删除。 

属性动画及时取消,注意webview内存泄漏问题

 

责任编辑:武晓燕 来源: Android开发编程
相关推荐

2010-08-10 10:00:57

Flex内存

2021-07-27 20:51:02

AndroidDNS网络

2021-07-29 14:20:34

网络优化移动互联网数据存储

2019-12-04 10:23:33

HBase内存MemStore

2019-12-13 10:25:08

Android性能优化启动优化

2010-07-29 14:08:05

Flex内存泄露

2020-12-21 08:32:07

内存性能优化

2013-02-20 14:32:37

Android开发性能

2013-09-17 10:32:08

Android性能优化数据库

2017-03-14 18:48:06

Android性能优化内存优化

2015-09-16 15:21:23

Android性能优化内存

2016-12-22 17:21:11

Android性能优化内存泄漏

2018-06-07 08:54:01

MySQL性能优化索引

2024-02-02 15:21:08

工具页面性能

2018-11-14 19:30:57

前端Javascript性能优化

2013-09-16 16:56:09

AndroidBitmap内存优化

2013-09-16 16:01:23

Android开发代码

2017-01-15 15:13:37

Android性能优化优化点

2015-09-16 15:48:55

Android性能优化电量

2015-09-16 14:37:50

Android性能优化运算
点赞
收藏

51CTO技术栈公众号