关于Android内存泄漏的一些理解

1 篇文章 0 订阅
1 篇文章 0 订阅

前言

内存泄漏可以说是android开发里老生常谈的问题,(最初只是为了面试,随着工作深入发现并没有那么简单)本文是个人在阅读一些博文后,写出一些自己的理解,也会针对可能不太了解的童鞋,一步一步刨析。同时也有疑惑的地方和不对的地方希望大佬们指点一下。

内存

在写这边博客之前,我已经抱着长篇大论的目的,希望自己能坚持下去,至少现在坚持。
java中jvm内存管理,把内存主要分为三块,栈,堆 和方法区。简单的理解来说:
栈 ->实际上只是存放引用的。
堆 ->用来存放实际new出来的对象。
方法区 ->包含一个常量池。用于存放常量。方法区还包括一块静态区域,用于存放static修饰的静态变量。
这是很不负责任的最简单的说法,实际上它的情况远不止如此。我只举一个简单的例子,一个方法中的临时变量,他的引用和值都是存放在栈内存中的,并不会放进堆内存中。

栈是什么?

把栈和队列理解清楚就行,栈是先进后出,理解成往桶子里放东西吧,最先放进去的最后才能拿出来。队列就是先进先出。和排队一样, 先来的就先办事。

  • ->android开发中对于activity的管理方式,就是任务栈。先进后出。最后点击的页面在返回时会先被销毁。
  • ->队列的例子也很多,最简单的就是messageQueue,就是一个队列,先拿到的消息,先执行。

个人理解:jvm内存中的栈所需的内存是很小的,因为它只存引用,举个简单例子,管理一群人,栈只管理名字,而活生生的人要放在堆里。栈只要一张纸就够了。堆要一个房间。
GC是java的垃圾回收机制,他只是回收堆中没有用的数据的,但是并不会回收栈中的,栈中的是系统自动分配释放的的。
上面说的临时变量是存放在栈中的,所以一般栈内存溢出的情况是频繁调用的方法里创建了临时变量。如:

   public static void test(){
        int sum = 0;
        while (true){
        int i = 1;
        sum+=i;
        Log.e("TAG", String.valueOf(sum));
    }

自己试着写了这样的代码,然后在单元测试跑了一下,
The IDE is running low on memory and this might affect performance. Please consider increasing available heap.
大概意思就是ide顶不住了,请考虑增加一下内存吧。
然而这里面,实际上这个 int i = 1; 很可惜,他不是在栈内存的。因为1是一个常量,放在常量池中。
如果在这里new了一个新的对象,是存在栈内存中的。
但如果是成员变量的话,就照常如:
Student mStudent; //在栈内存中创建一个类型Student的引用mStudent;
mStudent = new Student(); //mStudent 指向 堆中新建的new Student();

主要存对象的地方。
(堆是线程共享的,但是jvm会为每一个线程都创建一个栈,用来存放该线程执行方法的信息。)后期再说这个吧

内存泄漏

字面理解:内存泄漏出去了?真正含义:没有用的内存释放不掉。
eg:你写的activity,你已经在当前界面按下返回键了,这个activity已经出栈了。而且所有任务栈中都已经没有这个activity了,它已经执行了onDestory()方法,但是你使用profiler工具或者你自己的方式(发现自己写了个内存泄漏bug)
这个activity并没有销毁,游离在生命周期之外,谷歌爸爸也帮不了你了,
在这里插入图片描述
说明你成功实现了一次内存泄漏功能。这个activity其实你已经不想要,已经没有用了,但是它所占的内存并没有回收。
为什么呢?很简单,简单研究一下GC,为什么GC没有回收。因为内存里有些东西不能回收,还没工作完,但是这个东西又持有你这个activity的引用。怎么证明持有引用呢。很简单,可以调用成员方法(非静态的方法。),这肯定就是持有引用了,不持有怎么调用呢,。

Handler

先看一个经典的吧。
原理还有源码解析网上还是挺多的,一大堆。
为什么使用匿名内部类的handler会发生内存泄漏?
实际上销毁不了的是Message类型的msg;但是msg是怎么给handler去用的呢。
Looper类中loop方法有这样一行代码:
msg.target.dispatchMessage(msg);
而这个target正是handler;
喔,原来是这样。看到这个target,我的理解是,GC发现有一个msg清理不了,因为它还没有执行。被主线程的messageQueue持有了。
为什么没有执行,因为延迟了。handler的sendMessageAtTime方法中,会将msg给到当前线程的messageQueue,而messageQueue会在特定时间把msg装进自己的列队里了。怎么操作的这个就跟列队的数据结构有关了。然后被looper的loop方法循环执行到dispatchMessage方法,最终 msg.recycleUnchecked()方法会把msg持有的handler引用置空。这样就完成了一条消息的执行,完美,但如果msg还没到特定时间,就不会去执行,这样的引用链就是messageQueue ->msg->handler->activity;messageQueue是属于当前线程的,跟activity没啥关系,是不会销毁的,所以引用链不销毁就成了。所以最终导致activity无法销毁。

小结

所以。内存泄漏你要注意的就是有没有静态的或者是全局的其他方法会持有到你需要销毁的一些对象的引用。
在handler这个例子中,我认为关键的其实在于messageQueue。我们在主线程中使用handler是不需要调用Looper.prepare()方法的,因为这个方法已经在ActivityThread已经被调用了,源码中有这么一句:
Looper.prepareMainLooper();
点开这个方法;

  public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }

然后继续看prepare();

 private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

实际上就是将一个looper放进了一个sThreadLocal中去。ThreadLocal实际上就是一个变量,但是这个变量比较特殊,他对于每一个不同的Thread,都可以设置一个不同的值。而实际上prepareMainLooper()的myLooper()方法中才是真正的给到了一个新的messageQueue。

处理方法

 private static class MyHandler extends Handler {
        private WeakReference<HandlerOOMActivity> weakReference;
 
        public MyHandler(Activity activity) {
            weakReference = new WeakReference<>(activity);
            this.activity = activity;
        }
 
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            Log.e(TAG, "handleMessage: ");
        Activity activity = weakReference.get();
            switch (msg.what) {
                case 0:
                    if (activity != null) {
                        activity.tv.setText("...");
                   }
                    activity.tv.setText("...");
                    break;
            }
        }

1.使用静态内部类的方法。新建一个静态内部类去继承Handler。然后在new一个这个静态内部类的实例,静态内部类是不会被外部类的对象所持有的,而是被外部类所有实例共有,这样一来handler在内存中的并不会在堆内存中而是存在与方法区,不会持有activity 的引用。
2.如果需要使用activity实例呢?比如调用一个setText()方法,那么就可以使用弱引用weakRefenence;基本理解一下四大引用:
(优先级 强引用>软引用>弱引用>虚引用)意思就是同时存在多种引用,以优先级高的为准。

  • 强引用:除非OOM,否则是不会被回收的,用等于号创建的都是强引用。栈区的引用指向堆区的对象。
  • 软引用:当内存不足时可能被GC回收。
  • 弱引用:GC一旦发现了这个引用只剩弱引用,就会将其回收。
  • 虚引用:主要用来跟踪对象被垃圾回收的活动(对我们开发者来说,虚引用等于没有引用,是会被回收的)

所以使用弱引用引用到activity。这样一来如果只剩这个handler引用到activity 的话,activity也会被回收。这样一来就解决了内存泄漏的问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值