13518219792

建站动态

根据您的个性需求进行定制 先人一步 抢占小程序红利时代

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

[[415143]]

前言

一、Android内存泄露介绍

1、什么是内存泄露?

成都创新互联公司主营泽普网站建设的网络公司,主营网站建设方案,app软件定制开发,泽普h5小程序开发搭建,泽普网站营销推广欢迎泽普等地区企业咨询

2、内存泄露的危害

3、内存泄漏的原因

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

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

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

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

①Memory Monitor

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

②Allocation Tracker

该工具用途:

③Heap Viewer

该工具用于:

④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 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。

总结

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

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


分享标题:Android性能优化之从卡顿和ANR来彻底理解内存泄露原理和优化
分享链接:http://cdbrznjsb.com/article/dhopcge.html

其他资讯

让你的专属顾问为你服务