被使用的对象仍被其他对象所引用时,会造成该对象无法被gc及时回收,所占用的内存空间无法释放,从而导致内存单元的浪费。
Android开发过程中,一些不合理的开发方式会导致app存在内存泄露的情况,导致app性能下降,严重时会产生crash。下面介绍Android几种常见的内存泄露场景,以及优化方案。
单例导致的内存泄露
【问题】单例模式在Android开发过程中会经常用到,但是对于单例的使用不当可能会造成内存泄露。因为单例的静态特性,使得它的生命周期与application是一致的,如果单例对象持有了不再需要使用的对象的引用,那么在整个application生命周期中这个对象都不会被释放,从而造成内存泄露。
例如以上代码中,单例的getInstance方法传入了context的引用,该context指向的对象一般为Activity、Service等上下文,这时就会导致在整个application的生命周期内,该单例所持有的context无法释放,进一步导致Activity/Service内部资源以及视图无法被释放,严重影响app运行过程中的性能表现。
静态变量导致的内存泄露
【问题】静态变量存储在方法区(Method Area),它的生命周期从类加载开始,一直持续到进程结束。一旦静态变量初始化,那么它所持有的引用就只有等到进程结束才会释放。
比如,在Activity中为了避免重复创建Info,将sInfo声明为静态变量:
sInfo作为Activity的静态成员变量,持有Activity实例的引用,同时生命周期比Activity要长,在Activity退出时sInfo仍然引用了Activity,导致Activity无法及时释放,造成内存泄露。
【解决】在开发过程中,静态成员变量的生命周期很容易与其使用方生命周期不一致,为避免泄露,可尽量减少静态变量的使用,或者提供api回调,在使用方生命周期结束时将静态变量的引用指向null。
非静态内部类导致的内存泄露
【问题】非静态内部类,也就是匿名内部类,默认会持有其外部类的引用,当非静态内部类的生命周期长于外部类时,也会造成内存泄露。
Android开发过程中,非静态内部类的一个典型使用场景就是Handler,例如:
通过Handler的消息机制我们可以知道,Handler的引用会作为成员变量保存在msg中,也就是msg持有对Hanlder的引用,而Handler作为匿名内部类持有外部类Activity的引用,也就相当于发出的msg间接持有了Activity的引用。msg对象被发送到MessageQueue中,等待Looper的轮询处理,若Activity退出时,MessageQueue中仍存在未处理的msg,那么此时Activity就无法及时被释放,从而造成内存泄露。
【解决】通常在Android开发过程中,如果要使用内部类,那么可以采取静态内部类+弱引用的方式。
Handler通过弱引用的方式持有Activity的引用,这样在GC进行回收时,Activity占用的内存可以及时释放,从而避免了内存泄露。
当然,这样的做法是依赖GC的回收时机的,在Activity退出到GC到达之间的这段时间内,还是会有不必要的内存占用,所以最完善的做法是在Activity销毁时,将Handler中的msg全部都移除掉,不再进行处理:
匿名内部类的使用还有Thread,回调接口Listener等等,可使用类似的方法进行代码优化,避免不规范的内存使用。
未取消注册或回调造成的内存泄露
【问题】比如在Activity中动态注册广播监听,如果在Activity销毁时未进行unregister,那么这么广播会一直存在于系统中,并且持有Activity的引用,从而造成内存泄露。
【解决】注册与取消注册一定要成对使用。
在观察者模式的使用中,同样需要注意注册与取消注册的问题,比如RxJava+Retrofit的Observeble使用。
Timer和TimerTask造成的内存泄露
【问题】Timer和TimerTask一般会用于一些与时间相关的任务,比如循环滚动的ViewPager、倒计时相关的业务逻辑、轮询请求接口实现实时刷新等。如果Activity在销毁时,其成员变量Timer/TimerTask中的计时任务并没有完成,仍然持有Activity的引用,这时也会造成内存泄露。
【解决】在Activity#onDestroy()中将所有Timer/TimerTask成员进行cancel,可有效避免内存泄露。
集合保存的对象未及时清除导致的内存泄露
【问题】同样的生命周期不一致的问题,如果Collection/Map中保存的item不再需要使用,而集合仍然持有它们的引用,这时也会造成内存不合理占用。
【解决】这个问题需要关注的就是Collection/Map的生命周期,使用方在对其保存的items不再使用的时候需要及时clear/remove掉。
资源未及时关闭导致的内存泄露
【问题】文件存取的I/O操作、Stream流操作,数据库读取的Cursor操作,bitmap的读写操作,TypedArray的attr属性读取操作等,未及时进行关闭或者释放,也会造成内存泄露。
【解决】在业务操作结束的时候,及时释放相关的资源,避免占用内存空间,这些操作可大可小,很容易长时间累积之后OOM,需特别注意。
属性动画造成的内存泄露
【问题】与Timer类似,属性动画也是一个耗时任务,在Activity销毁时如果还有执行中的属性动画,同样会造成内存泄露。
【解决】在Activity销毁时对Activity中的属性动画执行canel操作。
WebView造成的内存泄露
【问题1】WebView在加载页面时,会长期占用内存而不能被释放,因为WebView的网络请求由内核实现,所以app端无法对其进行控制。
【解决】同大多数操作一样,在Activity销毁时调用WebView#destroy()。
【问题2】在Android 5.1版本以后,WebView中添加的Callback会持有Activity的引用,从而造成即使调用了destroy()方法,也无法释放WebView占用的内存。
【解决】在销毁WebView之前,先把WebView从其父容器之中移除。
总结
内存泄露在 Android 内存优化是一个比较重要的一个方面,很多时候程序中发生了内存泄露我们 不一定就能注意到,所有在编码的过程要养成良好的习惯。总结下来只要做到以下这几点就能避 免大多数情况的内存泄漏:
1. 单例的构造尽量不要依赖具体的Activity,两边的生命周期不一致。
2. 减少静态引用的使用,必要时在使用方生命周期结束回调中将静态变量置空。
3. 使用静态内部类+弱引用的方式代替非静态内部类。
4. 注册/取消注册广播或者观察者需要成对出现,并且对应使用方的生命周期。
5. Timer/TimerTask、ObjectAnimator及时取消。
6. I/O stream、File、bitmap、TypedArray及时关闭或者回收。
7. Activity销毁时对WebView进行完整的销毁。