从一个RxJava使用实例中探究Android内存泄露的产生

内存泄露的根本原因

当一个对象处于可以被回收状态时,却因为该对象被其他暂时不可被回收的对象持有引用,而导致不能被回收,如此一来,该对象所占用的内存被回收以作他用,这部分内存就算是被泄露掉了。简单来说,就是该丢掉的垃圾还占着有用的空间没有被及时丢掉

内存泄露示例

最近在项目中遇到一个有点“隐蔽”的内存泄露,是通过集成的内存泄露检测工具 LeakCanary 检测出来的。我们的项目中使用了第三方自动更新的sdk,并且写了AutoUpdateManager这个单例的类来统一管理自动更新相关功能。自动更新方法的代码如下:

    public Observable checkUpdate(final Context context, final boolean bForce) {
        return NetManager.getInstance().getCheckVersionUpdateObservable()
                .observeOn(Schedulers.newThread())
                .flatMap(new Func1>() {
                    @Override
                    public Observable call(CheckVersionUpdateRsp rsp) {
                        if (rsp.isNeedUpdate()) {
                            return getUpdateVersionInfo(context, bForce);
                        } else {
                            // 云校园后台禁止检查更新
                            return Observable.empty();
                        }
                    }
                }).observeOn(AndroidSchedulers.mainThread());
    }

    private Observable getUpdateVersionInfo(final Context context, final boolean bForce) {
        return Observable.create(new Observable.OnSubscribe() {
            @Override
            public void call(final Subscriber subscriber) {
                IFlytekUpdateListener updateListener = new IFlytekUpdateListener() {
                    @Override
                    public void onResult(int updateStatus, UpdateInfo updateInfo) {
                        if (hasUpdate && bForce) {
                            // 当前缓存有更新信息且无需考虑是否wifi,则直接将缓存的更新信息返回
                            subscriber.onNext(updateVersionInfo);
                            subscriber.onCompleted();
                            return;
                        }

                        if (updateStatus == UpdateErrorCode.OK && updateInfo != null && !(updateInfo.getUpdateType() == UpdateType.NoNeed)) {
                            //弹出更新dialog
                            hasUpdate = true;
                            AutoUpdateManager.this.updateInfo = updateInfo;
                            AutoUpdateManager.this.updateVersionInfo = new UpdateVersionInfo(updateInfo.getUpdateDetail(), updateInfo.getUpdateVersion());

                            subscriber.onNext(updateVersionInfo);
                            subscriber.onCompleted();
                        } else {
                            Timber.d("获取更新信息失败或者当前无更新的版本");
                            hasUpdate = false;
                            subscriber.onNext(null);
                            subscriber.onCompleted();
                        }
                    }
                };
                if (bForce) {
                    // forceUpdate指不区分wifi和移动网络均返回更新信息, 当前只使用这种方式
                    updateAgent.forceUpdate(context, updateListener);
                } else {
                    updateAgent.autoUpdate(context, updateListener);
                }
            }
        });
    }

上面代码中的checkUpdate方法在我们的MainActivity中被如下调用:

AutoUpdateManager.getInstance().checkUpdate(MainApplication.getContext(), true)
                .observeOn(AndroidSchedulers.mainThread())
                .compose(this.bindToLifecycle())
                .subscribe(new Action1() {
                    @Override
                    public void call(AutoUpdateManager.UpdateVersionInfo updateVersionInfo) {
                        showUpdateDialog(updateVersionInfo);
                    }
                }, new Action1() {
                    @Override
                    public void call(Throwable throwable) {

                    }
                });

代码看起来比较多,另外因为使用了RxJava来写网络回调,并且有所嵌套,所以对象之间的引用不能一下子理清楚。

分析之前,我们需要理解一句话:(非静态)内部类(包括匿名内部类)天然持有自己外部类的(隐式)引用。这句话其实很容易理解,但很多时候会被忽略。在java基础中,我们肯定学过如果A类中有一个内部类C,那么C的对象可以通过new A.C()来获取,所以C天然持有A的引用。只不过我们有时通过自动导入包名后,就不用在前面加上“A.”了。

LeakCanary工具检测到MainActivity有泄露,而且打印出了堆栈信息,分析发现从updateAgent.forceUpdate(context, updateListener)方法进入sdk里面的代码,层层追踪,发现最终传进去的updateListener参数被赋值给一个c类中的c变量,而这个c变量持有MainActivity的引用导致MainActivity的泄露。

public class c implements b {
    private HashMap a = new HashMap();
    private com.iflytek.autoupdate.c.c.c b;
    private IFlytekUpdateListener c;
    private com.iflytek.autoupdate.a.a d;
    private static c e = null;

    public static c a(IFlytekUpdateListener var0, com.iflytek.autoupdate.a.a var1) {
        if(e == null) {
            e = new c(var0, var1);
        }

        return e;
    }

    ……

    // updateListener最终传入了这个方法
    public void a(IFlytekUpdateListener var1) {
        this.c = var1;
    }

}

而很明显,c类是一个单例。

单例的内存泄露是最普遍的,因为单例对象本身是静态的,它的生命周期是跟应用程序的生命周期一样长的,所以单例中的成员变量如果持有了外部对象的引用(诸如Activity之类需要被即时回收的对象)而没有被即使释放,则一定会产生内存泄露的。

那么问题来了,为什么c变量(即updateListener)会持有MainActivity的引用呢?我们回到updateListener被new出来的地方看:

IFlytekUpdateListener updateListener = new IFlytekUpdateListener() {
    @Override
    public void onResult(int updateStatus, UpdateInfo updateInfo) {
        if (hasUpdate && bForce) {
            // 当前缓存有更新信息且无需考虑是否wifi,则直接将缓存的更新信息返回
            subscriber.onNext(updateVersionInfo);
            subscriber.onCompleted();
            return;
        }

        if (updateStatus == UpdateErrorCode.OK && updateInfo != null && !(updateInfo.getUpdateType() == UpdateType.NoNeed)) {
            //弹出更新dialog
            hasUpdate = true;
            AutoUpdateManager.this.updateInfo = updateInfo;
            AutoUpdateManager.this.updateVersionInfo = new UpdateVersionInfo(updateInfo.getUpdateDetail(), updateInfo.getUpdateVersion());

            subscriber.onNext(updateVersionInfo);
            subscriber.onCompleted();
        } else {
            Timber.d("获取更新信息失败或者当前无更新的版本");
            hasUpdate = false;
            subscriber.onNext(null);
            subscriber.onCompleted();
        }
    }
};

我们发现在updateListener的回调方法中,它引用了subscriber,而这个subscriber是RxJava中的观察者(Subscriber本身继成于Rx中的Observer)。在MainActivity中我们调用checkUpdate方法,订阅了此方法返回的被观察者(Observable),通过subscribe方法传进去了一个观察者Subscriber,当然这里的写法没有直接new一个Subscriber,而是new了两个Action1对象,其实在subscribe方法中,通过这两个对象,创建出了Subscriber的一个子类:

/**
 * A Subscriber that forwards the onXXX method calls to callbacks.
 * @param  the value type
 */
public final class ActionSubscriber extends Subscriber {

    final Action1 onNext;
    final Action1 onError;
    final Action0 onCompleted;

    public ActionSubscriber(Action1 onNext, Action1 onError, Action0 onCompleted) {
        this.onNext = onNext;
        this.onError = onError;
        this.onCompleted = onCompleted;
    }

    @Override
    public void onNext(T t) {
        onNext.call(t);
    }

    @Override
    public void onError(Throwable e) {
        onError.call(e);
    }

    @Override
    public void onCompleted() {
        onCompleted.call();
    }
}

而这个ActionSubscriber也最终被传入Observable.OnSubscribe类的回调call方法中,即上面提到的updateListener对象的回调方法里所引用的subscriber对象。最终通过层层传递,MainActivity的引用被sdk的内部单例c类所持有了。而且sdk没有提供释放引用的方法,于是导致了MainActivity的内存泄露。

那么可能有人同我一样会产生疑问,在Activity中我们经常对各种view设置点击事件,View.OnClickListener()其实就是Activity的匿名内部类,它也持有了Activity的引用,而我们也从来没有在Activity销毁之前调用view.setOnClickListener(null)这样的代码。那为什么不会产生内存泄露呢?

view.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // do something
    }
});

那是因为Activity中的View都是附着在Activity自身的视图中的,当Activity销毁时,ActivityManager会自动销毁相关的视图,View也随之销毁,自然不会再持有Activity的引用了。

如何防止内存泄露

管理好对象的生命周期,及时释放掉无用的对象。比如我们在Activity启动时注册了一个BroadcastReceiver,但在Activity销毁时没有及时进行反注册,程序就会打印出错误的log提示。

E/ActivityThread: Activity holenzhou.com.aboutfragment.SecondActivity has leaked IntentReceiver holenzhou.com.aboutfragment.TestReceiver@7e54937 that was originally registered here. Are you missing a call to unregisterReceiver()?

还有我们最常用的Handler,有时编译器会提示我们Handler可能会造成内存泄露,建议我们将它声明为静态内部类,同时持有外部Activity的弱引用。

/**
 * Instances of static inner classes do not hold an implicit
 * reference to their outer class.
 */
private static class MyHandler extends Handler {
  private final WeakReference mActivity;

  public MyHandler(SampleActivity activity) {
    mActivity = new WeakReference(activity);
  }

  @Override
  public void handleMessage(Message msg) {
    SampleActivity activity = mActivity.get();
    if (activity != null) {
      // ...
    }
  }
}

private final MyHandler mHandler = new MyHandler(this);

但其实有更简单的解决方法,就是在Activity的onStop方法中,通过下面的api将Handler相关的所有的Callbacks和Messages移除掉,只不过这种方式很容易被忘记。类似的还有Android的Timer类。

handler.removeCallbacksAndMessages(null);

有些我们自定义的对象需要我们自己手动释放相关引用。比如我们定义了一些单例对象后,持有了外部Activity的引用,就需要在适当的时候释放相关的引用。

还有我们经常见到的网络请求框架里面都会有一个cancel方法取消发送除去的网路请求,其实也是为了方便我们在Activity销毁前及时移除网络请求的异步回调,防止造成内存泄露。

你可能感兴趣的:(从一个RxJava使用实例中探究Android内存泄露的产生)