记一次Rxjava导致的内存泄漏

2019-12-15  本文已影响0人  设计失

昨天检测出Rxjava导致的内存泄漏之后,一时无从下手;想了一晚上还是无果,于是今天早上搜谷歌的时候换了另一个关键词rxjava ObservableOnSubscribe内存泄漏, 于是乎在网上找到了一篇大佬的文章,果断读之并加以收藏:

一张图搞定-RxJava2的线程切换原理和内存泄露问题分析

首先,安卓中内存泄漏无外乎一点:生命周期长的对象持有生命周期短的对象,导致GC无法到达从而引起内存泄漏的问题;那什么对象生命周期会长呢?

一般生命周期比较长的对象:单例匿名内部类HandlerStatic局部变量等等。

事情的起因:

某一天闲暇的下午,运营人员在线上直播间举办了一场轰轰烈烈的K歌活动;在某个时刻,在线人数一度达到五百人以上,公司各部高管进入直播间送礼物的送礼物~上麦位聊天的聊天,一开始的场面十分和谐;过了不到半小时,主持人那边反映说,客户端卡顿,消息数一直不停的刷;一些人就提议说把消息显示的公屏直接关了,聊天什么的都不显示;这样能够保证直播间不再那么卡顿,活动也能将就着进行!

分析:

直播间卡顿,一个是直播间人数过多,导致推流与拉流会很频繁,网络请求也会在回调的时候更新UI从而使得在16ms中处理过多的逻辑;同样的,在直播间人数过多的情况下,里面的观众会和麦位上的主播们互动,会不断的发送消息,不断的赠送礼物,所以在公屏下面会一直有消息;

当时我观察到在一秒钟内可以达到几百条消息,这是很恐怖的,也是很正常的,但是我们的界面就会很卡,而关闭公屏之后,界面就不会出现卡顿,所以我可以断定出现的问题在公屏上面的RecyclerView上,因为来一条消息会去notify一下,来一条消息就会刷新界面,这是代码逻辑处理不当导致的,所以我们得从公屏消息优化着手;

优化开始:

既然来一条消息就会导致界面的更新,我们完全可以做一个缓冲池的操作,把一段时间内进入的消息缓存起来,然后达到了这时间就一次性去刷新界面,这样就不会有在16ms内做过多的处理问题了;那问题是:这个缓冲池怎么写呢?

第一种方法

我们可以使用Handler去延时处理,然后将数据保存到LinkedBlockingQueue中,等到达一定时间后就从队列中取数据, LinkedBlockingQueue有一个特性是同步和从中取就删掉了元素,所以我们不需要管理数据的增删操作,这样的做法缺陷比较明显,使用Handler比较容易出现内存泄漏,而且代码维护起来也不太方便,这里不做演示~

另外一种方法

因为项目中我们大量都使用到了RxJava,所以当有一些特别复杂的逻辑的时候,我会优先考虑使用到RxJava,其中RxJava就有一个操作符buffer, 关于buffer的操作这里不做过多的解释,意思和我们优化前分析的一样:先将一段时间内的数据保存,然后过一段时间再一次性刷新界面;代码如下:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    recyclerView = findViewById(R.id.recycler_view);
    recyclerView.setLayoutManager(new LinearLayoutManager(this));
    adapter = new CurrentListAdapter(null);
    recyclerView.setAdapter(adapter);
    
    // 在oncreate() 方法中创建方法
    compositeDisposable.add(
            Observable
                    .create(new ObservableOnSubscribe<String>() {
                        @Override
                        public void subscribe(ObservableEmitter<String> emitter) throws Exception {
                            if (!emitter.isDisposed()) {
                                mEmitter = emitter;
                            }
                        }
                    })
                    .buffer(1_000, TimeUnit.MILLISECONDS)
                    .subscribeOn(Schedulers.io())
                    .observerOn(AndroidSchedulers.mainThread())
                    .subscribe(new Consumer<List<String>>() {
                        @Override
                        public void accept(List<String> strings) throws Exception {
                            // TODO
                            Log.d(TAG, "接受数据");
                            adapter.addDatas(strings);
                        }
                    }, new Consumer<Throwable>() {
                        @Override
                        public void accept(Throwable throwable) throws Exception {
                        }
                    })
    );
}

方法很简单,使用create创建操作符,在onCreate的时候创建一次发射器,然后拿到发射器之后可以在当前页面发送数据了,然后buffer操作符传入时间间隔,这里我们模拟在一秒钟内获取, 我们写一个按钮来发送数据:

int index = 0;
/**
 * 添加数据
 *
 * @param view
 */
public void addData(View view) {
    for (int i = index; i < index + 5; i++) {
        mEmitter.onNext(i + "");
        Log.d(TAG, "发送数据");
    }
    index += 5;
}

运行代码, 点击按钮,我们每次发送5个数据,这样在accept方法中就一次性更新了界面;很完美,这样的处理方式我们完全能接受, 当我们点击按钮:

// 初始化走一次
2019-12-15 18:47:31.442 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 接受数据

// 点击按钮发送数据
2019-12-15 18:47:31.636 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 发送数据
2019-12-15 18:47:31.636 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 发送数据

// 沦陷获取数据
2019-12-15 18:47:32.443 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 接受数据
2019-12-15 18:47:33.443 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 接受数据
2019-12-15 18:47:34.442 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 接受数据
2019-12-15 18:47:35.443 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 接受数据
2019-12-15 18:47:36.442 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 接受数据
2019-12-15 18:47:37.443 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 接受数据
2019-12-15 18:47:40.443 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 接受数据
2019-12-15 18:47:41.444 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 接受数据
2019-12-15 18:47:56.445 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 接受数据

// 再次点击按钮发送数据
2019-12-15 18:47:56.938 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 发送数据
2019-12-15 18:47:56.938 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 发送数据
2019-12-15 18:47:57.443 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 接受数据
2019-12-15 18:47:58.443 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 接受数据
2019-12-15 18:47:59.443 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 接受数据
2019-12-15 18:48:00.443 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 接受数据
2019-12-15 18:48:01.445 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 接受数据
2019-12-15 18:48:02.443 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 接受数据
2019-12-15 18:48:03.445 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 接受数据
2019-12-15 18:48:04.444 21352-21352/com.example.rxjavamemoryleak D/MainActivity: 接受数据

界面显示:


rxjava_buffer.gif

可以看到,每次点击按钮增加数据,会一次性添加五个数据,而且顺序和数据都没改变。

问题出现:

当我兴致勃勃的提交代码,为自己的【机智】感到高兴的时候,页面突然报出leakcanary内存检测的弹窗,当时没有太注意,就提交了代码。

leak内存泄漏.png

相信大家对这张图很熟悉了,通过profile我们也能看到,Activity结束之后,内存根本没有释放,反复的进入到这列表页面之后,内存会持续的增加:

profile检测.png
分析
上一篇下一篇

猜你喜欢

热点阅读