安卓内存分析(1)——常见内存泄漏场景

时间:2023-02-09 11:21:26

安卓内存分析(1)——常见内存泄漏场景

问题背景

安卓日常开发和学习过程中,内存泄漏是一个重要的话题,并且内存泄漏相关的问题会经常发生在我们身边。那么,首先我们来看看内存泄漏的一些常见场景有哪些?

问题分析

1、单例导致的内存泄漏

单例模式在我们项目中经常会用到,比如,我们项目中使用ActivityManager单例,获取这个单例需要传入context对象。代码如下:

public class ActivityManager {
private static final String TAG = "ActivityManager";
 
    private Context context;
 
    private static ActivityManager instance;
 
    private ActivityManager(Context context) {
        this.context = context;
    }
 
    public static ActivityManager getInstance(Context context) {
        if (instance == null) {
            instance = new ActivityManager(context);
        }
        return instance;
    }
}

上面代码,在构造方法里传入context,如果不加注意,很容易因为生命周期的长短而引起内存泄漏。注意请在这里传入Application的context而不是Activity的context,因为如果传入的是Activity的context,当Activity退出时,此context并不会被回收,因为单例对象持有该context的引用,从而引起内存泄漏。如果传入的是Application的context,便不会引发内存泄漏,因为context的生命周期和Application一样长,所以正确的单例写法如下:

public class ActivityManager {
private static final String TAG = "ActivityManager";
 
    private Context context;
 
    private static ActivityManager instance;
 
    private ActivityManager(Context context) {
        this.context = context.getApplicationContext();
    }
 
    public static ActivityManager getInstance(Context context) {
        if (instance == null) {
            instance = new ActivityManager(context);
        }
        return instance;
    }
}

2、静态变量导致的内存泄漏

静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放。 比如下面这样的情况:

public class MainActivity extends AppCompatActivity {

    public static Util util;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        util = new Util(this);
    }
}

    class Util {
        private Context mContext;
        public Util(Context context) {
            this.mContext = context;
        }
    }

代码分析:util作为 Activity 的静态成员,并且持有 Activity 的引用,但是 util 作为静态变量,生命周期肯定比 Activity 长。所以当 Activity 退出后, util 仍然引用了 Activity , Activity 不能被回收,这就导致了内存泄露。 在 Android 开发中,静态持有很多时候都有可能因为其使用的生命周期不一致而导致内存泄露,我们尽量少地使用静态持有的变量,以避免发生内存泄露。当然,我们也可以在适当的时候将静态变量置为 null,使其不再持有引用,也可以避免内存泄露。

3、非静态内部类导致的内存泄漏

非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。非静态内部类导致的内存泄露在Android开发中有一种典型的场景就是使用Handler,很多开发者在使用Handler是这样写的,代码如下:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        start();
    }

    private void start() {
        Message message = Message.obtain();
        message.what = 1;
        mHandler.sendMessage(message);
    }


    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == 1) {
                //doNothing
            }
        }
    };
}

代码分析:mHandler持有Activity引用,同时mHandler会作为成员变量保存在发送的消息msg中,即msg持有mHandler的引用。而mHandler是Activity的非静态内部类实例,即mHandler持有Activity的引用,那么我们就可以理解为msg间接持有Activity的引用。msg被发送后先放到消息队列MessageQueue中,然后等待Looper的轮询处理。那么当Activity退出后,msg可能仍然存在于消息对列MessageQueue中未处理或者正在处理,那么这样就会导致Activity无法被回收,以致发生Activity的内存泄露。同时可以再次看到,内存泄漏的本质还是:生命周期短的对象被生命周期长的对象应用。 通常在Android开发中如果要使用内部类,但又要规避内存泄露,一般都会采用静态内部类+弱引用的方式。代码如下:

public class MainActivity extends AppCompatActivity {
    MyHandler mHandler;

    public static class MyHandler extends Handler {

        private WeakReference<Activity> mActivityWeakReference;

        public MyHandler(Activity activity) {
            mActivityWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        start();
    }

    private void start() {
        Message message = Message.obtain();
        message.what = 1;
        mHandler.sendMessage(message);
    }
}

非静态内部类造成内存泄露类似的场景还有包括使用Thread或者AsyncTask等。要避免内存泄露的原理是一样的,像上面Handler一样使用静态内部类+弱应用的方式,有兴趣的同学可以进一步深入研究。

4、未取消注册和回调导致的内存泄漏

比如我们在activity中注册了广播,在activity退出前,没有及时的取消,那么这个广播就会一直存在系统中,同时一直保留对我们activity的引用,这样也就导致了内存泄漏,代码如下:

public class MainActivity extends AppCompatActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        this.registerReceiver(mReceiver, new IntentFilter());
    }
 
    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
        }
    };
 
    @Override
    protected void onDestroy() {
        super.onDestroy();
        // this.unregisterReceiver(mReceiver); // activity ondestroy时不及时取消注册
    }
}

所以我们在使用广播时,一定要记得及时取消注册,不然很容易发生内存泄漏了。

问题总结

本文初步介绍了安卓开发过程中内存泄漏的部分常见场景,包括:单例导致的内存泄漏、静态变量导致的内存泄漏、非静态内部类导致的内存泄漏、未取消注册和回调导致的内存泄漏等,持续更新,有兴趣的同学可以进一步深入研究。