前言
Android应用因为本身可用内存的限制,需要特别重视内存泄露的问题,本文总结了Android中常见的一些内存泄露原因及避免方式。
一、单例造成的内存泄露
由于单例的静态特性使得单例的生命周期和应用的生命周期一样长,这说明,如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么该对象将不能被正常回收,这就导致了内存泄露。
例,如下的写法是我们开发中非常常见的一种写法,但是其实会存在一些问题:
public class MusicManager {
private static MusicManager instance;
private Context context;
private MusicManager (Context context) {
this.context = context;
}
public static MusicManager getInstance(Context context) {
if (instance != null) {
instance = new MusicManager (context);
}
return instance;
}
}
当创建这个单例的时候,由于要传入一个Context,所以这个Context的生命周期的长短十分重要
1、如果传入的是application的context,那么没有任何问题,因为单例的生命周期和Application一样长
2、如果传入的是一个Activity的Context,当这个Context对应的Activity退出的时候,因为该context的引用被单例所持有,所以导致activity不会被回收
所以请避免单例模式传入的是activity的context,或者说当你想要写的单例需要持有一个activity时,请思考是否需要做成单例的。
上述代码可以做以下修改
public class MusicManager {
private static MusicManager instance;
private Context context;
private MusicManager(Context context) {
this.context = context.getApplicationContext();
}
public static MusicManager getInstance(Context context) {
if (instance != null) {
instance = new MusicManager(context);
}
return instance;
}
}
这样就不管调用者传入的是activity还是application的context都不会出现内存泄露
二、非静态内部类创建静态实例造成的内存泄露
我们经常会在activity中,创建一个非静态内部类的静态对象,如下代码
public class MainActivity extends AppCompatActivity {
private static TestResource mResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mManager == null){
mManager = new TestResource();
}
//...
}
class TestResource {
//...
}
}
这样些虽然避免了对象的重复创建,但是因为static修饰的变量的生命周期和应用一样长,然后非静态内部类会默认持有外部类的引用,这样导致了activity的实例一直被持有,导致activity销毁了但是不会被回收。正常的做法应该是将该内部类修饰成静态的,或者抽取出来封装成一个单例。
三、Handler造成的内存泄露
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(){
//发送消息
Message message = Message.obtain();
mHandler.sendMessage(message);
}
}
很多人在使用handler时,会如上的写法,但是mHandler是Handler的非静态匿名内部类 的实例,所以它持有外部类Activity的引用,而消息队列中的Message持有mHandler实例的引用,然后mHandler又持有Activity的引用,会出现,如果这个acitivity退出的时候,消息队列中还有未处理或者正在处理的消息时,就会引发内存泄露,所以要避免这种情况的出现。
可以通过创建一个静态Handler的内部类,然后对Handler持有的引用使用弱引用,这样可以避免activity的内存泄露。但是消息队列中可能还会有待处理的消息,所以在activity退出的时候,调用移除消息队列中的消息。
在activity销毁的时候,移除handler中的消息
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);//移除消息
}
创建一个static的Handler内部类。
static class MyHandler extends Handler {
WeakReference<Activity > mActivityReference;
//创建一个静态的handler的内部类,然后将activity的对象主动传进去,
MyHandler(Activity activity) {
mActivityReference= new WeakReference<Activity>(activity);
//用弱引用去持有该handler的对象
四、线程造成的内存泄露
线程造成的内存泄露主要是因为线程的生命周期不可控
new Thread(new Runnable() {
@Override
public void run() {
SystemClock.sleep(10000);
}
}).start();
上述的代码,很多人都会在项目中写到,现在的Runnable对象是一个匿名内部类,他默认持有外部类的引用,如果在activity销毁的时候,该线程还没执行完成,此刻的activity的内存资源就无法被回收,正确的做法还是使用静态内部类的方式,如下
static class MyRunnable implements Runnable{
@Override
public void run() {
SystemClock.sleep(10000);
}
}
new Thread(new MyRunnable()).start();
五、资源未关闭造成的内存泄露
对于使用了BraodcastReceiver、ContentObserver、File、Cursor、Stream、Bitmap等资源,在Activity销毁的时候应该及时关闭或者注销,否则这些资源将不会被回收,造成内存泄露
六、注册监听器没有及时移除导致的内存泄露
android中可以通过Context.getSystemService(int name)来获取到系统服务,这些服务工作在各自的进程中,帮助应用处理后台任务,硬件交互等。如果需要使用这些服务,可以注册监听器,这会导致服务持有了Context的引用,如果传入的是一个Activity的Context,那么在Activity销毁的时候没有注销这些监听器,可能会导致内存泄露。
public class LeaksActivity extends Activity implements LocationListener {
private LocationManager locationManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leaks);
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);//拿到LocationManager对象
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
TimeUnit.MINUTES.toMillis(5), 100, this);//注册一个监听
}
// Listener implementation omitted
}
如上代码,我们让Android的LocationManager通知我们位置更新。我们需要传入一个监听器,在这里我们让Activity实现了位置监听接口,这意味着LocationManager将持有该Activity的引用。现在如果出现了Activity销毁的情况,而且没有移除掉监听的话,该Activity的内存资源将不会被回收,就会导致内存泄露
七、WebView导致的内存泄露
因为WebView这个控件本身的一些缺陷,会出现内存泄露的情况,因为,我们在xml中使用WebView控件的时候,在它初始化的时候,会将activity对象传给WebView,在activity销毁的时候,如果WebView没有及时释放该context,就会出现内存泄露。
避免出现这种情况,可以通过以下几种方式去做:
A、避免在xml直接写WebView的控件,而是在代码中进行动态注册,然后传入一个applicationContext,而不是activity的Context
WebView mWebView = new WebView(getApplicationgContext());
LinearLayout mll = findViewById(R.id.xxx);
mll.addView(mWebView);
然后在activity销毁的时候,及时调用destory方法
protected void onDestroy(){
super.onDestroy();
mWebView.removeAllViews();
mWebView.destroy()
}
这样WebView就会持有applicationContext,而不是activity的Context,但是这样做会有个问题,当在WebView中打开连接或者打开的页面加载flash时,会出现类型转换异常,导致页面崩溃,因为加载flash的时候,系统会把WebView作为一个父控件,然后在该空间上绘制flash,它想找一个Activity的context来绘制他,而你传入的是ApplicationContext。
B、如果你不能给我删除引用,那么我自己来删除,核心思想是通过反射的方式,实现删除引用
public void setConfigCallback(WindowManager windowManager) {
try {
Field field = WebView.class.getDeclaredField("mWebViewCore");
field = field.getType().getDeclaredField("mBrowserFrame");
field = field.getType().getDeclaredField("sConfigCallback");
field.setAccessible(true);
Object configCallback = field.get(null);
if (null == configCallback) {
return;
}
field = field.getType().getDeclaredField("mWindowManager");
field.setAccessible(true);
field.set(configCallback, windowManager);
} catch(Exception e) {
}
}
但是该方法是基于于android的webkit内核的,在android 4.4及以后版本中会失效,因为4.4之后更换了WebView的内核为chromium。
C、参考QQ的做法,让使用WebView控件的页面,单独运行在一个进程中,在该页面退出的时候,关闭该进程。安卓开启多进程的方式,就是在四大组件的AndroidMenifest中指定android:process属性。
D、Android 5.1系统中,因为WebView源码的一些问题,会导致内存泄露,关键方法在于onAttachedToWindow和onDetachedFromWindow。
onAttachedToWindow 添加一些监听器,执行在onDraw方法之前,也就是view绘制之前
onDetachedFromWindow 移除掉这些监听器,执行在view销毁前
看上去这两个方法没有什么问题,但是在5.1的源码中可以看到
public void onAttachedToWindow() {
if (isDestroyed()) return;//有是否destroyed的判断
if (mIsAttachedToWindow) {
Log.w(TAG, "onAttachedToWindow called when already attached. Ignoring");
return;
}
......
mComponentCallbacks = new AwComponentCallbacks();
mContext.registerComponentCallbacks(mComponentCallbacks);
}
@Override
public void onDetachedFromWindow() {
if (isDestroyed()) return;
if (!mIsAttachedToWindow) {
Log.w(TAG, "onDetachedFromWindow called when already detached. Ignoring");
return;
}
......
if (mComponentCallbacks != null) {
mContext.unregisterComponentCallbacks(mComponentCallbacks);
mComponentCallbacks = null;
}
......
}
在执行移除监听前进行了该页面是否销毁的判断,也就是说,我们在activity的onDestory方法中调用WebView的destory方法,会销毁webView,会导致anDetachedFromWindow方法在判断是否WebView是否销毁的时候,返回true,也就是说下面remove监听的那些方法不会执行,会导致activty的引用一直被持有
我们可以通过提前触发onDetachedFormWindow方法来避免这种问题
protected void onDestroy() {
if (mWebView != null) {
((ViewGroup) mWebView.getParent()).removeView(mWebView);
mWebView.destroy();
mWebView = null;
}
super.onDestroy();
}
我们可以通过将WebView从父控件中移除来触发WebView的onDetachedFromWindow方法,让它提前执行onDetachedFromWindow方法。我在开发时,在mx4 pro的手机上遇到过这种问题,其系统就是android 5.1的,有一个页面的对象一直回收不了,导致内存一直增加最后OOM,最后发现是因为WebView这个控件导致的。
注:本文内容来自网络他人共享以及自己平时的总结,因水平有限,难免出错,欢迎大家指正。