Android之内存泄漏以及解决办法

文章链接:

知识点:

  1. 单例造成的内存泄漏原因和解决方法;
  2. Handler造成的内存泄漏原因和解决方法;
  3. 耗时线程造成的内存泄漏原因和解决方法;
  4. 非静态内部类造成的内存泄漏原因和解决方法;
  5. WebView引起的内存泄漏原因和解决方法;
  6. 资源未关闭造成的内存泄漏原因和解决方法;
  7. 集合对象造成的内存泄漏和解决方法;
  8. listview的adapter没有缓存造成的内存泄漏和解决方法;
  9. 内存泄漏和内存溢出的区别;
  10. 新名词记录{内存泄漏;LeakCanary:检测APP内存泄漏的第三方工具;}

概述


2017.4.28-这里需要明确两个概念

内存泄漏和内存溢出的区别:

内存泄漏:在程序中动态分配给对象内存空间,当此对象所在的程序结束之后,此对象所占用的内存并没有被释放,那么就造成内存泄漏了。判断条件:对象同时满足“可达”和未使用两个条件,就可以判断为内存泄漏。这里描述的是状态。
内存溢出:就是系统分配一个10大小的内存空间给一个对象,但是这个对象实际需要20的空间来存放,那么这就造成了内存溢出。这里描述的是结果。
联系点:内存泄露导致堆栈内存不断增大,从而引发内存溢出,内存泄漏时内存溢出的引发原因之一。
知道了内存泄漏和内存溢出的区别,就可以进行下面的查看了。


内存泄漏,这是一个很容易被人忽视的知识点。在不知不觉中,你的APP突然就崩了,而且只报了一个oom,或者是什么错误都没有,修复也就无从下手了。其实这个很有可能是由于你的APP最终占用的内存超过了设备给你的内存的上限,所以APP爆掉了。

众所周知,移动设备的资源很有限,Android系统给每个APP的内存都有一个阈值,一般是16M。如果程序里有对象一直占用APP内存空间的一部分,而且越来越大,那么你的APP最终的内存大小可能就超过阈值,从而引发内存溢出,最后系统因为“不好意思,你吃饭吃得过分了”,需要把你赶出屋子。表现就是你的APP闪退了。

谁都不愿看到自己的APP在用户的手上闪退了。不能闪退,这已经是不能低的底线了。所以这篇文章就是你应该要看的文章了。并且开始检查你的APP中是否下面几个相类似的用法,有的话,赶紧去看看会不会内存泄漏了。

我在这里是利用LeakCanary进行内存泄漏的检测的,LeakCanary的只是不属于本文的范畴,有需要的可以去搜索LeakCanary看看介绍和使用。

新加内容:

构造Adapter时,没有使用缓存造成的内存泄漏

listview的缓存策略我们都知道:创建(屏幕可见的item + 1 )个item,在一个item被划出屏幕时,系统会帮我们缓存起来,以便下次回收利用。如果我们在使用listview的时候,没有用到convertView,而是每次都重新new一个viewholder,那么可以想象当我们一个listview里面有10000条数据或更多,那么占用的APP内存就很大很大了,最终极有可能内存溢出。

所以,这里的解决方法是:在给listview的adapter里面,尽量的使用convertView进行item的回收利用,而不是每次都重新的创建新的item,这样不仅消耗性能,而且还容易造成内存泄漏。代码如下:

public View getView(int position, View convertView, ViewGroup parent) { 
ViewHolder viewholder = new ViewHolder(); 
convertView = mInflater.inflate(R.layout.item_layout, null);//item_layout是listview的每一个item布局文件
viewHolder.tv_userName = convertView.findViewById(R.id.tv_userName);
viewHolder.tv_userName.setText("yaojt");
return view; 
}

上面的listview的适配器中,每显示一天item,就会创建一个viewholder,不仅仅造成加载的时候消耗CPU等资源,会造成卡顿;加之如果item有上千条上万条,或者有大图片在item里面,那么当滑出屏幕的item没有能够及时的得到回收,就会造成内存泄漏了。所以上面的方法是一个潜在的风险。
那么如何来解决呢?我们需要用到listview的回收机制,当item滑出屏幕之后,变为不可见,系统会回收此item,那么我们就可以利用已经有的item,而不是重新创建一个。如此一来,内存里面最多就只有屏幕可见item数+1个item了。那么问题就比较好的解决了。
修改后的代码如下所示:

public View getView(int position, View convertView, ViewGroup parent) { 
ViewHolder viewholder = null; 
if (convertView == null) { 
viewholder = new ViewHolder(); 
convertView = mInflater.inflate(R.layout.item_layout, null);//item_layout是listview的每一个item布局文件
viewHolder.tv_userName = convertView.findViewById(R.id.tv_userName);
converView.setTag(viewholder);//这里将viewholder缓存起来,再利用
... 
} else { 
viewholder = convertView.getTag();//这里取出缓存的viewholder直接使用
... 
}
viewHolder.tv_userName.setText("yaojt");
return view; 
}

集合对象没有没有清理造成的内存泄漏

我们通常会把一些对象的引用利用集合来管理,当我们不需要该对象时,如果没有把它的引用从集合中清理掉,这样这个集合就会越来越大,占用的内存也越来越大。如果这个集合是static的话,会随程序常驻内存不会被释放掉,那情况就更严重了。

例如在我们需要监听屏幕点亮和关闭LockScreen。监听每次关闭屏幕事件,都会注册几个callback在ArrayList< InfoCallback>、ArrayList< SimStateCallback>集合中,但是当点亮屏幕之后,这些callback并没有及时的remove掉。可能一次两次集合不会很大,heap增长也不会很明显,不会出现大的问题。但是比如关闭和点亮屏幕这种用户很常操作的动作,可能不过一个小时,上述的集合可能体积就很大很大了,由于锁屏是驻留在system_server进程里,所以导致结果是手机重启。

所以这里的解决方法就是:对于不使用的集合,需要及时的removed。如果可以预计到集合的大小,尽量规定一个集合的大小,因为如果一个集合的size小了,Java会以*2+1的体积扩展集合容量。


单例造成的内存泄漏

单例模式的“威名”做开发的人一定都听过。单例模式的好处主要是提供一个实例,统一访问变量,不会造成过多的资源消耗。但是在Android开发中,我们经常会因为错误的使用单例模式而造成内存泄漏。

例如,实例化单例模式的时候,我们经常要传入一个context,如果单例的生命周期比我们的context生命周期更长,那么当我们的context需要释放时(context通常是activity),就会因为单例实例引用着context,因为这是强引用,导致context不能被及时回收,从而造成内存泄漏。

看下面的单例内存泄漏的例子:

public class LoginImp {
    private static LoginImp mInstance;
    private Context mContext;

    private LoginImp(Context context) {
        this.mContext = context;
    }

    public static LoginImp getInstance(Context context) {
        if (mInstance == null) {
            synchronized (LoginImp.class) {
                if (mInstance == null) {
                    mInstance = new LoginImp(context);
                }
            }
        }
        return mInstance;
    }

    public void login() {
    }

}

这里我们看到,如果我们在一个activity里面获取LoginImp实例,然后调用登录方法,在关闭此activity,就会造成内存泄漏。Android开发中我们尤其需要注意生命周期这一个概念,无论是任何对象的,因为移动设备的资源毕竟很有限。

解决方法:
既然是实例的生命周期可能比传入的context生命周期要长,那么我们可以根据context获得有更长的生命周期的对象,比如application对象的生命周期是和APP的生命周期一样长的,就和传入的context进行了解耦。修改后的代码如下:

//私有构造函数,获得application对象
private LoginManager(Context context) {
        this.mContext = context;
        //解决 办法要保证Context和AppLication的生命周期一样,而不是直接饮用context对象
        //this.mContext = context.getApplicationContext();
    }

课外音:我们这里使用到synchronized关键字,正确的使用方式是上面的单例模式那样子的,需要作双重的非空判断,才能够使单例真正的“单例”。如果需要连接更多,请看这里。


Handler造成的内存泄漏

Handler大家也是见得很多很多了吧。如果使用不当,那么也有内存泄漏的可能性。下面看一种情况。

public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler();
    private TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView) findViewById(R.id.text);//模拟内存泄露
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                dosomething();
            }
        }, 3 * 60 * 1000);
        this.finish();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}

在上面的代码中,mHandler会隐式的持有外部类的对象,这个外部类就是MainActivity。在mHandle中,我们延时执行180秒之后才开始dosomething(),但是我们立马finish()结束MainActivity,由于mHandler对MainActivity有着引用,所以MainActivity得不到释放,资源无法及时回收,从而造成内存泄漏。

那么这个问题如何来解决呢?

很简单,只要在MainActivity结束的时候,移除mHandler的引用不就可以了嘛。代码如下:

@Override
    protected void onDestroy() {
        super.onDestroy();
        //移除mHandler的引用即可
        mHandler.removeCallbacksAndMessages(null);
    }

又或者,我们可以使用软引用,代替mHandler对MainActivity的强引用,软引用在内存吃紧的情况下,Java回收机制会优先于回收软引用的对象。我们可以继承Handler,然后对传入的对象进行软引用。代码如下:

private SoftHandler mHandler = new SoftHandler(this);

public static class SoftHandler extends Handler {
    private final WeakReference<Activity> mActivity;

    public SoftHandler(Activity activity) {
        mActivity = new WeakReference<Activity>(activity);
    }

    @Override public void handleMessage(Message msg) {
        super.handleMessage(msg);
    }
}

补充,这里还有一种泄漏的方式:

public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler();
    private TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //这里声明的是后台handler
        Thread mThread = newHandlerThread("handler1", Process.THREAD_PRIORITY_BACKGROUND);   
        mThread.start();  
MyHandler mHandler = new MyHandler( mThread.getLooper( ) );  
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}

解释:HandlerThread的run方法是一个死循环,它不会自己结束,线程的生命周期超过了activity生命周期,当横竖屏切换,HandlerThread线程的数量会随着activity重建次数的增加而增加。

解决的方法就是在应该在onDestroy时将线程停止掉,调用如下方法:

mThread.getLooper().quit();

耗时线程造成的内存泄漏

现在第三方的包都帮我们做好了很多线程的工作,比如很多网络请求库,io库等等。但是我们也有可能直接使用AsyncTask原生类进行操作。AsyncTask在doInBackGround()方法里面做耗时的操作。时间长短不是我们能够控制的,在耗时操作过程中,我们需要结束发起此耗时操作的activity,那么这个activity就不能被及时的回收,造成内存泄漏。

解决方法是,在对activity进行finish的时候,也去检测AsyncTask是否还存活着,如果存活就结束掉AsyncTask对象。代码如下:

private AsyncTask asyncTask;

    @Override
    protected void onDestroy() {
        super.onDestroy();
        destroyAsyncTask();
    }

    //检测和结束AsyncTask对象
    private void destroyAsyncTask() {
        if (asyncTask != null && !asyncTask.isCancelled()) {
            asyncTask.cancel(true);
        }
        asyncTask = null;
    }

非静态内部类造成的内存泄漏

这个引起内存泄漏的方式,和Handler是一样的:都是隐式的持有了activity对象。只要改成static静态的内部类,就能够是内部类不会对activity有一个隐式的持有activity对象了。

代码如下:

public class MainActivity extends AppCompatActivity {
    private static UserBean mUserBean;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //模拟内存泄露
        if (mUserBean == null) {
            mUserBean= new UserBean();
            mUserBean.setAge(24);
            mUserBean.setName("yaojt");
        }
        finish();
    }
    //修改后:加入static修饰内部类
   static class UserBean { 
        private int age;
        private String name;

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
}

WebView引起的内存泄漏

HTML大行其道的今天,Android也很早加入对HTML的支持了,支持在原生页面中利用WebView控件加载HTML页面。但是WebView会在解析网页时,占用很多的内存。如果打开了十多个HTML页面,因为之前的页面不会被释放,用于快速返回前一个页面,所以占用内存就很很可能会爆,导致APP闪退。

解决方法是在我们推出承载WebView的activity之前,需要清除掉HTML页面占用的内存,释放资源。代码如下:

private void destroyWebView() {
        if (mWebView != null) {
            mLinearLayout.removeView(mWebView);
            mWebView.pauseTimers();
            mWebView.removeAllViews();
            mWebView.destroy();
            mWebView = null;
        }
    }

资源未关闭造成的内存泄漏

这个就比较好理解了,说的就是例如我们访问本地数据库的时候,为了不经常启动和关闭数据库,会保持一段时间对数据库的联系,但是我们在最后绝对不要忘了要及时关闭数据库。还有其他可能会因为未关闭而引起的内存泄漏问题:BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等等。解决方法就比较简单了,关闭连接就OK了。例如cursor游标需要在最后关闭掉。

cursor.close();

总结

上面就是几种在我们开发过程中,比较常遇到的内存泄漏的问题,我们需要特别注意上面的几个问题,因为有可能一开始内存泄漏问题不是很明显,但是操作到一段时间之后,APP就可能挂掉了。而你可能找不到哪里报错了,就算知道了是oom,你也很难知道如何来修复,因为内存泄漏的问题是“日积夜累”而发生的。

说白了就是:我们要在应该释放某些引用的时候,就要及时的释放掉,不能让它一直占用着资源。

如有任何问题,请及时与我联系。谢谢。