上一篇介绍了Android内存溢出,今篇我来继续介绍一下关于Android内存优化的内存泄露。
内存泄露的基础理解
- 一般内存泄露的原因是:由忘记释放分配的内存导致的。(如Cursor忘记关闭等)
- 逻辑内存泄露的原因是:当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中。
这样一方面占用了宝贵的内存空间,这样容易导致后续需要分配内存的时候,空闲空间不足而出现内存溢出OOM。所以我么要理解好内存泄露,泄露形象理解成煤气泄露,是我们不用煤气的时候,应该是关掉阀口,但是在不正常的情况中,阀口没有关好,导致煤气泄露了。
Android中常见的内存泄漏汇总
我们关键需要做到的是:及时回收没有使用的对象
需手动关闭的对象没有关闭,在try/catch/finally中的处理
- HTTP
- File
- ContendProvider
- Bitmap
- Uri
- Socket
- Cursor
onDestory()或者onPause()中未及时关闭对象
部分实例:- Handler泄露:我们的Handler是要求写出static的,但是实际开发中我们并没有写到,因此我们要使用弱引用和在onDestory()中removeCallbacksAndMessages(null)手动关闭。
- 广播泄露:在手动注册广播时:需要退出的时候unregisterReceiver()。
- Service泄露:使用Service进行某耗时操作结束后,需要手动stopself()。
- WebView需要手动调用WebView.onPause()和WebView.destory()。
- 常用第三方/开源框架泄露:ShareSDK、JPush、BaiduMap、ButterKnife等需要注意在对应的Activity的生命周期中关闭。
这些我就不一一说明了,大家或多或少都有接触到,这些都是需要我们手动关闭,属于一般内存泄露。
- 然而我们可以借助LeakCanary、MAT等工具来检测应用程序是否存在以下几种情况内存泄漏。
单例造成的内存泄漏
由于单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。如下这个典例:
public 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,所以这个Context的生命周期的长短至关重要:
1、传入的是Application的Context:这将没有任何问题,因为单例的生命周期和Application的一样长。
2、传入的是Activity的Context:当这个Context所对应的Activity退出时,由于该Context和Activity的生命周期一样长(Activity间接继承于Context),所以当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用。
所以正确的单例应该修改为下面这种方式:
public 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;
}
}
这样不管传入什么Context最终将使用Application的Context,而单例的生命周期和应用的一样长,这样就防止了内存泄漏
静态View
有时,当一个Activity经常启动,但是对应的View读取非常耗时,我们可以通过静态View变量来保持对该Activity的rootView引用。这样就可以不用每次启动Activity都去读取并渲染View了。这确实是一个提高Activity启动速度的好方法!但是要注意,一旦View attach到我们的Window上,就会持有一个Context(即Activity)的引用。而我们的View有事一个静态变量,所以导致Activity不被回收。当然了,也不是说不能使用静态View,但是在使用静态View时,需要确保在资源回收时,将静态View detach掉。
内部类
我们知道,非静态内部类持有外部类的一个引用。因此,如果我们在一个外部类中定义一个静态变量,这个静态变量是引用内部类对象。将会导致内存泄漏!因为这相当于间接导致静态引用外部类。
static InnerClass innerClass;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
innerClass = this.new InnerClass();
}
class InnerClass {
//
}
匿名类
与内部类一样,匿名类也会持有外部类的引用。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
//另一个线程中持有Activity的引用,并且不释放
while (true) ;
}
}.execute();
}
Handler类引起内存泄漏
在Activity中定义一下Handler类:
public class MainActivity extends Activity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//TODO
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandler.sendMessageDelayed(Message.obtain(), 60000);//延迟一分钟执行
finish();
}
}
我们知道非静态内部类会持有外部类的引用,这里Activity finish后,延时消息会继续存在主线程消息队列中1分钟,然后处理消息。而该消息引用了Activity的Handler对象,然后这个Handler又引用了这个Activity。这些引用对象会保持到该消息被处理完,这样就导致该Activity对象无法被回收,从而导致了上面说的 Activity泄露。
要修改这个问题,把Handler类定义为静态,然后通过WeakReference来持有外部的Activity对象。还在onDestory()中清掉Handler消息。
public class MainActivity extends Activity {
private CustomHandler mHandler;
private static class CustomHandler extends Handler {
private WeakReference<MainActivity> mWeakReference;
public CustomHandler(MainActivity activity) {
mWeakReference = new WeakReference<MainActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity activity = mWeakReference.get();
if (null != activity)
switch (msg.what) {
///
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandler = new CustomHandler(this);
}
@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
Thread对象引起内存泄露
同Handler对象可能造成内存泄露的原理一样,Thread的生命周期不一定是和Activity生命周期一致。
而且因为Thread主要面向多任务,往往会造成大量的Thread实例。
据此,Thread对象有2个需要注意的泄漏点:
- 创建过多的Thread对象
- Thread对象在Activity退出后依然在后台执行
解决方案是:
- 使用ThreadPoolExecutor,在同时做很多异步事件的时候是很常用的,这个不细说。
- 当Activity退出的时候,退出Thread。
TimerTask引起泄露
TimerTask对象在和Timer的schedule()方法配合使用的时候极容易造成内存泄露。要在合适的时候进行Cancel即可。
private void cancelTimer(){
if (mTimer != null) {
mTimer.cancel();
mTimer = null;
}
if (mTimerTask != null) {
mTimerTask.cancel();
mTimerTask = null;
}
}
监听器管理内存泄露
当我们需要使用系统服务时,比如执行某些后台任务、为硬件访问提供接口等等系统服务。我们需要把自己注册到服务的监听器中。然而,这会让服务持有 activity 的引用,如果在Activity销毁的时候没有注销这些监听器,会导致内存泄漏。
void registerListener() {
SensorManager sensorManager = (SensorManager) getSystemServic(SENSOR_SERVICE);
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
sensorManager.registerListene(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);
}
然而对于中级Android工程师的面试过程中,如果你是一名老鸟,这个问题想必已经深入你的心。但是你是一个对内存模模糊糊的工程师,你的答案可能让面试官并不满意。下面的总结对你或许有点帮助。
常见的内存泄露问题
- 单例造成的泄露。
- 静态View造成的泄露。
- 内部类持有外部类实例的强引用。
- Handler、Thread耗时操作劫持Activity的外部类。
- TimerTask对象没有及时回收和置空。
- 系统监听器的引起的泄露。
避免内存泄露的方法
- 注意Context上下文的使用。
- 谨慎使用static。
- 使用静态内部类来代替内部类。
- 静态内部类使用弱引用WeakReference来引用外部类。
- 在声明周期结束的时候释放资源。
- 优化布局文件,减少层级关系。
- 谨慎使用第三方框架。
- 谨慎使用多线程。
- 横竖屏切换引起的GC。
- 避免创建不必要的对象。
还有就是如何检测内存泄露问题了。具体操作下次再详细分析
- 善用Android Studio的标签Memory。
- LeakCanary的使用。
- MAT的使用。