Android性能优化之内存泄漏及解决方案

内存泄漏的本质

申请了的内存在不再使用时无法回收。

Android应用程序内存泄漏的含义

Android系统为每个应用程序都分配了相应限额的内存。当应用程序中产生的内存泄漏较多时,将会导致应用程序运行所需要的内存超过系统为其分配的限额,这时应用程序就会Crash(崩溃)。

常见引发内存泄漏的情况

  • 集合类
  • static关键字修饰的成员变量
  • 非静态内部类 / 匿名类
  • 资源对象使用后未关闭

引发内存泄漏的具体情况及解决方案

(1)集合类

具体表现
集合List仍然引用该对象,所以垃圾回收器GC依然不可回收该对象

        // 通过循环申请Object对象和将申请的对象逐个放入到集合List
        List<Object> objectList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Object o = new Object();
            objectList.add(o);
            o = null;
        }
        // 虽释放了集合元素引用的本身:o=null)
        // 但集合List仍然引用该对象,故垃圾回收器GC依然不可回收该对象

解决方案
集合类添加集合元素对象后,在使用后必须从集合中删除。

        // 集合类添加集合元素对象后,在使用后必须从集合中删除
        // 释放objectList
        objectList.clear();
        objectList=null;

完整例子

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {


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

        // 集合引发的内存溢出
        // 通过循环申请Object对象和将申请的对象逐个放入到集合List
        List<Object> objectList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Object o = new Object();
            objectList.add(o);
            o = null;
        }
        // 虽释放了集合元素引用的本身:o=null)
        // 但集合List仍然引用该对象,故垃圾回收器GC依然不可回收该对象

        // 解决方案
        // 集合类添加集合元素对象后,在使用后必须从集合中删除
        // 释放objectList
        objectList.clear();
        objectList=null;

    }

}

(2)static关键字修饰的成员变量

具体表现:
被static关键字修饰的成员变量的生命周期等于应用程序的生命周期。如果static关键字修饰的成员变量引用耗费资源过多的实例(如Context),则容易出现该成员变量的生命周期大于引用实例生命周期的情况,当引用实例需结束生命周期销毁时,会因静态变量的持有而无法被回收,从而出现内存泄漏,如:

public class StaticReferenceClass {
    // 定义1个静态变量
    private static Context mContext;
    public StaticReferenceClass(Context context) {
        // 引用的是Activity的context
        mContext = context;
    }
    // 当Activity需销毁时,由于mContext的生命周期等于应用程序的生命周期,所以Activity无法被回收,从而出现内存泄露。
}

解决方案:
方法一:尽量避免static成员变量引用资源耗费过多的实例(如 Context),若需引用 Context,则尽量使用Application的Context。

// 方法一:尽量避免static成员变量引用资源耗费过多的实例(如 Context),若需引用 Context,则尽量使用Application的Context
StaticReferenceClass staticReferenceClass = new StaticReferenceClass(getApplication());

方法二: 使用弱引用(WeakReference)代替强引用持有实例

public class StaticReferenceClass {
   // 定义1个静态变量
   // 使用弱引用(WeakReference) 代替 强引用 持有实例
   private static WeakReference<Context> mContext;
   public StaticReferenceClass(Context context) {
       // 引用的是Activity的context
       mContext = new WeakReference<>(context);
   }
}

特殊例子——单例: 单例的生命周期等于应用程序的生命周期。
如果一个对象已不需再使用,而单例对象却还继续持有该对象的引用时,那么该对象将不能被正常回收,从而导致内存泄漏。如:

import android.content.Context;
public class MySingleClass {

// 创建单例时,需传入一个Context
// 若传入的是Activity的Context,此时单例则持有该Activity的引用
// 由于单例一直持有该Activity的引用(直到整个应用生命周期结束),即使该Activity退出,该Activity的内存也不会被回收
// 特别是一些庞大的Activity,此处非常容易导致OOM
    private static MySingleClass ourInstance;
    private Context mContext;

    public static MySingleClass getInstance(Context context) {
        if (ourInstance == null) {
            ourInstance = new MySingleClass(context);
        }
        return ourInstance;
    }
    private MySingleClass(Context context) {
        this.mContext = context; // 传递的是Activity的context
    }
}

单例的解决办法:
因为单例模式引用的对象的生命周期等于应用的生命周期。所以应传递Application的Context,因Application的生命周期等于整个应用的生命周期,解决后的代码如下:

import android.content.Context;
public class MySingleClass {

// 创建单例时,需传入一个Context
// 若传入的是Activity的Context,此时单例则持有该Activity的引用
// 由于单例一直持有该Activity的引用(直到整个应用生命周期结束),即使该Activity退出,该Activity的内存也不会被回收
// 特别是一些庞大的Activity,此处非常容易导致OOM
    private static MySingleClass ourInstance;
    private Context mContext;

    public static MySingleClass getInstance(Context context) {
        if (ourInstance == null) {
            ourInstance = new MySingleClass(context);
        }
        return ourInstance;
    }
    private MySingleClass(Context context) {
        this.mContext = context.getApplicationContext(); // 传递的是Activity的context
    }
}

(3)非静态内部类 / 匿名类

非静态内部类 / 匿名类默认持有外部类的引用;而静态内部类则不会。有三类情况:

  • 非静态内部类的实例是静态引起的
  • 多线程以非静态内部类或匿名类使用引起的
  • 资源没有主动释放或注销引起的

1.当非静态内部类的实例是静态时
如果非静态内部类所创建的实例是静态的,那么它的生命周期等于应用的生命周期,又因为非静态内部类默认持有外部类的引用,所以会导致外部类无法释放,最终造成内存泄漏,如:
具体表现:

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {

    // 非静态内部类的实例的引用
    // 注:设置为静态实例
    public static InnerClass innerClass = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 保证非静态内部类的实例只有1个
        if (innerClass == null) {
            innerClass = new InnerClass();
        }
    }
    // 非静态内部类的定义
    private class InnerClass {
        //...
    }

}
// 造成内存泄露的原因:
// a. 当MainActivity销毁时,因非静态内部类单例的引用(innerClass)的生命周期等于应用App的生命周期,持有外部类MainActivity的引用
// b. 所以MainActivity无法被GC回收,从而导致内存泄漏

解决方案:

  • 1.将非静态内部类设置为:静态内部类(静态内部类默认不持有外部类的引用)
public class MainActivity extends AppCompatActivity {
    // 非静态内部类的实例的引用
    // 注:设置为静态
    public static InnerClass innerClass = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 保证非静态内部类的实例只有1个
        if (innerClass == null) {
            innerClass = new InnerClass();
        }
    }
    // 非静态内部类的定义
    private static class InnerClass {
        //...
    }
}
  • 2.将该内部类抽取出来封装成一个单例
public class InnerClass {
    private static final InnerClass ourInstance = new InnerClass();

    public static InnerClass getInstance() {
        return ourInstance;
    }
    private InnerClass() {}
}
  • 3.尽量避免非静态内部类所创建的实例是静态的。若需使用Context,建议使用Application的 Context。

import android.content.Context;
public class InnerClass {
    private static InnerClass ourInstance = null;
    private Context mContext;
    
    public static InnerClass getInstance() {
        return ourInstance;
    }

    public static InnerClass getInstance(Context context) {
        if (ourInstance == null) {
            ourInstance = new InnerClass(context);
        }
        return ourInstance;
    }
    private InnerClass(Context context) {
        this.mContext = context.getApplicationContext(); // 传递的是Activity的context
    }
}

2.多线程:AsyncTask、实现Runnable接口、继承Thread类
当AsyncTask、实现Runnable接口、 继承Thread类等多线程以非静态内部类 / 匿名类(即线程类属于非静态内部类/匿名类)方式来使用时,当工作线程正在处理任务,而外部类需销毁时,由于工作线程实例持有外部类引用,将使得外部类无法被垃圾回收器(GC)回收,从而造成内存泄漏。以Thread类来举例,其他依次类推。
具体表现:

// 非静态内部类方式
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 通过创建的内部类 实现多线程
        new InnerClassThread().start();
    }
    // 自定义的Thread子类
    private class InnerClassThread extends Thread{
        @Override
        public void run() {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
// 匿名内部类方式
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 通过匿名内部类 实现多线程
        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

}

分析
工作线程Thread类属于非静态内部类 / 匿名内部类,运行时默认持有外部类的引用,当工作线程运行时,如果外部类MainActivity需销毁时,将会因为此时工作线程类实例持有外部类的引用,而使得外部类无法被垃圾回收器(GC)回收,从而造成内存泄漏。
解决方案:
a.使用静态内部类:静态内部类默认不持有外部类的引用,将Thread的子类设置成静态内部类,如:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 通过创建的内部类,实现多线程
        new InnerClassThread().start();
    }

    // 自定义的Thread子类
    private static class InnerClassThread extends Thread{
        @Override
        public void run() {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

b.当外部类结束生命周期时,强制结束线程
使得工作线程实例的生命周期与外部类的生命周期同步,具体做法是当外部类结束生命周期时,强制结束线程。但是Thread.stop这个方法官方已标识为弃用状态,所以比较好的办法就是让线程自己停止自己。有两种处理方法:1、让线程任务执行完成,顺利结束退出。2、设置终止标志位,在循环的时候进行终止标志位检测,如果设置为终止状态则return结束线程,如:


public class MainActivity extends AppCompatActivity {

    private boolean isStop = false;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 执行多线程
        new Thread(new Runnable() {
            @Override
            public void run() {

                while(true){
                    if(isStop){//当需要结束线程的时候终止线程
                        //doSomething  进行一些收尾工作
                        return;
                    }
                    System.out.println(Thread.currentThread().getName());
                }
            }
        }).start();

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        isStop = true;
    }
}

这个标识位的放置的位置是很值得考虑的,你应该将它放到一个可以保证原子操作的地方。

(4)消息传递机制:Handler引发的内存溢出
请参考《Android性能优化之Handler内存溢出》
(5)资源对象使用后未关闭
对于资源如广播BraodcastReceiver、文件流File、数据库游标Cursor、图片资源Bitmap等的使用,如果在Activity销毁时,没有及时关闭或注销它们,那么这些资源将不会被回收,从而造成内存泄漏。
解决方案
在Activity销毁时 及时关闭或注销这些资源,如:

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 对于广播BraodcastReceiver:注销注册
        unregisterReceiver()
        // 对于文件流File:关闭流
        InputStream / OutputStream.close()
        // 对于数据库游标cursor:使用后关闭游标
        cursor.close()
        // 对于图片资源Bitmap:Android分配给图片的内存只有8M,如果1个Bitmap对象占内存较多,当它不再被使用时,应调用recycle()回收此对象的像素所占用的内存;最后再赋为null 
        Bitmap.recycle();
        Bitmap = null;

       // 对于动画(属性动画)
       // 将动画设置成无限循环播放repeatCount = “infinite”后
       // 在Activity退出时记得停止动画

    }

其他导致内存溢出的情况

  • 不再使用的WebView对象,如果不销毁,将一直占用着内存,无法回收,将导致内存溢出。
    解决方法:在不使用时,要主动销毁WebView对象。

谢谢阅读。

你可能感兴趣的:(Android开发)