Android中常见的内存泄露分析

前言

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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值