1. 背景
  2. 常见案例

背景

  众所周知,Android内存泄漏的排查一直是Android性能优化重要的组成部分.只要稍有不慎,直接或者间接地持有了Activity的引用,都会难以避免地引发内存泄漏.很多偶现的BUG,奇怪的Crash,或者是出现了OOM,可能它的罪魁祸首,都是内存泄漏.
  最近一直在给公司app做性能优化工作,中途遇到了很多内存泄漏的问题.现在将其整理出来,希望能给大家一个参考.

常见案例

1.静态类,静态变量持有Activity的引用

可能大家都知道静态变量,静态类一定会导致Activity的内存泄漏.也不会有人故意去写个静态变量然后去持有Activity的引用.但是有些场景还是容易被大家给忽略掉.现在给大家列举出来.

  • a.Actvitiy全局管理.
      在绝大多数情况下,大家可能会想到利用Activity的全局管理来实现整个应用的退出.这个时候难免就会出现一个全局静态List来管理Activity.如果我们将Activity注册到List中,可能在退出的时候就会引发内存泄漏了.
      所以建议大家在管理Activity的时候,采用弱引用的方式来对Activity进行全局管理.
    如:
private static LinkedList<WeakReference<FragmentActivity>> activityList;
  • b.单例模式-封装网络请求
      单例模式大家都耳熟能详了,很多童鞋在封装网络请求工具的时候都喜欢用单例模式.但是一旦把Activity的引用传进单例中,可能就会引发内存泄漏了.因此我们要尽量避免将Activity的引用传入单例模式中.如果仅仅是为了获取Context的上下文,我们可以利用Activity中getApplicationContext().来脱离来对Activity的引用.
      但是某些场景下我们的确是需要持有Actvity的引用.那我们要记得及时清除它.下文我将会单独介绍这种使用场景.
2.非静态的匿名内部类,内部类

  内部类现在大家使用的可能比少,我们只要注意申明的时候将内部类申明成static类型,就可以避免它持有对外部类(Activity)的引用.这里我们主要着重讲一下匿名内部类.

  • a.匿名内部类-封装网络请求回调
      绝大多数开发者在网络请求回调的时候都采用匿名内部类的方式来进行UI的渲染操作.殊不知,这时候其实这个匿名内部类已经持有了外部类Activity的引用.如果成功回调回来,并完成它的使命,那么大吉大利.但是某些情况下,由于网络请求过多或者过慢,我们在退出某个界面的时候,回调还是没有成功返回.那么这时,Activity就出现了泄漏了.
      这种情况的解决方法就是要在退出界面的时候及时取消掉跟这个界面有关的所有网络请求.就如上文我们在单例模式中提到的一样,我们就不得不持有Activity的引用,并将它作为一个TAG,给网络请求打上标记.在我们退出界面的时候,清除掉所有跟这个界面所有有关的请求.
      庆幸的是,如果我们使用的是OKHttp,或者OKGo,那么它已经帮我们实现了标记TAG的功能,我们只要在使用的时候给某条请求,贴上(Avtivity)标签,并在销毁的时候根据(Activity)标签来取消所有的请求.同时还要销毁Activtiy的引用(然后框架已经帮我们做了).那么我们就可以避免这次来之不易的内存泄漏了.
  • b.匿名内部类-自定义观察者模式回调
      观察者模式已经越来越被我们熟悉和喜爱,很多优秀的框架的设计都采用了这个模式.比如Rxjava,EventBus等等.但是当我们自己在设计观察者模式的时候,我一定不能忘记解注册这个过程,这个过程看起来好像没有什么用,但是没有它,那么观察者模式将会成为内存泄漏的罪魁祸首.因为我们在使用观察者模式的时候难免会开辟匿名内部类,如果我们不在Activity退出的时候及时销毁观察者,那么观察者所携带的匿名内部类将会导致内存泄漏.
  • c 匿名内部类-Callback在另一个匿名内部类中回调
    我们先假设这个CallBack的回调里面持有了Activity的引用,请看下面一段代码:
//假设这个CallBack的回调里面持有了Activity的引用
public void start(final CallBack callback) {
//mIConnectListener是成员变量
mCallback = callback;
        new Thread(() -> {
        	callback.ok();
            }
 }

public void clear(){
mIConnectListener =  null;
}

  乍一看好像没什么问题,我们利用了成员变量mCallback保存了callback引用,并最后利用了clear()释放掉了引用.但是当你真正运行的时候你会发现它还是内存泄漏了.这是为什么?
  这是因为Thread回调里面的callback已经不是曾经那个少年了.
换一句话说,就是当我们使用匿名内部类的时候,如果匿名内部类想要引用外部的局部变量,它就必须拷贝一份变量来作为自己的成员变量.就如上面Thread匿名内部类中用的callback其实是外面callback的拷贝.如果我们反编译class文件,我们就会看匿名内部类$Thread里面有一个成员变量 $callback.(这里以 $符号开头是匿名内部类的标志).至于为什么要拷贝,这里就不再详细解释了.大家可以查阅相关资料.毕竟我们这篇文章主要是讨论内存泄漏.
分析好原因,我们来说一下解决方案,其实很简单,我们不要在匿名内部类中使用局部变量就没这个问题了,我们可以直接在匿名内部类中使用mIConnectListener进行回调.

3.Toast

我们在用使用Toast或者封装Taost工具类的时候,一定要加上context.getApplicationContext(),以保证我们的Toast不持有Activty的引用.如:

Toast.makeText(context.getApplicationContext(), "", Toast.LENGTH_SHORT).show();

不然一旦我们离开当前Activity的时候,如果我们的Toast还没消失,那么必定会内存泄漏.

4.getSystemService获取系统服务

当我们在使用系统服务的时候(比如WifiManager),我们也要注意Context的传入,尽管某些时候使用某些服务传入Activity引用也并不会导致内存泄漏,但是从安全和使方面考虑,传入Context更有优势.

5.属性动画或者自定义动画

我们项目中可能经常会用到一些无限循环的属性动画,例如水波纹之类的,毕竟相对于帧动画和View动画,它太好用了.但是它的缺点就是容易导致内存泄露.从原理上来说它其实就是无限循环地在执行view的刷新操作.我们自定义无限循环的动画,其实也是这个思路.
那么这个时候我们就要注意及时将View的引用给释放掉.因此

如果我们是属性动画我们记得onDestory的时候调用animator.cancel();

@Override
protected void onDestroy() {     
	mAnimator.cancel();

如果我们是自定义动画,我们可以在onDetachedFromWindow进行动画的停止操作

@Override
    protected void onDetachedFromWindow() {
	     //TODO 停止动画操作
        super.onDetachedFromWindow();
    }

  
最后,建议大家使用leakcanary并结合mat分析工具或者AndroidStudio自带的profiler工具来让自己的代码变得更优秀.