Android中常见的内存泄露分析

来源:互联网 发布:vb自定义数据类型 编辑:程序博客网 时间:2024/04/30 04:37

前言

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这个控件导致的。


注:本文内容来自网络他人共享以及自己平时的总结,因水平有限,难免出错,欢迎大家指正。

0 0
原创粉丝点击