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

内存泄露的根本原因

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

内存泄露示例

最近在项目中遇到一个有点“隐蔽”的内存泄露,是通过集成的内存泄露检测工具 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销毁前及时移除网络请求的异步回调,防止造成内存泄露。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容

  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    宇宙只有巴掌大阅读 2,360评论 0 12
  • 【Android 内存泄漏】 引用: ★★★ 【知识必备】内存泄漏全解析,从此拒绝ANR,让OOM远离你的身边,跟...
    Rtia阅读 786评论 0 2
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    _痞子阅读 1,622评论 0 8
  • 所有知识点已整理成app app下载地址 J2EE 部分: 1.Switch能否用string做参数? 在 Jav...
    侯蛋蛋_阅读 2,407评论 1 4
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    apkcore阅读 1,216评论 2 7