常见内存泄漏场景以及处理方式

  1. 内存泄露的分类
  • 一般内存泄漏(traditional memory leak)的原因是:由忘记释放分配的内存导致的。
  • 逻辑内存泄漏(logical memory leak)的原因是:当应用不再需要这个对象,当仍未释放该对象的所有引用。

如果对象的有用存在期没有被明确定义的话,探查逻辑内存泄漏将会变成一件很主观的事情。幸好,Activity 明确定义了 生命周期,使得我们可以简单地知道一个 Activity 对象是否被泄漏了。在 Activity 的生命末期,onDestroy() 方法被调用来销毁 Activity ,这样做的原因可能是程序本身的意愿或者是 Android 需要回收一些内存。如果这个方法完成了,但是 Activity 的实例被堆根的一个强引用链持有着,那么 GC 就无法标记它为可回收 —— 尽管原本是想删掉它。因此,我们可以将一个泄露的 Activity 对象定义为一个超过其自然生命周期的对象。

Activity 是非常重的对象,所以你从来就不应该选择无视 Android 框架对它们的处理。然而,Activity 实例也有一些泄漏是非意愿造成的。在 Android 中,所有的可能导致内存泄漏的陷阱都围绕着两个基本场景:第一个是由独立于应用状态存在的全局静态对象对 Activity 的链式引用造成的;另一个是由独立于 Activity 生命周期的一个线程持有 Activity 的引用链造成。下面我们来解释一些你可能遇到这些场景的方式。

这里就需要了解 java 的一个知识点,这就是引用类型。java 分 四 种引用类型,分别是:强引用,软引用,弱引用,虚引用。这从这四种引用点名称可以推测,系统对于不用类型引用有不同的回收机制。为方便对比列出下方的表格
在这里插入图片描述
合理点使用不同的引用类型,可以避免出现内存泄露的情况

在Android开发中,出现内存泄露我认为可以分为两大部分,一部分是 java 相关的,另一部分是 Android Api使用容易出现内存泄露

  1. 泄露场景
    1). 静态 Activity
    泄漏一个 Activity 最简单的方法是:定义 Activity 时在内部定义一个静态变量,并将其值设置为处于运行状态的 Activity 。如果在 Activity 生命周期结束时没有清除引用的话,这个 Activity 就会泄漏。这是因为这个对象表示这个 Activity 类(比如:MainActivity )是静态的并且在内存中一直保持加载状态。如果这个类对象持有了对 Activity 实例的引用,就不会被选中进行 GC 了。
void setStaticActivity() {
  activity = this;
}
 
View saButton = findViewById(R.id.sa_button);
saButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
    setStaticActivity();
    nextActivity();
  }
});


在这里插入图片描述
2).单例模式
首先了解一下单例模式是个啥样子。

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的生命周期一样长,这样不对等的生命周期,这样赤裸裸的引用关系,内存泄露也就在所难免了。

但是这种情况也是可以避免的,解决方法大概有两种。 
单例模式生成的静态对象因为生命周期和应用的进程一致,一般只有当应用退出或者运行的进程被结束,对象才能结束对象的生命周。例如当一个单例对象引用一个 activity 变量,即便 activity 可能已经退出了,但是因为单例对象还持有 activity ,所以系统不能回收这个 activity 造成了内存泄露

1 确认SingleTonLeackDemo 那里面的mContext 是干嘛用的,有没有必要非得把Activity.this传进去。因为毕竟我们还有一个叫做applicationContext的东西也是Context的类型的,也可以做获取资源,获取服务等工作。没必要非得传Activity。一旦确认这里其实可以被applicationContext代替的话,那就改代码,Context参数该去的去掉,直接给mContext赋值为 this.mContext = MyApplication.getAppInstance().getApplicationContext(),总之就是想办法阻断指activity实例的那个引用。
2 如果此处要传递的非得是Activity的话,那好吧,最好把mContext改成弱引用!

使用弱应用,例如
private WeakReference<Activity> wr = null;
wr = new WeakReference<Activity>(myActivity);

3). 静态 View
某静态View持有Activity中的TextView的静态引用,而TextView又持有Activity的引用,从而导致Actity在onDestory之后不能释放。

我们有一个特定的 View :花费极大的代价来初始化,但是在同一个 Activity 的不同生命时间内没怎么变化过,我们该怎么办呢?我们可以简单地在初始化后就把这个 View 设为静态的,然后附加到 View 的层次关系中,就像我们在这里做的。现在假如 Activity 被销毁了,我们应该可以释放它占用的大部分内存。

void setStaticView() {
  view = findViewById(R.id.sv_button);
}
 
View svButton = findViewById(R.id.sv_button);
svButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
    setStaticView();
    nextActivity();
  }
});

在这里插入图片描述
稍等,有一点奇怪的地方。正如你知道的,在这种情况下,我们的 Activity 中,一个被附加的 View 会持有对它的 Context 的引用。通过使用一个 View 的静态引用,我们给 Activity 设定了一个持久化的引用链并且泄露了它。不要使附加的 View 静态化,如果你必须这么做的话,至少让它们在 Activity 完成之前从 View 层级关系的同一点上分离出来。
4).非静态内部类的静态实例引发的内存泄漏

/**
 * 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搞成静态内部类,要么就另起个文件写这个类。
因为内部类的一个特性是它们可以访问外部类的变量,所以它们必然持有了对外部类实例的引用以至于 Activity 会发生泄漏。

5) .非静态匿名内部类引发的泄露
同样的,匿名类同样持有了内部定义的类的引用。因此如果你在 Activity 中匿名地声明并且实例化了一个 AsyncTask的话就会发生泄漏。如果在 Activity 销毁后它仍在后台工作的话,对于 Activity 的引用会持续并且直到后台工作完成才会进行 GC。

void startAsyncTask() {
    new AsyncTask<void, void,="" void="">() {
        @Override protected Void doInBackground(Void... params) {
            while(true);
        }
    }.execute();
}
 
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View aicButton = findViewById(R.id.at_button);
aicButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        startAsyncTask();
        nextActivity();
    }
});

在这里插入图片描述
改为静态匿名内部类不就得了。这样就避免隐式持有Activity了。
6). 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。

解决方案 
1.把 Handler 实现做一个独立的类或者使用静态修饰
2.在 Handler 内部调用到 acivity 等外部变量是可以用弱引用修饰作为 activity 引用类型
3.Activity在onDestory时,应移除mHandler中未完成的任务

7). 监听器的没及时注销(Sensor Manager)
Android 系统提供了一些服务,要获取这些服务的对象都要使用到 Context 这个变量
当一个 activity 关闭后,这些系统级别的服务都会继续持有 Context 引用

解决方案 
1.在退出 Activity 后及时注销监听器
2.使用 ApplicationContext 代替 Context

8).匿名内部类使用(Threads , TimerTasks)
非静态内部类,匿名类阅读,可以轻易访问外围类的变量,也就是说匿名内部类可以持有外部类的的变量,当外部类的变量可以被回收的时候,当时因为内部类持有外部类的引用,这样就造成了内存泄露。但是静态内部类就不会引用外部变量

解决方案 
1.使用静态内部类
2.用弱引用就是引用到的变量
3.可以的话,在结束外部类时及时关闭匿名内部类(Thread)

9).资源对象没有关闭引发的内存泄漏
在android中,资源性对象比如Cursor,File,BitMap,视频等,系统都用了一些缓冲技术,在使用这些资源的时候,如果我们确保自己不再使用这些资源了,应当及时关闭,苟泽可能引起内存泄漏,因为有些操作不仅仅是涉及到dalvik虚拟机,还涉及到底层c/c++等内存管理,不能完全寄托于虚拟机帮我们完成内存管理。
在这些资源不使用的时候,记得要调用类似close(), destroy(),recycler(),release()等函数,这些函数往往会通过jni调用底层相关函数,完成相关的内存释放。
10).集合对象没有及时清理引发的内存泄漏
我们通常会把一些对象装入到集合中,当不使用的时候一定要记得及时清理集合,让相关对象不再被引用。如果集合是static、不断的往里面添加东西、又忘记去清理,肯定会引起内存泄漏。
11).注册、反注册未成对使用引发的内存泄漏
在android开发中,我们经常会在Activity的onCreate()中注册广播接收器,EventBus等,如果忘记成对的使用反注册,就可能会引发内存泄漏。开发过程中应当养成良好的习惯,在onCreate()onResume()中注册,要记得相应的在onDestroy()onPause中反注册。
12).context 的使用
Context 是 Android 开发中经常传递的变量,但是某些情况下本该被回收的 Context 却因为某些对象依然持有 Context 的引用,进而发生内存泄露

解决方案 
1.使用 ApplicationContext 代替 Context ,因为 ApplicationContext 的生命周期和应用(进程)一样长。
2.对于 Context 变量慎用 Static 修饰,

13).Cursor Bitmap Stream 没及时释放
打开资源文件,会把文件缓存在内存和 jvm 虚拟机中,在使用结束后如果没有 close() 则会发生内存泄露。

解决方案 
1.使用结束后及时调用 close()

14). listView 的 adpater 没使用 ConvertView 缓存
ListView 会缓存一部分 View ,没用使用 getview() 里面的参数 ConvertView
15).静态集合对象没有清理
静态集合里数据多,生命周期与应用一样长,但是依然占据了一部分内存,不需要时没有进行清理的动作

解决方案 
1.退出后 clear 掉集合里面的所有数据,然后赋值 null

16). webview 没有及时释放
webview 使用完毕后一样需要 destroy 掉,否者一直常驻内存

解决方案 
因为 webView 耗费大量内存,可以为 WebView 分配一个独立的线程,也主线程做通讯。不需要时也要记得销毁掉。

17).监听器
使用监听者模式,我们会添加一些监听器,但是移除被监听对象时,往往忘记取消设置的监听器
参考:
https://blog.csdn.net/north1989/article/details/51999920
https://www.jianshu.com/p/06dd32020d4e

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值