美文网首页安卓之美Android开发Android知识
从一个RxJava使用实例中探究Android内存泄露的产生

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

作者: HolenZhou | 来源:发表于2016-10-25 16:47 被阅读909次

    内存泄露的根本原因

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

    内存泄露示例

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

        public Observable<UpdateVersionInfo> checkUpdate(final Context context, final boolean bForce) {
            return NetManager.getInstance().getCheckVersionUpdateObservable()
                    .observeOn(Schedulers.newThread())
                    .flatMap(new Func1<CheckVersionUpdateRsp, Observable<UpdateVersionInfo>>() {
                        @Override
                        public Observable<UpdateVersionInfo> call(CheckVersionUpdateRsp rsp) {
                            if (rsp.isNeedUpdate()) {
                                return getUpdateVersionInfo(context, bForce);
                            } else {
                                // 云校园后台禁止检查更新
                                return Observable.empty();
                            }
                        }
                    }).observeOn(AndroidSchedulers.mainThread());
        }
    
        private Observable<UpdateVersionInfo> getUpdateVersionInfo(final Context context, final boolean bForce) {
            return Observable.create(new Observable.OnSubscribe<UpdateVersionInfo>() {
                @Override
                public void call(final Subscriber<? super UpdateVersionInfo> 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.<AutoUpdateManager.UpdateVersionInfo>bindToLifecycle())
                    .subscribe(new Action1<AutoUpdateManager.UpdateVersionInfo>() {
                        @Override
                        public void call(AutoUpdateManager.UpdateVersionInfo updateVersionInfo) {
                            showUpdateDialog(updateVersionInfo);
                        }
                    }, new Action1<Throwable>() {
                        @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<Long, c.a> 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 <T> the value type
     */
    public final class ActionSubscriber<T> extends Subscriber<T> {
    
        final Action1<? super T> onNext;
        final Action1<Throwable> onError;
        final Action0 onCompleted;
    
        public ActionSubscriber(Action1<? super T> onNext, Action1<Throwable> 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<SampleActivity> mActivity;
    
      public MyHandler(SampleActivity activity) {
        mActivity = new WeakReference<SampleActivity>(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销毁前及时移除网络请求的异步回调,防止造成内存泄露。

    相关文章

      网友评论

      • Vander丶:还是不太懂,为啥 MainActivity 的引用 就传递给单例了。

      本文标题:从一个RxJava使用实例中探究Android内存泄露的产生

      本文链接:https://www.haomeiwen.com/subject/aoizettx.html