急速开发系列——RxJava 实战技巧大全

3,102 阅读13分钟

这几天一直看《激荡三十年》,感觉非常不错。这篇文章本身并不想写,总觉得没什么含量,后面写着写着,发现其中的有些点还是非常有意思的,因此这篇文章的重点在rxjava使用场景说明及rxjava内存优化上。rxjava的使用场景更多的取决于我们所面临的业务以及当前rxjava的一些操作符是否提供了对业务的支持,而对于内存优化上,则是要求我们尽可能快的解除订阅关系。

另外本文对RxBinding,RxLifecycle的介绍并未做过多的深入,原因在于这两个库无论是从实现还是从使用角度来说都是比较简单的,我们完全可以自行书写类似的库。

话不多说,步入正题。

引入RxBinding

在引入RxBinding之前首先要弄明吧RxBinding是什么?。

所谓的RxBinding是用来为界面元素绑定事件的,比如为Buttong设置点击事件等。浏览其源码,不难发现其实现原理也是通过包装原有事件实现的。

为什么要引入RxBinding?使用RxJava一定要引入RxBinding么?

首先很确定的说使用RxJava不要求你一定要使用RxBinding,大部分情况下没必要用。这里之所以要谈RxBinding一方面是完善我们的知识体系,看看响应式编程的思想是如何应用在android界面元素上,另一方面是看看RxBinding能够有效的解决什么问题?

RxBinding提供了和RxJava一致的api体验,更重要的是它更好的符合RxJava做法:通过将事件转化为Observable对象,最终可以利用RxJava一系列操作符对其处理,最典型的使用场景是界面防抖动,这点我们在rxjava真实应用场景中做详细的介绍。

关于如何使用RxBinding,直接参见RxBinding项目说明即可:github.com/JakeWharton…

rxjava真实应用场景

在上一篇文章中为解决异常问题,我们引入了rxjava的支持。接下来我们来看看rxjava在实际工程中的显著应用。

场景零:线程切换

rxjava引入让使得线程切换更加的容易,几行代码就可以搞定。RxAnroid的引入更是让我们非常容易的能够切换到UI线程。可以说,引入RxJava,就放弃古老而沉重的AsyncTask吧(初学者还是要学AsyncTask的)。最典型的就是从网络中获取数据,然后在更新界面,很显然获取数据操作需要发生在子线程,更新UI操作发声明在主线程。这里我们以模拟从数据库中获取联系人操作为例:

private void getConcactFromDB() {
        Observable.create(new Observable.OnSubscribe>() {
            @Override
            public void call(Subscriber super List> subscriber) {
                
                ArrayList list = new ArrayList<>();
                for (int i = 0; i < 100; i++) {
                    list.add("user name:" + i);
                }
                
                SystemClock.sleep(5000);

                subscriber.onNext(list);
                subscriber.onCompleted();
            }
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1>() {
                    @Override
                    public void call(List list) {
                        Log.d("MainActivity", "更新界面:" + list.size());
                    }
                });
    }

场景一:接口依赖(flatmap)

目前大部分服务端接口设计都是通过用户名和密码登录获取access token,后面其他api的请求都是借助该token。对于需要注册功能的产品来说,我们经常面对这样的问题:使用用户名和密码登录成功后,保存服务器返回的access token,再调用服务端接口获取用户的详情信息。不难发现,这里获取用户详情的请求依赖登录请求.我们先来看传统方法是如何解决这问题:

private void handleLogin2(LoginPost post) {
        ApiFactory.getBaseApi().login(post).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new BaseSubscriber>(this){
                    @Override
                    public void onNext(Result tokenResult) {
                        if (tokenResult.isOk()) {
                            Token data = tokenResult.getData();
                            String token = data.getToken();
                           
                                    .observeOn(AndroidSchedulers.mainThread())
                                    .subscribe(new BaseSubscriber>(LoginActivity.this){
                                        @Override
                                        public void onNext(Result userResult) {
                                            
                                        }

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

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

开始时,我们大部分人会写出类似以上的代码。当然,这实现了我们想要的逻辑,但当你仔细思考的时候,会发现几个问题:

  1. 回调嵌套看起来令人疑惑。由于我们在大多数情况下是线性思维,那么此时当你看到onNext(Result tokenResult)中又去嵌套处理获取用户信息的接口你的思维不得不跳跃一下。
  2. 登录功能的异常处理点被分隔了,使我们不得不写出冗余的代码。
  3. 多次线程开销好像可以被进一步优化。

实际上这三个问题的根本原因在于我们在实现登录功能的时候是以方法作为最小单位,而不是以登录逻辑为最小单位,因此看起不是那么的连贯。现在来看看我们应该怎么样让上面的代码具有连贯性:

private void handleLogin(LoginPost post) {
        ApiFactory.getBaseApi().login(post).flatMap(new Func1, Observable>>() {

            @Override
            public Observable> call(Result tokenResult) {
                if (tokenResult.isOk()) {
                    Token data = tokenResult.getData();
                    String token = data.getToken();
                    
                    return ApiFactory.getUserApi().getUserProfile(token);
                } else {
                    return Observable.error(new ApiException(tokenResult.getCode(), tokenResult.getMsg()));
                }
            }
        }).subscribeOn(Schedulers.io())
                .doOnSubscribe(new Action0() {
                    @Override
                    public void call() {
                        showWaitDialog();
                    }
                }).subscribeOn(AndroidSchedulers.mainThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new BaseSubscriber>(this) {
                    @Override
                    public void onCompleted() {
                    }

                    @Override
                    public void onNext(Result userResult) {
                        

                    }

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


    }

通过flatmap操作符,不但解决了接口依赖问题,而且使得代码逻辑相比之前更具有连贯性。另外,这里引入的BaseSubscriber在我们Retrofit响应数据及异常处理策略做过说明了,不明白的同学可以自行查阅。

场景二:接口合并(merge)

很多情况下,一个界面中需要的数据来自多个数据源(请求),而只有当所有的请求的响应数据都拿到之后才能渲染界面。

接口结果同类型

当前数据源来自多个渠道,拿到的结果属于同一类型的,比如有些数据需要从本地数据读取,而另一些数据则从网络中获取,但无论哪个数据源今最后返回的数据类型是一样的,比如:

 private Observable> getDataFromNet() {
        ArrayList list = new ArrayList<>();
        for(int i=0;i<10;i++) {="" list.add("data="" from="" net:"="" +="" i);="" }="" return="" Observable.just(list);="" private="" Observable> getDataFromDisk() {
        ArrayList list = new ArrayList<>();
        for(int i=0;i<10;i++) {="" list.add("data="" from="" disk:"="" +="" i);="" }="" return="" Observable.just(list);="" }<="" code=""/>

上面的两个方法分别从磁盘和网络中获取数据,且最后的数据类型都是ArrayList,现在我们来合并这两个接口:

   private void getData() {
        Observable.merge(getDataFromDisk(), getDataFromNet()).subscribe(new Subscriber>() {
            @Override
            public void onCompleted() {
                
            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(ArrayList list) {
                for (String s : list) {
                    Log.d("MainActivity", s);
                }
            }
        });

接口结果不同类型

有些情况下,不同数据源返回的结果类型不一致,那该如何解决呢?比如当前存在两个接口:

@GET("dict/locations")
Observable>> getLocationList();

@GET("user")
Observable> getUserInfo(@Query("id") String id);

只有当这两个请求都完成后才能更新UI,那我们该怎么做呢?同样还是使用merge操作符,关键在于如何区分响应:

    private void getData(String uid) {
        Observable>> locationOb = ApiFactory.getUserApi().getLocationList();
        Observable> userOb = ApiFactory.getUserApi().getUserInfo(uid);

        Observable.merge(locationOb,userOb).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber>() {
                    @Override
                    public void onCompleted() {
                        
                    }

                    @Override
                    public void onError(Throwable e) {

                    }

                    @Override
                    public void onNext(Result result) {
                        Object data = result.getData();
                        if(data instanceof User ){
                            
                        } else if (data instanceof ArrayList) {
                            
                        }
                    }
                });

    }

场景三:构建多级缓存(concat)

缓存机制想必是众所周知。这里我们就以常见的三级缓存机制为例:首先从内存中获取数据,如果内存中不存在,则从硬盘中获取数据,如果硬盘中不存在数据,则从网络中获取数据。现在看看RxJava是如何帮我们解决这个问题:

 
 private void getData(String url) {

        Observable.concat(getDataInMemory(url),getDataInDisk(url),getDataInNet(url)).takeFirst(new Func1() {
            @Override
            public Boolean call(Bitmap bitmap) {
                return bitmap!=null;
            }
        }).observeOn(AndroidSchedulers.mainThread()).subscribe(new Action1() {
            @Override
            public void call(Bitmap bitmap) {
                
            }
        });
    }

    
    private Observable getDataInMemory(final String url) {
        final Map memoryCache = new HashMap<>();
        
        

        return Observable.create(new Observable.OnSubscribe() {

            @Override
            public void call(Subscriber subscriber) {
                if (memoryCache.containsKey(url)) {
                    subscriber.onNext(memoryCache.get(url));
                }
                subscriber.onCompleted();
            }
        });
    }

    
    private Observable getDataInDisk(final String url) {
        final Map diskCache = new HashMap<>();
        
        

        return Observable.create(new Observable.OnSubscribe() {

            @Override
            public void call(Subscriber subscriber) {
                if (diskCache.containsKey(url)) {
                    subscriber.onNext(diskCache.get(url));
                }
                subscriber.onCompleted();
            }
        });

    }

    
    private Observable getDataInNet(final String url) {
        return Observable.create(new Observable.OnSubscribe(){

            @Override
            public void call(Subscriber subscriber) {
                Bitmap bitmap=null;
                

                subscriber.onNext(bitmap);
                subscriber.onCompleted();
            }
        }).subscribeOn(Schedulers.io());
    }

rxjava为我们提供的concat操作符可以很容的的实现多级缓存机制。这里需要记住在getData()方法中不要忘记使用takeFirst()。concat操作符接受多个Observable,并按其顺序串联,
在订阅的时候会返回所有Observable的数据(按顺序依次返回)。换言之,如果在getData()中不实用takeFirst(),将会并行的从内存,硬盘及网络中检索数据,这显然不是我们想要的。takeFirst操作符可以从返回的数据中取出第一个,并中断数据检索的过程。我们知道,检索速度:内存>硬盘>网络,这就意味着当我们从内存中获取到数据的时候就不会再从硬盘中获取数据,反之,则从硬盘中获取数据;当我们从硬盘中获取到数据的时候就不会再从网络中获取到数据了,反之,则从网络中获取。

这样就实现了我们的最终目标。

场景四:定时任务(timer)

在一些情况下我们需要执行定时任务,传统的做法上有两种方式可选择:Timer和SchelchExector。但是在引入rxjava之后,我们有了第三种选择:

    private void startTimerTask() {
        Observable.timer(2, TimeUnit.SECONDS).subscribe(new Action1() {
            @Override
            public void call(Long aLong) {
                Log.d("MainActivity", "start execute task:" + Thread.currentThread().getName());
            }
        });
    }

场景五:周期任务(interval)

当然rxjava通过interval提供了周期任务的支持:

 private void startIntervalTask() {
        Observable.interval(5, TimeUnit.SECONDS).subscribe(new Action1() {
            @Override
            public void call(Long aLong) {
                Log.d("MainActivity", "start task:" + Thread.currentThread().getName());
            }
        });
    }

场景六:数据过滤(filter)

在处理集合时,我们经常需要过滤操作,这时候使用filte操作符就非常有用,用个简单示例:

   private void dataFilter() {
        final HashSet hashSet = new HashSet<>();
        hashSet.add("1");
        hashSet.add("2");

        ArrayList list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("");

        Observable.from(list).filter(new Func1() {
            @Override
            public Boolean call(String s) {
                return !TextUtils.isEmpty("") && !hashSet.contains(s);
            }
        }).subscribe(new Action1() {
            @Override
            public void call(String s) {
                Log.d("MainActivity", "result: " + s);
            }
        });

    }

场景七:界面防抖动(throttleFirst)

所谓的界面防抖动就是用于处理快速点击某控件导致重复打开界面的操作,比如点击某个button可以打开一个Activity,正常情况下,我们一旦点击了该Button便会等待该Activity。在应用响应比较慢,用户以为无响应而多次点击Button或者恶意快速点击的情况下,会重复打开同一个Activity,当用户想要退出该Activity的时候体验会非常差。

通过rxjava提供的throttleFirst操作符我们能够很容易防止按钮在单位时间内被重复点击的问题:

RxView.clicks(mBtnTest2).throttleFirst(1L, TimeUnit.SECONDS).subscribe(new Action1() {
        @Override
        public void call(Void aVoid) {
            Toast.makeText(MainActivity.this, "button2 clicked", Toast.LENGTH_SHORT).show()
        }
    })

场景八:老接口适配(just)

当你在为老项目添加rxjava支持的时候,难免需要将一些方法返回类型转为Observable.通过just操作符不需要对原方法进行任何修改便可实现:

private int oldMethod(int x, int y) {
        return x+y;
    }

private void addTest() {
    Observable.just(oldMethod(4, 9)).subscribe(new Action1() {
        @Override
        public void call(Integer result) {
            Log.d("MainActivity", "result:" + result);
        }
    });
    }

场景十:响应式界面

界面元素更新

在信息填充界面时,我们经常会遇到只有填写完必要的信息之后,提交按钮才能被点击的情况。比如在登录界面时,只有我们填写完用户名和密码之后,登录按钮才能被点击。通过借助rxjava提供的combineLatest操作符我们可以容易的实现这种响应式界面

  EditText mEditUsername = (EditText) findViewById(R.id.editText3)
        EditText mEditPwd = (EditText) findViewById(R.id.editText4)
        final Button mBtnLogin = (Button) findViewById(R.id.button2)
        mBtnLogin.setEnabled(false)

        Observable usernameOb = RxTextView.textChanges(mEditUsername)
        Observable pwdOb = RxTextView.textChanges(mEditPwd)

        Observable.combineLatest(usernameOb, pwdOb, new Func2() {
            @Override
            public Boolean call(CharSequence username, CharSequence pwd) {

                return !TextUtils.isEmpty(username) && !TextUtils.isEmpty(pwd)
            }
        }).subscribe(new Action1() {
            @Override
            public void call(Boolean isLogin) {
                mBtnLogin.setEnabled(isLogin)
            }
        })

RxJava内存优化

内存优化

借助rxjava提供的线程调度器Scheduler我们可以很容的实现线程切换,目前Scheduler提供了一下几种调度策略:

  • Schedulers.immediate():默认的调度策略,不指定线程,也就是运行在当前线程
  • Schedulers。newThread():运行在一个新创建的线程当中,相当于new Thread()操作。
  • Schedulers.io():采用了线程池机制,内部维护了一个不限制线程数量的线程池,用于IO密集型操作。
  • Schedulers.computation():同样采用了线程池机制,只不过线程池中线程的数量取决与CPU的核数,以便实现最大性能。通常用于CPU密集型操作,比如图形处理。

通过上面的介绍,我们基本能做出以下使用规则:对于网络请求及读写大量本地数据等操作,既可以采用Schedulers.newThread()也可以采用Schedulers.io(),但是优先采用Schedulers.io(),对于计算量比较大的,当然是采用Schedulers.computation()。
这样,我们既能达到较好的性能,又尽可能的减少内存占用。

内存泄漏

尽管rxjava非常简单易用,但是随着订阅的增多内存开销也会随之增大,尤其是在配合使用网络请求的时候,稍不注意就容易造成内存泄漏。早期我也犯过多次这种错误。

当我们不需要的时候,主动取消订阅。比如在下面的代码中,我们开启一个周期任务用来不断的输出信息,那么我们需要在该Activity被销毁的时候调用mSubscription.unsubscribe()来主动的解除订阅关系防止内存泄漏。

public class MainActivity extends AppCompatActivity {

    private Subscription mSubscription;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button mBtnTest1 = (Button) findViewById(R.id.btn_test1);

        mBtnTest1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mSubscription = startIntervalTask();
            }
        });
    }

    private Subscription startIntervalTask() {
        return Observable.interval(5, TimeUnit.SECONDS).subscribe(new Action1() {
            @Override

            public void call(Long aLong) {
                Log.d("MainActivity", "start task:" + Thread.currentThread().getName());
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        
        if (mSubscription != null && !mSubscription.isUnsubscribed()) {
            mSubscription.unsubscribe();
        }
    }
}

看完上面简单的示例,想必你也明白rxjava所造成的内存泄漏往往和组件的生命周期相关。也就是我们要重点关注那些在在组件销毁之后,订阅关系却仍然存在的情况。大部分情况下,当我们的视图销毁之后,订阅关系就没有必要存在了,所以需要我们主动取消订阅即可。

存在一种特殊情况:当我们进入某个界面后,往往会发出网络请求,在返回数据后首先需要缓存数据,然后在更新界面视图。这种情况下当然不能在视图销毁后立刻解除订阅关系。那么这里需要注意的是更新UI之前需要自行判断当前视图是否存在,存在则更新,不存在就没有必要更新了。

在我们的工程中,往往存在很多个视图(Activity,Fragment等),如果在每个视图当中都要手动的解除订阅关系是件很繁琐的事情。这里有两种方式:一是在基类当中,比如BaseActivity,BaseFragment中统一取消订阅,另外一种方式就是使用RxLifeCycle这个库。

使用RxLifecycle

RxLifeCycle可以帮助我们在组件生命周期的某个阶段或者指定事件发生时自动取消订阅,其项目地址在:github.com/trello/RxLi…,如果你不介意引入新的库,建议使用。

compile 'com.trello:rxlifecycle:0.3.1'
compile 'com.trello:rxlifecycle-components:0.3.1'

RxLifeCycle主要提供了两个方法bindToLifecycle()和bindUntilEvent(),分别用来绑定生命周期和事件。

绑定生命周期(bindToLifecycle())

绑定生命周期的做法本质上是通过监听组件(Activity,Fragment)生命周期的变化来自动解除订阅关系。用法如下:


public class MainActivity extends RxAppCompatActivity {

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

    @Override
    protected void onResume() {
        super.onResume();
        
        
        
        startIntervalTask();
    }

    private Subscription startIntervalTask() {
        return Observable.interval(5, TimeUnit.SECONDS).compose(this.bindToLifecycle()).doOnUnsubscribe(new Action0() {
            @Override
            public void call() {
                Log.d("MainActivity", "解除订阅");
            }
        }).subscribe(new Action1() {
            @Override
            public void call(Long aLong) {
                Log.d("MainActivity", "start task:" + Thread.currentThread().getName());
            }
        });
    }

}

绑定生命周期的方式适应于在一开始就能够确定开始点,即能明确的知道订阅关系发生在哪个阶段(onCreate()?onResume(),onStart()),这样才能在恰当的方式解除订阅关系。比如在onResume()中联网获取用户数据,那么需要在onPause()中解除。

绑定事件(bindUntilEvent())

RxLifecycle中将组件生命周期的各个阶段转化为相对应的事件,因此绑定事件的方式和绑定生命周期的方式并无太大区别。和使用bindToLifecycle()不一样的是,绑定事件的方式只关心何时解除订阅关系。因为在很多情况下,我们所做的操作并不一定是在onResume()开始,在onPause()结束,此时显然不能用绑定生命周期的方法。来看看绑定事件如何使用:


public class MainActivity extends RxAppCompatActivity {

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

        Button mBtnTest1 = (Button) findViewById(R.id.btn_test1);

              mBtnTest1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                
                startIntervalTask();
    }


    
    
    private Subscription startIntervalTask() {
        return Observable.interval(5, TimeUnit.SECONDS).compose(this.bindUntilEvent(ActivityEvent.PAUSE)).doOnUnsubscribe(new Action0() {
            @Override
            public void call() {
                Log.d("MainActivity", "解除订阅");
            }
        }).subscribe(new Action1() {
            @Override
            public void call(Object o) {
                Log.d("MainActivity", "start task");
            }
        });
    }

}

总结

这里介绍了有关rxjava一些实际应用场景。尽管rxjava看起来非常容易 使用,但其内存使用问题需要我们重点关注。