Android中内存泄露的常见场景以及优化
【概念】Java中,当一个不再需要被使用的对象仍被其他对象所引用时,会造成该对象无法被gc及时回收,所占用的内存空间无法释放,从而导致内存单元的浪费。
Android开发过程中,一些不合理的开发方式会导致app存在内存泄露的情况,导致app性能下降,严重时会产生crash。下面介绍Android几种常见的内存泄露场景,以及优化方案。
单例导致的内存泄露
【问题】单例模式在Android开发过程中会经常用到,但是对于单例的使用不当可能会造成内存泄露。因为单例的静态特性,使得它的生命周期与application是一致的,如果单例对象持有了不再需要使用的对象的引用,那么在整个application生命周期中这个对象都不会被释放,从而造成内存泄露。
public class AppSettings { private static AppSettings sInstance; private Context mContext; private AppSettings(Context context) {
this.mContext = context; } public static AppSettings getInstance(Context context) { if (sInstance == null) { sInstance = new AppSettings(context); } return sInstance; } }
例如以上代码中,单例的getInstance方法传入了context的引用,该context指向的对象一般为Activity、Service等上下文,这时就会导致在整个application的生命周期内,该单例所持有的context无法释放,进一步导致Activity/Service内部资源以及视图无法被释放,严重影响app运行过程中的性能表现。
【解决】为了避免这样的单例造成的内存泄露,可以改成
private AppSettings(Context context) { this.mContext = context.getApplicationContext(); }
全局context对应的本身就是应用程序的上下文,和单例的生命周期保持一致,这样就避免了内存泄露。
静态变量导致的内存泄露
【问题】静态变量存储在方法区(Method Area),它的生命周期从类加载开始,一直持续到进程结束。一旦静态变量初始化,那么它所持有的引用就只有等到进程结束才会释放。
比如,在Activity中为了避免重复创建Info,将sInfo声明为静态变量:
public class MainActivity extends AppCompatActivity { private static Info sInfo;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (sInfo != null) { sInfo = new Info(this);
} }
}
sInfo作为Activity的静态成员变量,持有Activity实例的引用,同时生命周期比Activity要长,在Activity退出时sInfo仍然引用了Activity,导致Activity无法及时释放,造成内存泄露。
【解决】在开发过程中,静态成员变量的生命周期很容易与其使用方生命周期不一致,为避免泄露,可尽量减少静态变量的使用,或者提供api回调,在使用方生命周期结束时将静态变量的引用指向null。
非静态内部类导致的内存泄露
【问题】非静态内部类,也就是匿名内部类,默认会持有其外部类的引用,当非静态内部类的生命周期长于外部类时,也会造成内存泄露。
Android开发过程中,非静态内部类的一个典型使用场景就是Handler,例如:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
start(); }
private void start() { Message msg = Message.obtain();
msg.what = 1;
mHandler.sendMessage(msg); } private Handler mHandler = new Handler() {
@Override public void handleMessage(Message msg) {
if (msg.what == 1) { // do something
}
}
}}; }
通过Handler的消息机制我们可以知道,Handler的引用会作为成员变量保存在msg中,也就是msg持有对Hanlder的引用,而Handler作为匿名内部类持有外部类Activity的引用,也就相当于发出的msg间接持有了Activity的引用。msg对象被发送到MessageQueue中,等待Looper的轮询处理,若Activity退出时,MessageQueue中仍存在未处理的msg,那么此时Activity就无法及时被释放,从而造成内存泄露。
【解决】通常在Android开发过程中,如果要使用内部类,那么可以采取静态内部类+弱引用的方式。
public class MainActivity extends AppCompatActivity { private Handler mHandler; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mHandler = new MyHandler(this); start(); }
private void start() { Message msg = Message.obtain(); msg.what = 1; mHandler.sendMessage(msg); }
private static class MyHandler extends Handler { private WeakReference<MainActivity> activityWeakReference; public MyHandler(MainActivity activity) {
activityWeakReference = new WeakReference<>(activity); }
@Override public void handleMessage(Message msg) { MainActivity activity = activityWeakReference.get();
if (activity != null) { if (msg.what == 1) {
// do something }
} }
} }
Handler通过弱引用的方式持有Activity的引用,这样在GC进行回收时,Activity占用的内存可以及时释放,从而避免了内存泄露。
当然,这样的做法是依赖GC的回收时机的,在Activity退出到GC到达之间的这段时间内,还是会有不必要的内存占用,所以最完善的做法是在Activity销毁时,将Handler中的msg全部都移除掉,不再进行处理:
@Overrideprotected void onDestroy() { super.onDestroy(); mHandler.removeCallbacksAndMessages(null); }
匿名内部类的使用还有Thread,回调接口Listener等等,可使用类似的方法进行代码优化,避免不规范的内存使用。
未取消注册或回调造成的内存泄露
【问题】比如在Activity中动态注册广播监听,如果在Activity销毁时未进行unregister,那么这么广播会一直存在于系统中,并且持有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);
} }
在观察者模式的使用中,同样需要注意注册与取消注册的问题,比如RxJava+Retrofit的Observeble使用。
Timer和TimerTask造成的内存泄露
【问题】Timer和TimerTask一般会用于一些与时间相关的任务,比如循环滚动的ViewPager、倒计时相关的业务逻辑、轮询请求接口实现实时刷新等。如果Activity在销毁时,其成员变量Timer/TimerTask中的计时任务并没有完成,仍然持有Activity的引用,这时也会造成内存泄露。
【解决】在Activity#onDestroy()中将所有Timer/TimerTask成员进行cancel,可有效避免内存泄露。
集合保存的对象未及时清除导致的内存泄露
【问题】同样的生命周期不一致的问题,如果Collection/Map中保存的item不再需要使用,而集合仍然持有它们的引用,这时也会造成内存不合理占用。
【解决】这个问题需要关注的就是Collection/Map的生命周期,使用方在对其保存的items不再使用的时候需要及时clear/remove掉。
资源未及时关闭导致的内存泄露
【问题】文件存取的I/O操作、Stream流操作,数据库读取的Cursor操作,bitmap的读写操作,TypedArray的attr属性读取操作等,未及时进行关闭或者释放,也会造成内存泄露。
【解决】在业务操作结束的时候,及时释放相关的资源,避免占用内存空间,这些操作可大可小,很容易长时间累积之后OOM,需特别注意。
属性动画造成的内存泄露
【问题】与Timer类似,属性动画也是一个耗时任务,在Activity销毁时如果还有执行中的属性动画,同样会造成内存泄露。
【解决】在Activity销毁时对Activity中的属性动画执行canel操作。
@Overrideprotected void onDestroy() { super.onDestroy(); mAnimator.cancel(); }
WebView造成的内存泄露
【问题1】WebView在加载页面时,会长期占用内存而不能被释放,因为WebView的网络请求由内核实现,所以app端无法对其进行控制。
【解决】同大多数操作一样,在Activity销毁时调用WebView#destroy()。
【问题2】在Android 5.1版本以后,WebView中添加的Callback会持有Activity的引用,从而造成即使调用了destroy()方法,也无法释放WebView占用的内存。
【解决】在销毁WebView之前,先把WebView从其父容器之中移除。具体分析见:http://blog.csdn.net/xygy8860/article/details/53334476
@Overrideprotected void onDestroy() { super.onDestroy(); // 先从父控件中移除 WebView mWebViewContainer.removeView(mWebView); mWebView.stopLoading(); mWebView.getSettings().setJavaScriptEnabled(false); mWebView.clearHistory(); mWebView.removeAllViews(); mWebView.destroy(); }
总结
1. 单例的构造尽量不要依赖具体的Activity,两边的生命周期不一致。
2. 减少静态引用的使用,必要时在使用方生命周期结束回调中将静态变量置空。
3. 使用静态内部类+弱引用的方式代替非静态内部类。
4. 注册/取消注册广播或者观察者需要成对出现,并且对应使用方的生命周期。
5. Timer/TimerTask、ObjectAnimator及时取消。
6. I/O stream、File、bitmap、TypedArray及时关闭或者回收。
7. Activity销毁时对WebView进行完整的销毁。

浙公网安备 33010602011771号