RxJava详解(一)

2018-07-31  本文已影响0人  CharonChui

RxJava详解(一)

年初的时候就想学习下RxJava然后写一些RxJava的教程,无奈发现已经年底了,然而我还么有写。今天有点时间,特别是发布了RxJava 2.0后,我决定动笔开始。

现在RxJava变的越来越流行了,很多项目中都使用了它。特别是大神JakeWharton等的加入,以及RxBinding、Retrofit、RxLifecycle等众多项目的,然开发越来越方便,但是上手比较难,不过一旦你入门后你就会发现真是太棒了。

在介绍RxJava之前,感觉有必要说一下什么是函数响应式编程(FRP)?

函数响应式编程(FRP)为解决现代编程问题提供了全新的视角。一旦理解它,可以极大地简化你的项目,特别是处理嵌套回调的异步事件,复杂的列表过滤和变换,或者时间相关问题,而且RxJava是响应式编程的一个具体实现。

这里以一个真实的例子来开始讲解函数响应式编程怎么提高我们代码的可读性。我们的任务是通过查询GitHubAPI, 首先获取用户列表,然后请求每个用户的详细信息。这个过程包括两个web服务端点:
https://api.github.com/users-获取用户列表;https://api.github.com/users/{username}-获取特定用户的详细信息,例如https://api.github.com/users/mutexkid

普通情况下是这样写的:

//The "Nested Callbacks" Way
public void fetchUserDetails() {
    //first, request the users...
    mService.requestUsers(new Callback<GithubUsersResponse>() {
        @Override
        public void success(final GithubUsersResponse githubUsersResponse,
                            final Response response) {
            Timber.i(TAG, "Request Users request completed");
            final synchronized List<GithubUserDetail> githubUserDetails = new ArrayList<GithubUserDetail>();
            //next, loop over each item in the response
            for (GithubUserDetail githubUserDetail : githubUsersResponse) {
                //request a detail object for that user
                mService.requestUserDetails(githubUserDetail.mLogin,
                                            new Callback<GithubUserDetail>() {
                    @Override
                    public void success(GithubUserDetail githubUserDetail,
                                        Response response) {
                        Log.i("User Detail request completed for user : " + githubUserDetail.mLogin);
                        githubUserDetails.add(githubUserDetail);
                        if (githubUserDetails.size() == githubUsersResponse.mGithubUsers.size()) {
                            //we've downloaded'em all - notify all who are interested!
                            mBus.post(new UserDetailsLoadedCompleteEvent(githubUserDetails));
                        }
                    }

                    @Override
                    public void failure(RetrofitError error) {
                        Log.e(TAG, "Request User Detail Failed!!!!", error);
                    }
                });
            }
        }

        @Override
        public void failure(RetrofitError error) {
            Log.e(TAG, "Request User Failed!!!!", error);
        }
    });
}

尽管这不是最差的代码-至少它是异步的,因此在等待每个请求完成的时候不会阻塞-但由于代码复杂(增加更多层次的回调代码复杂度将呈指数级增长)因此远非理想的代码。
当我们不可避免要修改代码时(在前面的web service调用中,我们依赖前一次的回调状态,因此它不适用于模块化或者修改要传递给下一个回调的数据)也远非容易的工作。
我们亲切的称这种情况为“回调地狱”。

而通过RxJava的方式:

public void rxFetchUserDetails() {
    //request the users
    mService.rxRequestUsers().concatMap(Observable::from)
    .concatMap((GithubUser githubUser) ->
                    //request the details for each user
                    mService.rxRequestUserDetails(githubUser.mLogin)
    )
    //accumulate them as a list
    .toList()
    //define which threads information will be passed on
    .subscribeOn(Schedulers.newThread())
    .observeOn(AndroidSchedulers.mainThread())
    //post them on an eventbus
    .subscribe(githubUserDetails -> {
        EventBus.getDefault().post(new UserDetailsLoadedCompleteEvent(githubUserDetails));
    });
}

如你所见,使用函数响应式编程模型我们完全摆脱了回调,并最终得到了更短小的程序。让我们从函数响应式编程的基本定义开始慢慢解释到底发生了什么,并逐渐理解上面的代码,这些代码托管在GitHub上面。

从根本上讲,函数响应式编程是在观察者模式的基础上,增加对Observables发送的数据流进行操纵和变换的功能。

RxJava简介

在介绍RxJava之前先说一下RxRx的全称是Reactive Extensions,直译过来就是响应式扩展。

rx_logo.png

Rx基于观察者模式,它是一种编程模型,目标是提供一致的编程接口,帮助开发者更方便的处理异步数据流。ReactiveX.io给的定义是,Rx是一个使用可观察数据流进行异步编程的编程接口,ReactiveX结合了观察者模式、迭代器模式和函数式编程的精华。Rx已经渗透到了各个语言中,有了Rx所以才有了RxJavaRx.NETRxJSRxSwiftRx.rbRxPHP等等,

这里先列举一下相关的官网:

RxJavaGitHub上的介绍是:a library for composing asynchronous and event-based programs by using observable sequences for the Java VM.
翻译过来也就是一个基于事件和程序在Java VM上使用可观测的序列来组成异步的库。RxJava的本质就是一个实现异步操作的库,它的优势就是简洁,随着程序逻辑变得越来越复杂,它依然能够保持简洁。

其实一句话总结一下RxJava的作用就是:异步

这里可能会有人想不就是个异步吗,至于辣么矫情么?用AsyncTaskHandler甚至自定义一个BigAsyncTask分分钟搞定。

但是RxJava的好处是简洁。异步操作很关键的一点是程序的简洁性,因为在调度过程比较复杂的情况下,异步代码经常会既难写也难被读懂。 Android创造的AsyncTaskHandler其实都是为了让异步代码更加简洁。虽然RxJava的优势也是简洁,但它的简洁的与众不同之处在于,随着程序逻辑变得越来越复杂,它依然能够保持简洁。

扩展的观察者模式

RxJava的异步实现,是通过一种扩展的观察者模式来实现的。

观察者模式面向的需求是:A对象(观察者)对B对象(被观察者)的某种变化高度敏感,需要在B变化的一瞬间做出反应。举个例子,新闻里喜闻乐见的警察抓小偷,警察需要在小偷伸手作案的时候实施抓捕。在这个例子里,警察是观察者,小偷是被观察者,警察需要时刻盯着小偷的一举一动,才能保证不会漏过任何瞬间。程序的观察者模式和这种真正的『观察』略有不同,观察者不需要时刻盯着被观察者(例如A不需要每过2ms就检查一次B的状态),而是采用注册(Register)或者称为订阅(Subscribe)的方式,告诉被观察者:我需要你的某某状态,你要在它变化的时候通知我。 Android开发中一个比较典型的例子是点击监听器OnClickListener。对设置OnClickListener来说,View是被观察者,OnClickListener是观察者,二者通过 setOnClickListener()方法达成订阅关系。订阅之后用户点击按钮的瞬间,Android Framework就会将点击事件发送给已经注册的OnClickListener。采取这样被动的观察方式,既省去了反复检索状态的资源消耗,也能够得到最高的反馈速度。当然,这也得益于我们可以随意定制自己程序中的观察者和被观察者,而警察叔叔明显无法要求小偷『你在作案的时候务必通知我』。

OnClickListener的模式大致如下图:

btn_onclick.jpg

如图所示,通过setOnClickListener()方法,Button持有OnClickListener的引用(这一过程没有在图上画出);当用户点击时,Button自动调用OnClickListeneronClick() 方法。另外,如果把这张图中的概念抽象出来(Button -> 被观察者、OnClickListener -> 观察者、setOnClickListener() -> 订阅,onClick() -> 事件),就由专用的观察者模式(例如只用于监听控件点击)转变成了通用的观察者模式。如下图:

btn_rxjava_observable.jpg

RxJava作为一个工具库,使用的就是通用形式的观察者模式。

RxJava的观察者模式

RxJava的基本概念:

与传统观察者模式不同,RxJava的事件回调方法除了普通事件onNext()(相当于onClick()/onEvent())之外,还定义了两个特殊的事件:onCompleted()onError():
但是RxJava与传统的观察者设计模式有一点明显不同,那就是如果一个Observerble没有任何的的Subscriber,那么这个Observable是不会发出任何事件的。

RxJava的观察者模式大致如下图:

rxjava_observer_1.jpg

基本实现

基于上面的概念, RxJava的基本实现主要有三点:

RxJava入门示例

一个Observable可以发出零个或者多个事件,知道结束或者出错。每发出一个事件,就会调用它的SubscriberonNext方法,最后调用Subscriber.onComplete()或者Subscriber.onError()结束。

Hello World

compile 'io.reactivex:rxandroid:1.2.1'
// Because RxAndroid releases are few and far between, it is recommended you also
// explicitly depend on RxJava's latest version for bug fixes and new features.
compile 'io.reactivex:rxjava:1.2.3'
// 创建被观察者、数据源
Observable<String> observable = Observable.create(new Observable.OnSubscribe<String>() {
    @Override
    public void call(Subscriber<? super String> subscriber) {
        // 可以看到,这里传入了一个 OnSubscribe 对象作为参数。OnSubscribe 会被存储在返回的 Observable 对象中,它的作用相当于一个计划表,当 Observable      
        //被订阅的时候,OnSubscribe 的 call() 方法会自动被调用,事件序列就会依照设定依次触发(对于上面的代码,就是观察者Subscriber 将会被调用三次 onNext() 和一次 
        // onCompleted())。这样,由被观察者调用了观察者的回调方法,就实现了由被观察者向观察者的事件传递,即观察者模式。
        Log.i("@@@", "call");
        subscriber.onNext("Hello ");
        subscriber.onNext("World !");
        subscriber.onCompleted();
    }
});
// 创建观察者
Subscriber<String> subscriber = new Subscriber<String>() {
    @Override
    public void onCompleted() {
        Log.i("@@@", "onCompleted");
    }

    @Override
    public void onError(Throwable e) {
        Log.i("@@@", "onError");
    }

    @Override
    public void onNext(String s) {
        Log.i("@@@", "onNext : " + s);
    }
};
// 关联或者叫订阅更合适。
observable.subscribe(subscriber);

一旦subscriber订阅了observableobservable就会调用subscriber对象的onNextonComplete方法,subscriber就会打印出Hello World.

Observable.subscribe(Subscriber)的内部实现是这样的(仅核心代码):

// 注意:这不是`subscribe()`的源码,而是将源码中与性能、兼容性、扩展性有关的代码剔除后的核心代码。
public Subscription subscribe(Subscriber subscriber) {
   subscriber.onStart();
   onSubscribe.call(subscriber);
   return subscriber;
}

可以看到subscriber()做了3件事:

整个过程中对象间的关系如下图:

rxjava_observable_list.gif

讲到这里很多人肯定会骂傻X,这TM简洁你妹啊...,这里只是个入门Hello World,真正的简洁等你看完全部介绍后就明白了。

RxJava内置了很多简化创建Observable对象的函数,比如Observable.just()就是用来创建只发出一个事件就结束的Observable对象,上面创建Observable对象的代码可以简化为一行

Observable<String> observable = Observable.just("Hello ", "World !");

接下来看看如何简化Subscriber,上面的例子中,我们其实并不关心onComplete()onError,我们只需要在onNext的时候做一些处理,这时候就可以使用Action1类。

Action1<String> action1 = new Action1<String>() {
    @Override
    public void call(String s) {
        Log.i("@@@", "call : " + s);
    }
};

Observable.subscribe()方法有一个重载版本,接受三个Action1类型的参数

rxjava_subscribe_params.png

所以上面的代码最终可以写成这样:

Observable.just("Hello ", "World !").subscribe(new Action1<String>() {
    @Override
    public void call(String s) {
        Log.i("@@@", "call : " + s);
    }
});

这里顺便多提一些subscribe()的多个Action参数:

Action1<String> onNextAction = new Action1<String>() {
    // onNext()
    @Override
    public void call(String s) {
        Log.d(tag, s);
    }
};
Action1<Throwable> onErrorAction = new Action1<Throwable>() {
    // onError()
    @Override
    public void call(Throwable throwable) {
        // Error handling
    }
};
Action0 onCompletedAction = new Action0() {
    // onCompleted()
    @Override
    public void call() {
        Log.d(tag, "completed");
    }
};

observable.subscribe(onNextAction, onErrorAction, onCompletedAction);

简单解释一下这段代码中出现的Action1Action0Action0RxJava的一个接口,它只有一个方法call(),这个方法是无参无返回值的;由于onCompleted() 方法也是无参无返回值的,因此Action0可以被当成一个包装对象,将onCompleted()的内容打包起来将自己作为一个参数传入subscribe()以实现不完整定义的回调。这样其实也可以看做将 onCompleted()方法作为参数传进了subscribe(),相当于其他某些语言中的『闭包』。Action1也是一个接口,它同样只有一个方法call(T param),这个方法也无返回值,但有一个参数;与Action0同理,由于onNext(T obj)onError(Throwable error)也是单参数无返回值的,因此Action1可以将onNext(obj)onError(error)打包起来传入subscribe()以实现不完整定义的回调。事实上,虽然Action0Action1API中使用最广泛,但RxJava是提供了多个ActionX形式的接口(例如Action2, Action3)的,它们可以被用以包装不同的无返回值的方法。

假设我们的Observable是第三方提供的,它提供了大量的用户数据给我们,而我们需要从用户数据中筛选部分有用的信息,那我们该怎么办呢?
Observable中去修改肯定是不现实的?那从Subscriber中进行修改呢? 这样好像是可以完成的。但是这种方式并不好,因为我们希望Subscriber越轻量越好,因为很有可能我们需要
在主线程中去执行Subscriber。另外,根据响应式函数编程的概念,Subscribers更应该做的事情是响应,响应Observable发出的事件,而不是去修改。
那该怎么办呢? 这就要用到下面的部分要讲的操作符。

接口变化

RxJava 2.x拥有了新的特性,其依赖于4个基础接口,它们分别是:

其中用的比较多的自然是PublisherFlowable,它支持背压(backpressure)。关于背压给个简洁的定义就是:

背压是指在异步场景中,被观察者发送事件速度远快于观察者的处理速度的情况下,一种告诉上游的被观察者降低发送速度的策略。

简而言之,背压是流速控制的一种策略。
其实RxJava 2.x最大的改动就是对于backpressure的处理,为此将原来的Observable拆分成了新的ObservableFlowable,同时其他相关部分也同时进行了拆分。

rxjava1vs2.png

RxJava 2.x中,Observable用于订阅Observer,不再支持背压(1.x中可以使用背压策略),而Flowable用于订阅Subscriber,是支持背压的。

操作符(Operators)

RxJava提供了对事件序列进行变换的支持,这是它的核心功能之一.所谓变换,就是将事件序列中的对象或整个序列进行加工处理,转换成不同的事件或事件序列。
操作符就是为了解决对Observable对象的变换的问题,操作符用于在Observable和最终的Subscriber之间修改Observable发出的事件。RxJava提供了很多很有用的操作符。
比如map操作符,就是用来把把一个事件转换为另一个事件的。

map

Returns an Observable that applies a specified function to each item emitted by the source Observable and emits the results of these function applications.

Observable<String> just = Observable.just("Hello ", "World !");
Observable<String> map = just.map(new Func1<String, String>() {
    @Override
    public String call(String s) {
        return s + "@@@";
    }
});
map.subscribe(new Action1<String>() {
    @Override
    public void call(String s) {
        Log.i("@@@", s);
    }
});

上面的代码打印出的结果是:

12-12 15:51:22.184 472-472/com.charon.rxjavastudydemo I/@@@: Hello @@@
12-12 15:51:22.184 472-472/com.charon.rxjavastudydemo I/@@@: World !@@@

map()操作符就是用于变换Observable对象的,map操作符返回一个Observable对象,这样就可以实现链式调用,在一个Observable对象上多次使用map操作符,最终将最简洁的数据传递给Subscriber对象。

map操作符更有趣的一点是它不必返回Observable对象返回的类型,你可以使用map操作符返回一个发出新的数据类型的Observable对象。
比如上面的例子中,Subscriber并不关心返回的字符串,而是想要字符串的hash值。

Observable<String> just = Observable.just("Hello ", "World !");
Observable<Integer> map = just.map(new Func1<String, Integer>() {
    @Override
    public Integer call(String s) {
        return s.hashCode();
    }
});
map.subscribe(new Action1<Integer>() {
    @Override
    public void call(Integer integer) {
        Log.i("@@@", "" + integer);
    }
});

上面部分的打印结果是:

12-12 15:54:35.515 8521-8521/com.charon.rxjavastudydemo I/@@@: -2137068114
12-12 15:54:35.516 8521-8521/com.charon.rxjavastudydemo I/@@@: -1105126669

map()的示意图:

rxjava_map.jpg

通过上面的部分我们可以得知:

flatmap

Returns an Observable that emits items based on applying a function that you supply to each item emitted by the source Observable, where that function returns an Observable, and then merging those resulting Observables and emitting the results of this merger.

flatMap()是一个很有用但非常难理解的变换,首先假设这么一种需求:假设有一个数据结构『学生』,现在需要打印出一组学生的名字。实现方式很简单:

Student[] students = ...;
Subscriber<String> subscriber = new Subscriber<String>() {
    @Override
    public void onNext(String name) {
        Log.d(tag, name);
    }
    ...
};
Observable.from(students)
    .map(new Func1<Student, String>() {
        @Override
        public String call(Student student) {
            return student.getName();
        }
    })
    .subscribe(subscriber);

很简单。那么再假设:如果要打印出每个学生所需要修的所有课程的名称呢?(需求的区别在于,每个学生只有一个名字,但却有多个课程)首先可以这样实现:

Student[] students = ...;
Subscriber<Student> subscriber = new Subscriber<Student>() {
    @Override
    public void onNext(Student student) {
        List<Course> courses = student.getCourses();
        for (int i = 0; i < courses.size(); i++) {
            Course course = courses.get(i);
            Log.d(tag, course.getName());
        }
    }
    ...
};
Observable.from(students)
    .subscribe(subscriber);

依然很简单。那么如果我不想在Subscriber中使用for循环,而是希望Subscriber中直接传入单个的Course对象呢(这对于代码复用很重要),用map()显然是不行的,因为map() 是一对一的转化,而我现在的要求是一对多的转化。那怎么才能把一个Student转化成多个Course呢?

这个时候,就需要用flatMap()了:

Student[] students = ...;
Subscriber<Course> subscriber = new Subscriber<Course>() {
    @Override
    public void onNext(Course course) {
        Log.d(tag, course.getName());
    }
    ...
};
Observable.from(students)
    .flatMap(new Func1<Student, Observable<Course>>() {
        @Override
        public Observable<Course> call(Student student) {
            return Observable.from(student.getCourses());
        }
    })
    .subscribe(subscriber);

mapflatmap在功能上是一致的,它也是把传入的参数转化之后返回另一个对象。区别在于flatmap是通过中间Observable来进行,而map是直接执行.flatMap()中返回的是个 Observable对象,并且这个Observable对象并不是被直接发送到了Subscriber的回调方法中。

flatMap()的原理是这样的:

这三个步骤,把事件拆成了两级,通过一组新创建的Observable将初始的对象『铺平』之后通过统一路径分发了下去。而这个『铺平』就是flatMap()所谓的flat

flatMap()就是根据你的规则,将Observable转换之后再发射出去,注意最后的顺序很可能是错乱的,如果要保证顺序的一致性,要使用concatMap()
由于可以在嵌套的Observable中添加异步代码flatMap()也常用于嵌套的异步操作,例如嵌套的网络请求(Retrofit + RxJava)

flatMap()示意图:

rxjava_flatmap.jpg

throttleFirst()

在每次事件触发后的一定时间间隔内丢弃新的事件。常用作去抖动过滤。
例如按钮的点击监听器:

RxView.clickEvents(button); // RxBinding 代码`
    .throttleFirst(500, TimeUnit.MILLISECONDS) // 设置防抖间隔为 500ms 
    .subscribe(subscriber); // 妈妈再也不怕我的用户手抖点开两个重复的界面啦。

from

convert various other objects and data types into Observables

from()接收一个集合作为输入,然后每次输出一个元素给subscriber.

List<String> s = Arrays.asList("Java", "Android", "Ruby", "Ios", "Swift");
Observable.from(s).subscribe(new Action1<String>() {
    @Override
    public void call(String s) {
        Log.i("@@@", s);
    }
});

filter

返回满足过滤条件的数据。

Observable.from(new Integer[]{1, 2, 3, 4, 5, 6, 7, 8, 9})
                .filter(new Func1<Integer, Boolean>() {
                    @Override
                    public Boolean call(Integer integer) {
                        return integer < 5;
                    }
                })
                .subscribe(new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {
                        Log.i("@@@", "integer=" + integer); //1,2,3,4
                    }
                });

timer

Timer会在指定时间后发射一个数字0,该操作符运行在Computation Scheduler

Observable.timer(3, TimeUnit.SECONDS).observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<Long>() {
                    @Override
                    public void call(Long aLong) {
                        Log.i("@@@", "aLong=" + aLong); // 延时3s
                    }
                });

interval

创建一个按固定时间间隔发射整数序列的Observable.
interval默认在computation调度器上执行。

Observable.interval(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread())
        .subscribe(new Action1<Long>() {
            @Override
            public void call(Long aLong) {
                Log.i("@@@", "aLong=" + aLong); //从0递增,间隔1s 0,1,2,3....
            }
        });

Repeat

重复执行

doOnNext

其实觉得doOnNext应该不算一个操作符,但考虑到其常用性,我们还是咬咬牙将它放在了这里。它的作用是让订阅者在接收到数据之前干点有意思的事情。假如我们在获取到数据之前想先保存一下它,无疑我们可以这样实现。

distinct

这个操作符非常的简单、通俗、易懂,就是简单的去重

take

take,接受一个long型参数count,代表至多接收count个数据。

等等...就不继续介绍了,到时候查下文档就好了。

是不是感觉没什么卵用,也稀里糊涂的,下面用一个网络请求的例子:

很多时候我们在使用RxJava的时候总是和Retrofit进行结合使用,而为了方便演示,这里我们就暂且采用OkHttp3进行演示,配合mapdoOnNext,线程切换进行简单的网络请求:

Observable.create(new ObservableOnSubscribe<Response>() {
    @Override
    public void subscribe(@NonNull ObservableEmitter<Response> e) throws Exception {
        Builder builder = new Builder()
                .url(mUrl)
                .get();
        Request request = builder.build();
        Call call = new OkHttpClient().newCall(request);
        Response response = call.execute();
        e.onNext(response);
    }
}).map(new Function<Response, MobileAddress>() {
    @Override
    public MobileAddress apply(@NonNull Response response) throws Exception {

        Log.e(TAG, "map 线程:" + Thread.currentThread().getName() + "\n");
        if (response.isSuccessful()) {
            ResponseBody body = response.body();
            if (body != null) {
                Log.e(TAG, "map:转换前:" + response.body());
                return new Gson().fromJson(body.string(), MobileAddress.class);
            }
        }
        return null;
    }
}).observeOn(AndroidSchedulers.mainThread())
    .doOnNext(new Consumer<MobileAddress>() {
        @Override
        public void accept(@NonNull MobileAddress s) throws Exception {
            Log.e(TAG, "doOnNext 线程:" + Thread.currentThread().getName() + "\n");
            mRxOperatorsText.append("\ndoOnNext 线程:" + Thread.currentThread().getName() + "\n");
            Log.e(TAG, "doOnNext: 保存成功:" + s.toString() + "\n");
            mRxOperatorsText.append("doOnNext: 保存成功:" + s.toString() + "\n");

        }
    }).subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Consumer<MobileAddress>() {
            @Override
            public void accept(@NonNull MobileAddress data) throws Exception {
                Log.e(TAG, "subscribe 线程:" + Thread.currentThread().getName() + "\n");
                mRxOperatorsText.append("\nsubscribe 线程:" + Thread.currentThread().getName() + "\n");
                Log.e(TAG, "成功:" + data.toString() + "\n");
                mRxOperatorsText.append("成功:" + data.toString() + "\n");
            }
        }, new Consumer<Throwable>() {
            @Override
            public void accept(@NonNull Throwable throwable) throws Exception {
                Log.e(TAG, "subscribe 线程:" + Thread.currentThread().getName() + "\n");
                mRxOperatorsText.append("\nsubscribe 线程:" + Thread.currentThread().getName() + "\n");

                Log.e(TAG, "失败:" + throwable.getMessage() + "\n");
                mRxOperatorsText.append("失败:" + throwable.getMessage() + "\n");
            }
        });

更多内容请看下一篇文章RxJava详解(二)

参考:


上一篇下一篇

猜你喜欢

热点阅读