Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 563|回复: 0

Android 内存泄漏分析与解决方法

[复制链接]
  • TA的每日心情
    奋斗
    2024-4-6 11:05
  • 签到天数: 748 天

    [LV.9]以坛为家II

    2034

    主题

    2092

    帖子

    70万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    705612
    发表于 2021-8-27 14:02:25 | 显示全部楼层 |阅读模式

    在分析Android内存泄漏之前,先了解一下JAVA的一些知识
    1. JAVA中的对象的创建

    • 使用new指令生成对象时,堆内存将会为此开辟一份空间存放该对象
      垃圾回收器回收非存活的对象,并释放对应的内存空间

    2.Java如何判定对象存活或死亡?

    • 引用计数法
      1给对象中添加一个引用计数,假如为count
      2当引用这个对象时:count++
      3当count==0时:对象处于,也就是说没有其它地方在引用这个对象了,对象就处于“死亡”状态,回收对象

    • 可达性分析算法
      举个例子:像找人一样,A认识B,B认识C,C认识D,那么A就要吧通过这样的关系认识D,如果能找到D,说明D对象是存活的,不能回收,如果通过所有的关系都找不到D,说明D是“死亡”的,回收D对象。
      可达性分析算法的定义:通过一系列的称为 GC
      Roots 的对象作为起点,从这些节点开始向下搜索,搜索把走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连(就是从GC Roots 到这个对象不可达)时,则证明此对象是不可用的。如下图,Object5,Object6,Object7就是不可达对象,是要被回收的对象
      75e26954092541a59a9979876fdfee03_image.png

    问:哪些对象可以作为GC Roots对象呢?

    • 1虚拟机栈中引用的对象
    • 2方法区中类静态属性引用的对象
    • 3方法区中常量引用的对象
    • 4本地方法栈中JNI引用的对象

    3.引用分类

    • 强引用:只要强引用还存在,垃圾回收器永远不回收强引用的对象.如下
    Object obj = new Object()  //强引用
    
    • 软引用:在内存溢出异常之前,回收对象
    String str=new String("123");   // 强引用  
    SoftReference softRef=new SoftReference(str);     // 软引用
    
    • 弱引用 : 在下一次 GC 时,无论当前内存是否足够,都会回收被引用的对象
    String str=new String("abc");      
    WeakReference abcWeakRef = new WeakReference(str);  
    str=null;
    
    • 虚引用:虚引用 : 没用,形同虚设,唯一的用处是在对象回收时,会收到一个系统通知

    注:JAVA中这4种引用的级别由高到低依次为: 强引用 > 软引用 > 弱引用 > 虚引用

    ** 4.JAVA中内存分配 **

    • 静态储存区:编译时就分配好,在程序整个运行期间都存在。它主要存放静态数据和常量
    • 栈区:当方法执行时,会在栈区内存中创建方法体内部的局部变量,方法结束后自动释放内存
    • 堆区:通常存放 new 出来的对象。由 Java 垃圾回收器回收。

    上面的是JAVA的一些预备知识,下面分析Android内存泄漏相关

    ** 1 内存泄漏与内存溢出**

    • 内存泄漏:Memory Leak , 无用的对象应该被回收的没有被回收
    • 内存溢出:常说的OOM,没有足够的内存供分配了

    ** 2 Android内存泄漏分类 **

    • 长期持有(Activity)Context导致的
      (1) 单例类持有Activity引用
      (2) 长生命周期引用短生命周期

    • 由非静态内部类或者匿名内部类导致的
      (1) Handler泄漏

    • 资源使用完忘记释放
      (1) Cursor,InputStream/OutputStream 忘记调用close

    • 使用某些系统服务不当
      (1) 在6.0系统,获取ConnectivityManager服务,如果第一次使用的是Activity对应的Context去获取这个服务,就会导致内存泄漏

    • 延迟的任务也可能导致内存泄漏
      (1) Handler 的消息未处理完,这时如果Handler是在Activity内存类实现的,消息引用Handler,Handler又引用了Activity,这时如果关闭Activity,就会造成内存泄漏

    • 忘记注销监听器或者观察者
      (1) 比如 EventBus.unregister() 忘记调用

    注:非静态内部类和匿名内部类都会潜在的引用它们所属的外部类,但是静态内部类却不会

    ** 3 Android内存泄漏分析工具 **

    • MAT
    • LeakCanary
    • Strictmode
    • Android Memory Monitors

    推荐使用LeakCanary,•LeakCanary是一个检测Java和Android内存泄漏的库,集成LeakCanary之后,只需要等待内存泄漏出现就可以了无需认为进行主动检测

    ** 4 LeakCanary的添加 **

    • 第一步:在build.gradle中添加依赖
      compile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
    • 第二步:在Application的onCreate()方法中添加 LeakCanary.install(this);

    完成以上两步,就添加了LeakCanary,接下来就正常开发测试就行了,如果有内存泄漏,就会在通知栏中会有相应的通知,点开看就可以了,找到对应的内存泄漏的地方,解决

    下面是演示的内存泄漏的几张图,可以看一下:
    2b1eb131874b43298dcbe15fd3cdfaf2_image.png

    569db1221ca94d6f83c7a6c9f1c857c4_image.png

    3932f31ca9ad483182d47073321e567a_image.png

    5 Android内存泄漏的案例

    • 案例一:单例造成的内存泄漏
      典型比如context的使用不当造成内存泄漏
    public class ToastUtils {
        private static String oldMsg;
        protected static Toast toast = null;
        private static long oneTime = 0;
        private static long twoTime = 0;
        private static long gapTime = 3 * 1000;//3s只显示一次
    
        public static void show(Context context, String s) {
            if (context != null && !TextUtils.isEmpty(s)) {
                if (toast == null) {
                    toast = Toast.makeText(context, s, Toast.LENGTH_SHORT);
                    toast.show();
                    oneTime = System.currentTimeMillis();
                } else {
                    twoTime = System.currentTimeMillis();
                    if (s.equals(oldMsg)) {
                        if (twoTime - oneTime > gapTime) {
                            toast.show();
                        }
                    } else {
                        oldMsg = s;
                        toast.setText(s);
                        toast.show();
                    }
                }
                oneTime = twoTime;
            }
        }
    }
    
    

    在Activity 中使用:

    ToastUtils.show(this, "登录成功");
    

    上面的代码就会出现内存泄漏,因为在activity中使用ToastUtils.show(this, "登录成功")的时候,传的第一个参数 this 代表当时的activity,而ToashTuils中的toast变量是一个静态变量,
    代码如下

     protected static Toast toast = null;
    

    创建toast对象如下代码

    toast = Toast.makeText(context, s, Toast.LENGTH_SHORT);
    

    Toast.makeText的第一参数就是上面传的activity,Toast类中有一个变量mContext会保存这个activity,就是强引用,但是toast又是一个静态的变量,静态变量的生命同期是和当前的APP的进程一样长的,所以这时我们如果关闭这个Activity,就会导致Activity被静态变量强引用,垃圾回收永远不会回收这个Activity,所以就会出现内存泄漏。
    我们看一下Toast.makeText的源码
    2e50f3e3cd964177ab6e20667f6df027_image.png

    上面图中,new一个Toast,把context传给了Toast的构造方法。

    0f331513d8b741fa97d02dfaca34075e_image.png

    所以调用 ToastUtils.show(this, "登录成功");就会导致 activity 被静态的toast变量强引用了,导致内存泄漏。

    解决方法

    用ApplicationContext替代Activity,如下代码

     public static void show(Context context, String s) {
    	 //在这里获取applicationContext,applicationContext的生命周期是和进程一样长
    	 //这样就不会出现内存泄漏了
    	context = context.getApplicationContext();
    	
            if (context != null && !TextUtils.isEmpty(s)) {
                if (toast == null) {
                    toast = Toast.makeText(context, s, Toast.LENGTH_SHORT);
                    toast.show();
    	  ......
    	 }
    }
    
    • 案例二 内部类或者匿名内部类造成的内存泄漏
      比如在Activity中使用Handler不当造成的内存泄漏
      如下图
      2c7182b25b9d404caa4b0b1bb3d53113_image.png

    上图:在MainActivity中有一个匿名内部类Handler,并且有一个此类的对象 uiHandler。
    这时我们如果在MainAcitity 中调用下面代码,就会出现内存泄漏

    uiHandle.sendMessageDelayed(uiHandle.obtainMessage(),60 * 1000);
    

    uiHandler.obtainMessage获取的msg 中有一个成员变量 target,target保存的就是uiHandle,而uiHandler又是内部类创建的对象,所以uiHandler隐式的会对当前的外部类,也就是MainActivity会有一个强引用,如下
    msg -> uiHandler -> MainActivity
    msg 引用了uiHandler,uiHandler引用了MainActivity,然后这个msg需要60s后才被处理完,在处理过程中,如果退出MainActivity,这时候就会导致内存泄漏,MainActivity回收不了。应该被回收的对象没有被回收掉,就是内存泄漏。

    注:handler机制不明白的可以先看下handler机制,message,handler,loop的关系

    解决方法

    • 可以在MainActivity的onDestroy()方法调用下面代码:
    uiHandle.removeCallbacksAndMessages(null);
    
    • Handler不要用内部类,用静态的内部类,因为静态的内部类不会引用外部类,需要外部类的地方,用弱引用,代码如下:
      68a1a0c678504ee3819e0745edd67000_image.png

    使用弱引用的时候,需要作一下判断是否为null。

    • 案例三:Activity context的不正确使用
      上面的两个案例中其实也是context的使用场景不当造成的内存泄漏,这里不再举例,我们通常使用的两种context是 Acitivty和 Application,只需要注意对context的使用不要超过它的生命同期。部分情况下可以使用applicationContext代替activity的context,因为applicatoinContext会随着应用程序的存在而存在,而不依赖于activity的生命周期。还有要慎重对context使用static关键字。

    • 案例四:一些资源使用完后没有关闭
      如数据库的游标 Cursor,输入输出流 InputStream/OutputStream没有close

    • 案例五:注册的监听器没有反注册
      如EventBus.register,ButterKnife等没有在activity的onDestroy中反注册或者其它地方反注册

    • 案例六:系统服务的泄漏
      在实际项目中发现的,在6.0系统上在activity中第一次如果用的是activity对应的context获取ConnectivityManager服务会造成内存泄漏。
      代码对下:

         /**
         * 判断是否有网络连接
         * @param context
         * @return
         */
        public static boolean isNetworkConnected(Context context) {
            if (context != null) {
                ConnectivityManager cm = (ConnectivityManager)
                        context.getSystemService(Context.CONNECTIVITY_SERVICE);
                NetworkInfo mNetworkInfo = cm.getActiveNetworkInfo();
                if (mNetworkInfo != null) {
                    return mNetworkInfo.isAvailable();
                }
            }
            return false;
        }
    

    如果是第一次在activity中调用如下代码,会发现内存泄漏

    • 注:是第一次在activity中调用,如果第一次是在application中调用不会出现内存泄漏,原因请参考下面这篇文章: http://www.jianshu.com/p/7d4b55f7ed9f
    //注意,这个this 是代表的是当前的activity
    isNetworkConnected(this)
    

    简单介绍下:先从Context的getSystemService方法开始,我们知道Activity是从ContextWrapper继承而来的,ContextWrapper中持有一个mBase实例,这个实例指向一个ContextImpl对象,同时ContextImpl对象持有一个OuterContext对象,对于Activity来说,这个OuterContext就是Activity对象。所以调用getSystemService最终会调用到ContextImpl的getSystemService方法。
    在6.0上,在6.0上,ConnectivityManager实现为单例,创建这个单例对象的时候,把相应的OuterContext就是Activity对象,保存到了ConnectivityManager中,就造成了一个单例对象强引用了activity对象,从而造成了内存泄漏,如果是第一次用的是application,则保存的不是activity而是application,反而不会出现内存泄漏了。

    使用LeakCanary检测 ConnectivityManager 内存泄漏图如下:
    3e97cb0dd4434abab3c431c8b8aa0e53_image.png

    解决方法

    使用applicationContext去获取服务,不要使用activityContext去获取服务

    上面的就是对Android内存泄漏的一些总结,如果有不正确的或者需要补充的地方,请指出,一块学习进步

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-4-28 07:01 , Processed in 0.157430 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表