rxjava真实应用场景
在上一篇文章中为解决异常问题,我们引入了rxjava的支持。接下来我们来看看rxjava在实际工程中的显著应用。
场景零:线程切换
rxjava引入让使得线程切换更加的容易,几行代码就可以搞定。RxAnroid的引入更是让我们非常容易的能够切换到UI线程。可以说,引入RxJava,就放弃古老而沉重的AsyncTask吧(初学者还是要学AsyncTask的)。最典型的就是从网络中获取数据,然后在更新界面,很显然获取数据操作需要发生在子线程,更新UI操作发声明在主线程。这里我们以模拟从数据库中获取联系人操作为例:
private void getConcactFromDB() { Observable.create(new Observable.OnSubscribe<List<String>>() { @Override public void call(Subscriber<? super List<String>> subscriber) { //模拟从数据库中获取数据 ArrayList<String> 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<List<String>>() { @Override public void call(List<String> 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<Result<Token>>(this) { @Override public void onNext(Result<Token> tokenResult) { if (tokenResult.isOk()) { Token data = tokenResult.getData(); String token = data.getToken(); //保存token操作 ApiFactory.getUserApi().getUserProfile(token).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new BaseSubscriber<Result<User>>(LoginActivity.this) { @Override public void onNext(Result<User> userResult) { //处理用户信息 } @Override public void onError(Throwable e) { //处理错误 } }); } } @Override public void onError(Throwable e) { //处理错误 } }); }
开始时,我们大部分人会写出类似以上的代码。当然,这实现了我们想要的逻辑,但当你仔细思考的时候,会发现几个问题:
- 回调嵌套看起来令人疑惑。由于我们在大多数情况下是线性思维,那么此时当你看到
onNext(Result<Token> tokenResult)
中又去嵌套处理获取用户信息的接口你的思维不得不跳跃一下。 - 登录功能的异常处理点被分隔了,使我们不得不写出冗余的代码。
- 多次线程开销好像可以被进一步优化。
实际上这三个问题的根本原因在于我们在实现登录功能的时候是以方法作为最小单位,而不是以登录逻辑为最小单位,因此看起不是那么的连贯。现在来看看我们应该怎么样让上面的代码具有连贯性:
private void handleLogin(LoginPost post) { ApiFactory.getBaseApi().login(post).flatMap(new Func1<Result<Token>, Observable<Result<User>>>() { @Override public Observable<Result<User>> call(Result<Token> tokenResult) { if (tokenResult.isOk()) {//获取token成功 Token data = tokenResult.getData(); String token = data.getToken(); //保存token操作 return ApiFactory.getUserApi().getUserProfile(token);//获取用户信息 } else {//获取token,直接触发onError()方法 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<Result<User>>(this) { @Override public void onCompleted() { } @Override public void onNext(Result<User> userResult) { //处理用户信息 } @Override public void onError(Throwable e) { //处理错误 } }); }
通过flatmap操作符,不但解决了接口依赖问题,而且使得代码逻辑相比之前更具有连贯性。另外,这里引入的BaseSubscriber在我们Retrofit响应数据及异常处理策略做过说明了,不明白的同学可以自行查阅。
场景二:接口合并(merge)
很多情况下,一个界面中需要的数据来自多个数据源(请求),而只有当所有的请求的响应数据都拿到之后才能渲染界面。
接口结果同类型
当前数据源来自多个渠道,拿到的结果属于同一类型的,比如有些数据需要从本地数据读取,而另一些数据则从网络中获取,但无论哪个数据源今最后返回的数据类型是一样的,比如:
private Observable<ArrayList<String>> getDataFromNet() { ArrayList<String> list = new ArrayList<>(); for(int i=0;i<10;i++) { list.add("data from net:" + i); } return Observable.just(list); } private Observable<ArrayList<String>> getDataFromDisk() { ArrayList<String> list = new ArrayList<>(); for(int i=0;i<10;i++) { list.add("data from disk:" + i); } return Observable.just(list); }
上面的两个方法分别从磁盘和网络中获取数据,且最后的数据类型都是ArrayList<String>
,现在我们来合并这两个接口:
private void getData() { Observable.merge(getDataFromDisk(), getDataFromNet()).subscribe(new Subscriber<ArrayList<String>>() { @Override public void onCompleted() { //更新界面 } @Override public void onError(Throwable e) { } @Override public void onNext(ArrayList<String> list) { for (String s : list) { Log.d("MainActivity", s); } } });
接口结果不同类型
有些情况下,不同数据源返回的结果类型不一致,那该如何解决呢?比如当前存在两个接口:
@GET("dict/locations") Observable<Result<ArrayList<String>>> getLocationList(); @GET("user") Observable<Result<User>> getUserInfo(@Query("id") String id);
只有当这两个请求都完成后才能更新UI,那我们该怎么做呢?同样还是使用merge操作符,关键在于如何区分响应:
private void getData(String uid) { Observable<Result<ArrayList<String>>> locationOb = ApiFactory.getUserApi().getLocationList(); Observable<Result<User>> userOb = ApiFactory.getUserApi().getUserInfo(uid); Observable.merge(locationOb,userOb).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<Result<? extends Object>>() { @Override public void onCompleted() { //更新UI } @Override public void onError(Throwable e) { } @Override public void onNext(Result<? extends Object> 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<Bitmap, Boolean>() { @Override public Boolean call(Bitmap bitmap) { return bitmap!=null; } }).observeOn(AndroidSchedulers.mainThread()).subscribe(new Action1<Bitmap>() { @Override public void call(Bitmap bitmap) { //处理图片 } }); } //从内存中获取 private Observable<Bitmap> getDataInMemory(final String url) { final Map<String, Bitmap> memoryCache = new HashMap<>(); //模拟内存中的数据 //... return Observable.create(new Observable.OnSubscribe<Bitmap>() { @Override public void call(Subscriber<? super Bitmap> subscriber) { if (memoryCache.containsKey(url)) { subscriber.onNext(memoryCache.get(url)); } subscriber.onCompleted(); } }); } //从硬盘中获取 private Observable<Bitmap> getDataInDisk(final String url) { final Map<String, Bitmap> diskCache = new HashMap<>(); //模拟内存中的数据 //... return Observable.create(new Observable.OnSubscribe<Bitmap>() { @Override public void call(Subscriber<? super Bitmap> subscriber) { if (diskCache.containsKey(url)) { subscriber.onNext(diskCache.get(url)); } subscriber.onCompleted(); } }); } //从网络中获取 private Observable<Bitmap> getDataInNet(final String url) { return Observable.create(new Observable.OnSubscribe<Bitmap>(){ @Override public void call(Subscriber<? super Bitmap> subscriber) { Bitmap bitmap=null; //从网络获取图片bitmap 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<Long>() { @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<Long>() { @Override public void call(Long aLong) { Log.d("MainActivity", "start task:" + Thread.currentThread().getName()); } }); }
场景六:数据过滤(filter)
在处理集合时,我们经常需要过滤操作,这时候使用filte操作符就非常有用,用个简单示例:
private void dataFilter() { final HashSet<String> hashSet = new HashSet<>(); hashSet.add("1"); hashSet.add("2"); ArrayList<String> list = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); list.add(""); Observable.from(list).filter(new Func1<String, Boolean>() { @Override public Boolean call(String s) { return !TextUtils.isEmpty("") && !hashSet.contains(s); } }).subscribe(new Action1<String>() { @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<Void>() { @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<Integer>() { @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<CharSequence> usernameOb = RxTextView.textChanges(mEditUsername); Observable<CharSequence> pwdOb = RxTextView.textChanges(mEditPwd); Observable.combineLatest(usernameOb, pwdOb, new Func2<CharSequence, CharSequence, Boolean>() { @Override public Boolean call(CharSequence username, CharSequence pwd) { return !TextUtils.isEmpty(username) && !TextUtils.isEmpty(pwd); } }).subscribe(new Action1<Boolean>() { @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<Long>() { @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这个库。
总结
这里介绍了有关rxjava一些实际应用场景。尽管rxjava看起来非常容易 使用,但其内存使用问题需要我们重点关注。
本文转自:http://blog.csdn.net/dd864140130/article/details/52714272