首页 > 安全资讯 >

浅析内存泄漏

16-10-08

本人还在培训期,对网络上的知识点进行归纳、总结,然后再分享给大家,有时候也会加入了一些个人的看法和见解,如果有一些谬误欢迎留言。从我目前学习的情况看来,内存泄漏的意思大概就是:一个代码不规范导致的,内存空间使用之后无法及时回收的问题。

本人还在培训期,对网络上的知识点进行归纳、总结,然后再分享给大家,有时候也会加入了一些个人的看法和见解,如果有一些谬误欢迎留言。

从我目前学习的情况看来,内存泄漏的意思大概就是:
一个代码不规范导致的,内存空间使用之后无法及时回收的问题。

几个内存泄漏的样例以及分析

案例选自百度文库

一、非静态内部类创建静态实例导致的内存泄漏

错误案例:

public class MainActivity extends Activity {
    static Demo sInstance = null;

    @Override
    public void onCreate(BundlesavedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (sInstance == null) {
            sInstance = new Demo();
        }
    }

    class Demo {
        void doSomething() {
            //...
        }
    }
}

分析:上面的代码中的sInstance实例类型为静态实例,在MainActivity的第一个实例创建时,sInstance会获得并一直持有MainActivity的引用。因为sInstance持有MainActivity的引用,所以MainActivity是无法被GC回收的,一旦MainActivity创建了第二个实例,进程中会存在2个MainActivity实例,旧的MainActivity对象就成了无法回收的垃圾对象。
所以,对于不是单例的Activity,应该避免在activity里面实例化其非静态内部类的静态实例。对于上述的这种情况,完全可以将内部类设计成单例类。

二、单例模式持有外部本应该被释放的对象

错误案例:

class AppManager {
    private static AppManager instance;
    private Context context;

    private AppManager(Context context) {
        this.context = context;
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

分析:单例的静态特性使得单例的生命周期和应用的生命周期一样长,如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。
上述代码中创建这个单例的时候,由于需要传入一个Context,

如果传入的是Application的Context:这将没有任何问题,因为单例的生命周期和Application的一样长; 但是,传入的如果是Activity的Context:当这个Context所对应的Activity退出时,由于这个单例类拥有Context的引用,而Context和Activity的生命周期一样长(Activity间接继承于Context),当前Activity就无法被正常回收。

正确的单例应该修改为下面这种方式:

class AppManager {
    private static AppManager instance;
    private Context context;

    private AppManager(Context context) {
        this.context = context.getApplicationContext();
    }

    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
} 

三、Handler造成的内存泄漏

错误案例:

public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            //...         
        }
    };

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

    private void loadData() {
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
}

分析:由于mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,我们知道消息队列是在一个Looper线程中不断轮询处理消息,那么只要Handler发送的Message尚未被处理,则该Message及发送它的 Handler对象将被线程MessageQueue一直持有。由于Handler属于TLS(Thread Local Storage)变量,生命周期和Activity是不一致的,很容易导致Activity无法正确释放。
Handler 的使用要尤为小心,否则将很容易导致内存泄露的发生。

一种做法为:

public class MainActivity extends AppCompatActivity {
    private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView

    private static class MyHandler extends Handler {
        private WeakReference reference;

        public MyHandler(Context context) {
            reference = new WeakReference<>(context);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity) reference.get();
            if (activity != null) {
                activity.mTextView.setText("");
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView) findViewById(R.id.textview);
        loadData();
    }

    private void loadData() {
        //...request          
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
}

缺点分析:创建一个静态Handler内部类,然后对Handler持有的对象使用弱引用,这样在回收时也可以回收Handler持有的对象,这样虽然避免了Activity泄漏,不过Looper线程的消息队列中还是可能会有待处理的消息,所以我们在Activity的Destroy时或者Stop时应该移除消息队列中的消息,

更准确的做法如下:

public class MainActivity extends AppCompatActivity {
    private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView

    private static class MyHandler extends Handler {
        private WeakReference reference;

        public MyHandler(Context context) {
            reference = new WeakReference<>(context);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity) reference.get();
            if (activity != null) {
                activity.mTextView.setText("");
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView) findViewById(R.id.textview);
        loadData();
    }

    private void loadData() {
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
}

使用mHandler.removeCallbacksAndMessages(null)移除消息队列中所有消息和所有的Runnable。当然也可以使用 mHandler.removeCallbacks();或mHandler.removeMessages();来移除指定的Runnable和Message。
如果是HandlerThread应该在onDestroy时将线程停止掉:mThread.getLooper().quit();

四、线程造成的内存泄漏

错误案例(2个):

    public void test() {
        //AsyncTask错误案例
        new AsyncTask() {
            @Override
            protected Void doInBackground(Void... params)
            {
                SystemClock.sleep(10000);
                return null;
            }
        }.execute();

        //Thread错误案例
        new Thread(new Runnable() {
            @Override
            public void run() {
                SystemClock.sleep(10000);
            }
        }).start();
    }

分析:如果Thread的run方法一直在循环的执行不停,而该Thread又持有了外部变量,那么这个外部变量即发生内存泄漏。上面的异步任务和Runnable都是一个匿名内部类,因此它们对当前Activity都有一个隐式引用。如果Activity在销毁之前,任务还未完成,那么将导致Activity的内存资源无法回收,造成内存泄漏。

正确的做法还是使用静态内部类的方式:

//AsyncTask正确用法
static class MyAsyncTask extends AsyncTask {
    private WeakReference weakReference;

    public MyAsyncTask(Context context) {
        weakReference = new WeakReference<>(context);
    }

    @Override
    protected Void doInBackground(Void... params) {
        SystemClock.sleep(10000);
        return null;
    }

    @Override
    protected void onPostExecute(Void aVoid) {
        super.onPostExecute(aVoid);
        MainActivity activity = (MainActivity) weakReference.get();
        if (activity != null) {
            //...            
        }
    }
}
new MyAsyncTask(this).execute();

//Thread正确用法
static class MyRunnable implements Runnable {
    @Override
    public void run() {
        SystemClock.sleep(10000);
    }
}
new Thread(new MyRunnable()).start();

这样就避免了Activity的内存资源泄漏,当然在Activity销毁时候也应该取消相应的任务AsyncTask::cancel(),避免任务在后台执行浪费资源。

五、注册某个对象后未反注册

注册广播接收器、注册观察者等等,比如:BraodcastReceiver忘记解注册……

六、Android特殊组件或者类忘记释放

比如:Cursor忘记销毁、Socket忘记close、TypedArray忘记recycle、callback忘记remove、Bitmap忘记recycle等等。虽然有些系统程序,它本身是可以自动取消注册的,但是我们还是应该在我们的程序中明确的取消注册,程序结束时应该把所有的注册都取消掉。

七、集合中对象没清理造成的内存泄露

  某个集合类(List)被一个static变量引用,同时这个集合类没有删除自己内部的元素。

如何阻止内存泄漏?

以下是一些阻止内存泄漏的快速动手技巧。

一、注意集合类

例如HashMap,ArrayList,等等。因为它们是内存泄漏经常发生的地方。当它们被声明为静态时,它们的生命周期就同应用程序的生命周期一般长。

二、注意资源的回收以及解注册

如果一个监听器已经注册,但是当这个类不再被使用时却未被注销,就会发生内存泄漏。在自定义控件的时候、位图处理的时候……注意:TypedArray、Bitmap的回收等等。

三、如果一个类管理它自己的内存

如果一个类管理它自己的内存,程序员应该对内存泄漏保持警惕。很多时候当一个对象的成员变量指向其他对象时,不再使用时需要被置为null(“置为null”这一点待考证,以前这么写过,然后被嘲笑了,但是想想似乎也对)。

四、避免在activity里面实例化其非静态内部类的静态实例

因为非静态内部类默认会持有外部类的引用,而又使用了该非静态内部类创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。正确的做法为: 将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例。

五、避免将Activity作为对象使用,注意Context的生命周期

因为activity的引用的生命周期,超越了activity对象的生命周期。也就是常说的Context泄漏,因为activity就是context。
想要避免context相关的内存泄漏,需要注意以下几点:

不要对activity的context长期引用(一个activity的引用的生存周期应该和activity的生命周期相同) 如果可以的话,尽量使用关于application的context来替代和activity相关的context 如果一个acitivity的非静态内部类的生命周期不受控制,那么避免使用它;正确的方法是使用一个静态的内部类,并且对它的外部类有一WeakReference。

六、及时停止耗时操作

这一点主要体现在Handler和Thread以及一些网络请求上,由于事务不能及时处理完毕,当拥有外部引用的时候,将导致资源无法回收。

相关文章
最新文章
热点推荐