Android填坑之路-内存泄漏的查找与解决

本文探讨了Android应用中的内存泄漏问题,包括错误信息、内存使用情况和内存分配策略。通过实例解释了栈和堆的区别,明确了内存泄漏的定义和可能导致的问题。文章提出了预防内存泄漏的建议,如避免非静态内部类的静态实例、及时关闭资源对象、正确使用Context等,并介绍了leakcanary工具用于定位分析内存泄漏。此外,还提供了具体的解决方案,如使用WeakReference处理Activity引用和静态内部类的Handler,以及妥善处理BroadcastReceiver、Cursor和无限循环动画。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  早上打开Mantis发现测试报错,打开logcat文件发现错误信息:java.lang.RuntimeException: Parcel android.os.Parcel@5223a96: Unmarshalling unknown type code 2131427699 at offset 488。

    这应该就是内存泄露了

    同时查看下内存使用情况adb shell dumpsys meminfo <packageName>,

发现应用的Activity退出后,view一直高居不下,更加证明了我的推断。以前没注意到这个问题,现在不解决不行了,不然ooms是迟早的事。

先复习下Java内存分配策略

Java程序运行时的内存分配策略有三种:静态分配、栈式分配和堆式分配,对应的三种存储策略使用的主要是静态存储区、栈区和堆区。

静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。 
栈区 :当方法被执行时,方法体内的局部变量都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 
堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。 
栈与堆的区别:

在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。当在一段方法块中定义一个变量时,Java 就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。

堆内存用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。在堆中分配的内存,将由 Java 垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。

举个例子:

public class Sample() {  

    int s1 = 0;  

    Sample mSample1 = new Sample();  

    public void method() {  

    int s2 = 1;  

    Sample mSample2 = new Sample();  

    }

}

Sample mSample3 = new Sample();

Sample 类的局部变量 s2 和引用变量 mSample2 都是存在于栈中,但 mSample2 指向的对象是存在于堆上的。

mSample3 指向的对象实体存放在堆上,包括这个对象的所有成员变量 s1 和 mSample1,而它自己存在于栈中。

结论:

局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。

成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的。


1.什么是内存泄漏?

用动态存储分配函数动态开辟的控空间,在使用完毕后未释放,结果导致一直占据该内存的单元,知道程序结束。即所谓的内存泄漏。简单来说就是该内存空间使用完毕之后未回收。

2.内存泄漏会导致的问题 

内存泄露就是系统回收不了那些分配出去但是又不使用的内存, 随着程序的运行,可以使用的内存就会越来越少,机子就会越来越卡,直到内存数据溢出,然后程序就会挂掉,再跟着操作系统也可能无响应。

引发原因:对象在生命周期结束时被另一个对象通过强引用持有而无法释放造成的,思路就是避免使用非静态内部类,定义内部类时,要么是放在单独的类文件中,要么就是使用静态内部类。因为静态的内部类不会持有外部类的引用,所以不会导致外部类实例的内存泄露。当你需要在静态内部类中调用外部的Activity时,我们可以使用弱引用来处理。 


Handler避免定义成非静态内部类(private修饰)


单例是我们比较简单常用的一种设计模式,然而如果单例使用不当也会导致内存泄露。比如这个例子,DashBoardTypeface需要持有一个Context作为成员变量,并且使用该Context创建字体资源。 
instance作为静态对象,其生命周期要长于普通的对象,其中也包含Activity,当我们退出Activity,默认情况下,系统会销毁当前Activity,然后当前的Activity被一个单例持有,导致垃圾回收器无法进行回收,进而产生了内存泄露。

解决的方法就是不持有Activity的引用,而是持有Application的Context引用。

场景

  • 非静态内部类的静态实例 
    非静态内部类会维持一个到外部类实例的引用,如果非静态内部类的实例是静态的,就会间接长期维持着外部类的引用,阻止被回收掉。
  • 资源对象未关闭 
    资源性对象如Cursor、File、Socket,应该在使用后及时关闭。未在finally中关闭,会导致异常情况下资源对象未被释放的隐患。
  • 注册对象未反注册 
    未反注册会导致观察者列表里维持着对象的引用,阻止垃圾回收。
  • Handler临时性内存泄露 
    Handler通过发送Message与主线程交互,Message发出之后是存储在MessageQueue中的,有些Message也不是马上就被处理的。在Message中存在一个 target,是Handler的一个引用,如果Message在Queue中存在的时间越长,就会导致Handler无法被回收。如果Handler是非静态的,则会导致Activity或者Service不会被回收。 
    由于AsyncTask内部也是Handler机制,同样存在内存泄漏的风险。 
    此种内存泄露,一般是临时性的。

预防

  • 不要维持到Activity的长久引用,对activity的引用应该和activity本身有相同的生命周期。
  • 尽量使用context-application代替context-activity
  • Activity中尽量不要使用非静态内部类,可以使用静态内部类和WeakReference代替。

参考博客:http://blog.youkuaiyun.com/xyq046463/article/details/51769728


还有另一个神器

Android应用内存泄露leakcanary工具定位分析

leakcanary是一个开源项目,一个内存泄露自动检测工具,是著名的GitHub开源组织Square贡献的,它的主要优势就在于自动化过早的发觉内存泄露、配置简单、抓取贴心,缺点在于还存在一些bug,不过正常使用百分之九十情况是OK的,其核心原理与MAT工具类似。

关于leakcanary工具的配置使用方式这里不再详细介绍,因为真的很简单,详情点我参考官方教程学习使用即可。

PS:之前在优化性能时发现我们有一个应用有两个界面退出后Activity没有被回收(dumpsys meminfo发现一直在加),所以就怀疑可能存在内存泄露。但是问题来了,这两个Activity的逻辑十分复杂,代码也不是我写的,相关联的代码量也十分庞大,更加郁闷的是很难判断是哪个版本修改导致的,这时候只知道有泄露,却无法定位具体原因,使用MAT分析解决掉了一个可疑泄露后发现泄露又变成了概率性的。可以发现,对于这种概率性的泄露用MAT去主动抓取肯定是很耗时耗力的,所以决定直接引入leakcanary神器来检测项目,后来很快就彻底解决了项目中所有必现的、偶现的内存泄露。

总之一点,工具再强大也只是帮我们定位可能的泄露点,而最核心的GC ROOT泄露信息推导出泄露问题及如何解决还是需要你把住代码逻辑及泄露核心概念去推理解决。

 Android应用开发规避内存泄露建议

有了上面的原理及案例处理其实还不够,因为上面这些处理办法是补救的措施,我们正确的做法应该是在开发过程中就养成良好的习惯和敏锐的嗅觉才对,所以下面给出一些应用开发中常见的规避内存泄露建议:

Context使用不当造成内存泄露;不要对一个Activity Context保持长生命周期的引用(譬如上面概念部分给出的示例)。尽量在一切可以使用应用ApplicationContext代替Context的地方进行替换(原理我前面有一篇关于Context的文章有解释)。

非静态内部类的静态实例容易造成内存泄漏;即一个类中如果你不能够控制它其中内部类的生命周期(譬如Activity中的一些特殊Handler等),则尽量使用静态类和弱引用来处理(譬如ViewRoot的实现)。

警惕线程未终止造成的内存泄露;譬如在Activity中关联了一个生命周期超过Activity的Thread,在退出Activity时切记结束线程。一个典型的例子就是HandlerThread的run方法是一个死循环,它不会自己结束,线程的生命周期超过了Activity生命周期,我们必须手动在Activity的销毁方法中中调运thread.getLooper().quit();才不会泄露。

对象的注册与反注册没有成对出现造成的内存泄露;譬如注册广播接收器、注册观察者(典型的譬如数据库的监听)等。

创建与关闭没有成对出现造成的泄露;譬如Cursor资源必须手动关闭,WebView必须手动销毁,流等对象必须手动关闭等。

不要在执行频率很高的方法或者循环中创建对象,可以使用HashTable等创建一组对象容器从容器中取那些对象,而不用每次new与释放。

避免代码设计模式的错误造成内存泄露;譬如循环引用,A持有B,B持有C,C持有A,这样的设计谁都得不到释放。

关于规避内存泄露上面我只是列出了我在项目中经常遇见的一些情况而已,肯定不全面,欢迎拍砖!当然了,只有我们做到好的规避加上强有力的判断嗅觉泄露才能让我们的应用驾驭好自己的一亩三分地。

参考 https://www.2cto.com/kf/201510/445485.html


具体解决方法,举个栗子

1、context 在调用getInstance的时候传入application的context,或者在RequestImpl的构造函数里面调用context.getApplicationContext()

2、Handler 若handler实例的轮询器或者消息队列是在非主线程创建的,那这样创建handler实例是不存在什么问题,如果looper或者messagequeue创建在主线程,我们需要将handler声明为静态内部类,同时用WeakRefenrce与引用外部对象(弱引用)。因此有如下的handler解决方案

MyHandler mHandler = new MyHandler(this);  

static class MyHandler extends Handler {  

WeakReference<MainActivity> activityReference;  

MyHandler(MainActivity activity) {  

        activityReference = new WeakReference<>(activity);  

        }  

        @Override  

        public void handleMessage(Message msg) {  

        MainActivity activity = activityReference.get(); //判断是因为GC是对弱引用进行回收的  

        if (activity != null) {

activity.getTextView().setText("测试");  

}

}

3、BroadcastReceiver,ContentObserver 之类的没有解除注册啊;Cursor,Stream 之类的没有 close 啊;无限循环的动画在 Activity 退出前没有停止啊;一些其他的该 释放或者回收的没有被操作,比如自定义view的属性获取类 TypedArray需要调用recycle

总的来说,要避免Context相关的内存泄露,铭记以下几条:

•不要对Activity(Activity继承自Context)作长期的引用(一个指向Activity的引用与Activity本身有相同的生命周期);
•(如果使用长引用)试着用Application代替Activity;

•如果你不能控制内部类的生命周期,避免使用非静态内部类,应该用静态内部类,并且对里面的Activity作弱引用。该问题的解决方法是:对于外部类,用WeakReference构造静态内部类,同时要在视图根完成,并且它的WeakReference内部类要有一个实例(WeakReference)。

•垃圾回收不是防止内存泄露的保险方式

(有点混乱,不喜勿喷)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值