欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android 中 Activity的内存泄漏,原因以及处理方法

程序员文章站 2022-04-19 17:36:19
...

文章参考: 
八个造成 Android 应用内存泄露的原因 
Android内存泄漏产生的原因以及解决方案OOM 
android 常见内存泄漏原因及解决办法 
内存泄漏,说的更直白点,就是你想让一个对象在下次GC的时候彻底被回收,但是呢,这个对象所处的条件不符合GC所认定的应当回收的条件,而导致实际上没有被回收依然占用着内存空间,像这样的对象多了,,迟早会把内存撑爆引发大名鼎鼎的OOM问题。Android中最最露骨的就是Activity的内存泄漏。因为Activity持有了太多的东西,包括图片,数据等等吧。。它的泄露势必会浪费很大的内存空间。

那么什么是GC所认定的回收条件呢? 就是指向该对象的相关引用链断了,致使这个对象相关的资源 成为了一个孤岛样子的状态,及封闭了,与外界无联系了。这样一种状态,就是可以被回收的状态了。所以要你搞清内存泄漏,你首先要搞清的问题就是内存相关的那波。包括怎么存,以及看代码要弄明白各种情况下,到底是谁持有谁的引用。谁的地址,还被有效的指针指着,导致该删的删不掉。搞清楚这些,才能正确的断掉引用链。

下面会讲到几种引发泄漏的几种情况以及泄漏的原因,并且分析引用关系。

Handler引发的内存泄漏
android handler机制 
如果目前对handler一无所知的话,建议看一下连接中的博客。 
Handler在实际的android项目中经常用于改变UI相关的界面变化。尤其是在子线程中的时候往往会用一个handler send一个message这样的方式改变相关的界面。但是如果大家了解Handler的机制的话,好好分析,其实会发现如果handler使用不当的话,是很可能引发内存泄漏的。 
首先Handler在整个机制内的引用关系会是这样的,Handler会被它所发送的Message引用,(Message里面有个target的变量就是Handler),然后发送完消息之后Message会按照时间顺序加入到一个叫messagequeque的队列里面去。这个messageQueue是Looper的成员变量。所涉及的引用关系如下: 
每一个Thread只要调用了Looper的prepare方法,就会出现下面的现象。thread会持有一个values对象,values对象内部的数据结构存储了(key, map) 诸如(sThreadLocal, 当前线程对应的looper)这样的数据结构, 其中looper里面便有messagerqueue的引用。这也就导致了messagequeue的存在周期是相当长的(线程有多长它就有多长,并且此时的线程多为UI线程,绝对长的周期),因为messagequeue在looper里,然而looper被Thread中Value以键值对的形式存储起来了。也就意味着thread不死,values就一直被引用,那么looper中的messagequeue也就会一直存在。在android里面,如果不特殊写的话,唯有我们的主线程,间接持有messagequeque的。 
所以这样的话,handler的整条引用关系也就可以理出来, handler 被message.target持有, message.target又在message对象里面存着,message被messagequeque持有,但是重点是messagequeue的周期相当长。。这样的话貌似也与Activity没有啥关系哈。。但是handler如果使用不当,恰好持有了Activity的引用呢???那就出问题了! 
放一段恰好有这类问题的代码:

package com.example.forev.roundrectviewp;

import android.app.Activity;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends Activity {

private TextView mPostMessageTv;
private TextView mOpenActivityBTv;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    setViews();
}

private void setViews() {
    mPostMessageTv = findViewById(R.id.tv_get);
    mOpenActivityBTv = findViewById(R.id.tv_post);

    mPostMessageTv.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Message message = new Message();
            message.what = MyHandler.WHAT_CHANGEBACKGROUD;
            //此时new出来的对象已经默认持有了 MainActivity的引用!执行了此句立马点击跳转B的话,会引发泄漏
            new MyHandler().sendMessageDelayed(message, 30 * 1000);
        }
    });

    mOpenActivityBTv.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent(MainActivity.this, BActivity.class);
            startActivity(intent);
        }
    });
}

//内部类,在对象被new的那一刻,就默认已经持有了外部类的引用。
private class MyHandler extends Handler{

    public static final int WHAT_CHANGEBACKGROUD = 0x00;
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case WHAT_CHANGEBACKGROUD:
                mPostMessageTv.setBackgroundColor(Color.parseColor("#FFFF00"));
                break;
        }
    }
}

}


从上面代码,Handeler作为内部类的时候,会持有外部类的强引用(很多情况下,内部类会持有外部类的引用,接下来会讲这个),假设Activity点击了mPostMessageTv按钮之后又点击了跳转B的按钮,如果内存吃紧的话,要GC,但是,因为Handler的Message还在队列里排队没有执行到,所以Activity就这样被强引用链牵着,从而被GC判定为不可回收。就导致出现了OOM。

好,接下来简单说下内部类:内部类可分为 成员内部类,局部内部类, 匿名内部类, 静态内部类。

成员内部类相当于一个外部类的成员变量,可以访问外部类的所有成员变量和方法。上面的代码,MyHandler就是MainActivity的成员内部类。所以在改变mPostMessageTv背景的时候,我们是直接用的mPostMessageTv.setBackgroundColor(Color.parseColor("#FFFF00"));。但是成员内部类是会持有外部类引用的。

局部内部类:定义在方法里,比方法的范围还要小,是内部类中最少用的一种类型。与此同时,因为在方法体里面,所以它会像局部变量一样不可以被public , protected, private, static修饰。并且它只能够访问final类型的局部变量这样的话,局部内部类也就只能在其所属的方法中被new,并且调用方法。那么局部内部类会不会持有外部类的引用呢?好像是不会持有。介绍的文章太少,走个例子吧,把上面的代码改一下

    private void setViews() {
    mPostMessageTv = findViewById(R.id.tv_get);
    mOpenActivityBTv = findViewById(R.id.tv_post);
    mPostMessageTv.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Message message = new Message();
            //将handler写成局部内部类
            class MyHandler extends Handler{

                public static final int WHAT_CHANGEBACKGROUD = 0x00;
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case WHAT_CHANGEBACKGROUD:
                            mPostMessageTv.setBackgroundColor(Color.parseColor("#FFFF00"));
                            break;
                    }
                }
            }
            message.what = MyHandler.WHAT_CHANGEBACKGROUD;
            MyHandler myHandler = new MyHandler();

            myHandler.sendMessageDelayed(message, 30 * 1000);
        }
    });

    mOpenActivityBTv.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent(MainActivity.this, BActivity.class);
            startActivity(intent);
        }
    });
}


看下debug的图片: 
 
从这里可以看出来是持有外部类引用的。

匿名内部类 
对于匿名内部类,可以说在界面编程里面,几乎每每都要用到这个。但是他的样子也是怪怪的。下面贴一段代码看看这个匿名内部类

/**
 * 匿名内部类案例
 * @author forev
 *
 */
public class UnnameInnerClass {
public static void main(String[] args) {
    UnnameInnerClass unnameInnerClass = new UnnameInnerClass();
    unnameInnerClass.printBirdInfo(new Bird(){
        String getBirdName() {
            // TODO Auto-generated method stub
            return "大黄鸭";
        }

        String getBirdAge() {
            // TODO Auto-generated method stub
            return "1";
        }
    });
}

//也就是Bird是一个抽象类,但是尽管它没有被实现,依然可以调用它的方法。
public void printBirdInfo(Bird bird){
    System.out.println("the bird name is :" + bird.getBirdName() + " and the bird age is :" + bird.getBirdAge());
}

}



bird代码: 

abstract class Bird { 
abstract String getBirdName(); 
abstract String getBirdAge(); 
} 


使用匿名内部类的话,必须要做的是实现父类或者接口,当然也只能继承一个父类或者实现一个接口,匿名内部类直接用一个new来生成对象的引用,当然这个引用是隐式的。 
看到代码是不是感觉似曾相识?咱们view.setonclickListner()是不是里面常常现new一个OnclickListner然后实现里面的onclick方法呢?是的,这种用法就是典型的匿名内部类。 
匿名内部类是能new出来的,所以它不能是一个抽象类或者接口,所以在new的时候必须要实现这些抽象方法,以确保该类方法里面逻辑的完整性。同时呢,匿名内部类在哪里被new,就会持有所在类对象的引用。 
这样,有关于内部类,单独写一篇博吧。这个知识点要讲的并不少。总之现在记住,匿名内部类new出来的那个对象是会持有其外部类引用。 
静态内部类与静态匿名内部类 
记住一个,static修饰的东西不在类的管辖范围之内。所以一旦被static修饰之后它不属于任何对象。所以这两种情况下是不会持有外部类引用的。

讲到这里,关于handler使用不当引发的内存泄漏自然也有了对应的解决方法, 1 要么把Myhandler搞成静态内部类。 2 或者另外立一个文件专门写一个Myhandler类。 这两种一个可以避免内部类持有外部类,另一个更粗暴地不让它当内部类。 
当然写使用Handler的时候,尤其是发送一个延时消息的时候,要做就做彻底些,除了避免Handler持有Activity引发的内存泄露之外,在Activity结束的时候最好要remove一下发出去的那个message。要养成一个良好的习惯。但是假设有的情况,Handler必须要一个Activity的对象来进行相关UI处理的话,又怎么办呢?此时我们不得不把Activity传进去。那么针对这种情况哈,不要忘记有一个叫弱引用的存在,,可以把activity搞成一个弱引用。这样尽可能的避免内存泄漏。弱引用也可以在其他的情况下很好的解决内存泄漏等问题。但是要做好的是相关处理, 假设get()为null,总得有个相关的处理吧。

单例模式引发的内存泄漏
首先了解一下单例模式是个啥样子。public class SingletonDemo1 {
//这是一个很靠谱的单例模式,既避免了多线程访问引发的数据篡改问题,又可以相对较快地
//得到这个单例
//这个静态内部类并不希望被其他类访问到,所以用private修饰
//并且static修饰的东西,会在第一次被使用的时候
//静态内部类的加载不需要依附外部类,在使用时才加载。

private static class InstanceHolder{
    private final static SingletonDemo1 singleton = new SingletonDemo1();
}

public SingletonDemo1 getInstatnce(){
    return InstanceHolder.singleton;
}

}



看到单例模式的样子,是不是感觉到一旦getInstance()之后,这个InstanceHolder.singleton的生命周期会相当长。对的,单例的周期是和app的生命周期一致。并且整个app内只有一个这样的实例。但是由于其超长的生命周期,一旦它持有比较大的对象引用,并且这个对象的作用发挥完了还没有释放它的话,是极易引发内存泄漏的。尤其是这个对象如果是Activity的话,以Activity所持有的那些资源来看的话,泄露的空间可是相当可观的。再看一段代码:

public class SingleTonLeackDemo {
private Context mContext;   
private static class InstanceHolder{
    private final static SingleTonLeackDemo INSTANCE = new SingleTonLeackDemo();
}

void SingleTonLeackDemo(Context context){
    this.mContext = context;
}

private SingleTonLeackDemo setContext(Context context) {
    this.mContext = context;
    return this;
}

//假设是单线程调用
public static SingleTonLeackDemo getInstance(Context context) {
    return InstanceHolder.INSTANCE.setContext(context);
}
}


假设上一段代码里面的context,如果传入了一个Activity对象,比如来了个SingleTonLeackDemo.getInstance(Activity.this)这样的调用,就会导致内存泄漏。因为Activity传进去的时候,就会被赋给SingleTonLeackDemo对象的成员变量mContext, 但是事实上SingleTonLeackDemo对象的生命周期却和app的生命周期一样长,这样不对等的生命周期,这样赤裸裸的引用关系,内存泄露也就在所难免了。但是这种情况也是可以避免的,解决方法大概有两种。 
1 确认SingleTonLeackDemo 那里面的mContext 是干嘛用的,有没有必要非得把Activity.this传进去。因为毕竟我们还有一个叫做applicationContext的东西也是Context的类型的,也可以做获取资源,获取服务等工作。没必要非得传Activity。一旦确认这里其实可以被applicationContext代替的话,那就改代码,Context参数该去的去掉,直接给mContext赋值为 this.mContext = MyApplication.getAppInstance().getApplicationContext(),总之就是想办法阻断指activity实例的那个引用。 
2 如果此处要传递的非得是Activity的话,那好吧,最好把mContext改成弱引用!

非静态内部类的静态实例引发的内存泄漏。
这个就很好理解了。看看如下代码:

/**
 * create by yayali
 * 本activity主要展现一种非静态内部类的静态实例引发内存泄漏的问题。
 */
public class InnerClassLeackAct extends AppCompatActivity {
private static InnerClass sInnerClass;  
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_inner_class_leack);
    setData();
}

private void setData() {
    if (sInnerClass == null) {
        sInnerClass = new InnerClass();
    }
}

private class InnerClass{

}
}



上面的代码里面,首先InnerClass作为一个内部类,被new 的时候本身就隐式持有了其外部类Activity的引用,但是偏偏Activity又有了个静态变量,后来又指向了innerclass的实例,静态变量的声明周期可是很长很长滴,activity就这样也没做啥却被隐式引用了。流氓不撒手锅从天上来。 
这时候你可能会想,我才不会写出这么弱智的代码呢。但是呢,咱们编码的时候是需要面临一些情景的,有时候某些情景是真的需要我们用static的对象来记录一些东西,以便于下次再调用到该类的时候能够根据这里面的数据做一些逻辑。那么对于这种泄露如果解决的话,就想办法使这个innerClass 不隐式引用Activity,这样即使innerclass被静态变量引用了,那起码也跟Activity没太大的关系。这样的话,要么把InnerClass搞成静态内部类,要么就另起个文件写这个类。

非静态匿名内部类引发的泄露
这个讲起来其实是和 第一种情况既handler使用不当引发的内存泄漏一模一样。包括那个部分的案例代码都是妥妥的非静态匿名内部类(MyHandler)引发的内存泄漏。 
再给个栗子:

/**
 * create by yayali
 * 非静态匿名内部类引发的内存泄漏
 */
public class InnerClassLeackAct extends AppCompatActivity { 
/**
 * 此时Runnable便是一个非静态匿名内部类。
 * 那么也意味着 mRun此时隐式持有当前对象的引用
 * 我们必须实现它的方法以便于它可以new出来。
 */
private Runnable mRun = new Runnable() {
    @Override
    public void run() {
        try {
            Thread.sleep(300 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
};

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_inner_class_leack);
    Thread thread = new Thread(mRun);
    thread.start();
}

}



那么此时的做法是什么呢?改为静态匿名内部类不就得了。这样就避免隐式持有Activity了。

其他的内存泄漏 摘抄的(因为工作主要做业务编程了至今我没有太遇到过)
注册、反注册未成对使用引发的内存泄漏
在android开发中,我们经常会在Activity的onCreate()中注册广播接收器,EventBus等,如果忘记成对的使用反注册,就可能会引发内存泄漏。开发过程中应当养成良好的习惯,在onCreate()onResume()中注册,要记得相应的在onDestroy()onPause中反注册。

资源对象没有关闭引发的内存泄漏
在android中,资源性对象比如Cursor,File,BitMap,视频等,系统都用了一些缓冲技术,在使用这些资源的时候,如果我们确保自己不再使用这些资源了,应当及时关闭,苟泽可能引起内存泄漏,因为有些操作不仅仅是涉及到dalvik虚拟机,还涉及到底层c/c++等内存管理,不能完全寄托于虚拟机帮我们完成内存管理。 
在这些资源不使用的时候,记得要调用类似close(), destroy(),recycler(),release()等函数,这些函数往往会通过jni调用底层相关函数,完成相关的内存释放。

集合对象没有及时清理引发的内存泄漏
我们通常会把一些对象装入到集合中,当不使用的时候一定要记得及时清理集合,让相关对象不再被引用。如果集合是static、不断的往里面添加东西、又忘记去清理,肯定会引起内存泄漏。
--------------------- 
作者:娅娅梨 
来源:CSDN 
原文:https://blog.csdn.net/weixin_28774815/article/details/80903221 
版权声明:本文为博主原创文章,转载请附上博文链接!