RxJava 是如何實現執行緒切換的(上)

IAM四十二發表於2018-01-23

前言

通過前一篇的從觀察者模式出發,聊聊RxJava,我們大致理解了RxJava的實現原理,在RxJava中可以非常方便的實現不同執行緒間的切換。subscribeOn 用於指定上游執行緒,observeOn 用於指定下游執行緒,多次用 subscribeOn 指定上游執行緒只有第一次有效,多次用 observeOn 指定下次執行緒,每次都有效;簡直太方便了,比直接使用Handler省了不少力氣,同時也不用去關注記憶體洩漏的問題了。本篇就來看看在RxJava中上游是如何實現執行緒切換。

RxJava 基礎原理

為了方便後面的敘述,這裡通過下面的UML圖簡單回顧一下上一篇的內容。

RxJava 是如何實現執行緒切換的(上)

此圖並沒有完整的展現圖中各個介面和類之間的各種關係,因為那樣會導致整個圖錯綜複雜,不便於檢視,這裡只繪製出了RxJava各個類之間核心關係網路

從上面的UML圖中可以看出,具體的實現類只有ObservableCreate和CreateEmitter。CreateEmitter是ObservableCreate的內部類(PlantUML 怎麼繪製內部類,沒搞懂,玩的轉的同學請賜教呀(^▽^))。

上篇說過Observable建立的過程,可以簡化如下:

  Observable mObservable=new ObservableCreate(new ObservableOnSubscribe())
複製程式碼

結合圖可以更直觀的體現出這一點。ObservableCreate 內部持有ObservableOnSubscribe的引用。

當觀察者訂閱主題後:

mObservable.subscribe(mObserver);
複製程式碼

ObservableCreate 中的subscribeActual()方法就會執行,

    protected void subscribeActual(Observer<? super T> observer) {
        CreateEmitter<T> parent = new CreateEmitter<T>(observer);
        observer.onSubscribe(parent);

        try {
            source.subscribe(parent);
        } catch (Throwable ex) {
            Exceptions.throwIfFatal(ex);
            parent.onError(ex);
        }
    }

複製程式碼

在這個過程中會建立CreateEmitter 的例項,而這個CreateEmitter實現了Emitter和Disposable介面,同時又持有Observer的引用(當然這個引用是ObservableCreate傳遞給他的)。接著就會執行ObservableOnSubscribe的subscribe 方法,方法的引數即為剛剛建立的CreateEmitter 的例項,接著一系列連鎖反應,Emitter 介面中的方法(onNext,onComplete等)開始執行,在CreateEmitter內部,Observer介面中對應的方法依次執行,這樣就實現了一次從主題(上游)到觀察者(下游)的事件傳遞。

source.subscribe(parent)

這裡的 source 是ObservableOnSubscribe的例項,parent是CreateEmitter的例項。上面加粗文字敘述的內容,就是這行程式碼,可以說這是整個訂閱過程最核心的實現。

好了,回顧完基礎知識後,馬上進入正題,看看RxJava是如何實現執行緒切換的。

RxJava 之 subscribeOn

我們知道正常情況下,所有的內容都是在主執行緒執行,既然這裡提到了執行緒切換,那麼必然是切換到了子執行緒,因此,這裡需要關注執行緒的問題,我們就帶著下面這幾個問題去閱讀程式碼。

  • 1.是哪個物件在什麼時候建立了子執行緒,是一種怎樣的方式建立的?
  • 2.子執行緒又是如何啟動的?
  • 3.上游事件是怎麼跑到子執行緒裡執行的?
  • 4.多次用 subscribeOn 指定上游執行緒為什麼只有第一次有效 ?

示例

首先看一下,日常開發中實現執行緒切換的具體實現


    private void multiThread() {
        Observable.create(new ObservableOnSubscribe<String>() {
            @Override
            public void subscribe(ObservableEmitter<String> e) throws Exception {
                e.onNext("This msg from work thread :" + Thread.currentThread().getName());
                sb.append("\nsubscribe: currentThreadName==" + Thread.currentThread().getName());
            }
        })
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<String>() {
                    @Override
                    public void accept(String s) throws Exception {
                        Log.e(TAG, "accept: s= " + s);
                    }
                });
    }

複製程式碼

這段程式碼,使用過RxJava的同學再熟悉不過了,上游事件會在一個名為 RxNewThreadScheduler-1 的執行緒執行,下游執行緒會切換回我們熟悉的Android UI執行緒。

我們就從subscribeOn(Schedulers.newThread()) 出發,看看這個程式碼的背後,到底發生了什麼。

RxJava 是如何實現執行緒切換的(上)

subscribeOn

這裡我們先不管Schedulers.newThread() 是什麼鬼,首先看看這個subscribeOn()方法。

Observable.java--- subscribeOn(Scheduler scheduler)

    public final Observable<T> subscribeOn(Scheduler scheduler) {
        ObjectHelper.requireNonNull(scheduler, "scheduler is null");
        return RxJavaPlugins.onAssembly(new ObservableSubscribeOn<T>(this, scheduler));
    }
複製程式碼

可以看到,這個方法需要一個Scheduler 型別的引數。

RxJavaPlugins.java--- onAssembly(@NonNull Observable source)

    public static <T> Observable<T> onAssembly(@NonNull Observable<T> source) {
        Function<? super Observable, ? extends Observable> f = onObservableAssembly;
        if (f != null) {
            return apply(f, source);
        }
        return source;
    }
複製程式碼

O(∩_∩)O哈哈~,是不是覺得似曾相識,和create操作符一個套路呀。因此,observeOn也可以簡化如下:

new ObservableSubscribeOn<T>(this, Schedulers.newThread());
複製程式碼

這裡你也許會有疑問,這個this是什麼呢?其實這個this就是Observable,具體到上面的程式碼來說就是ObservableCreate,總之就是一個具體的Observable。

接著看ObservableSubscribeOn 這個類

public final class ObservableSubscribeOn<T> extends AbstractObservableWithUpstream<T, T> {
}

複製程式碼

看一下 AbstractObservableWithUpstream.java


abstract class AbstractObservableWithUpstream<T, U> extends Observable<U> implements HasUpstreamObservableSource<T> {

    /** The source consumable Observable. */
    protected final ObservableSource<T> source;

    AbstractObservableWithUpstream(ObservableSource<T> source) {
        this.source = source;
    }

    @Override
    public final ObservableSource<T> source() {
        return source;
    }

}
複製程式碼

再看一下 HasUpstreamObservableSource.java

/**
 * Interface indicating the implementor has an upstream ObservableSource-like source available
 * via {@link #source()} method.
 *
 * @param <T> the value type
 */
public interface HasUpstreamObservableSource<T> {
    /**
     * Returns the upstream source of this Observable.
     * <p>Allows discovering the chain of observables.
     * @return the source ObservableSource
     */
    ObservableSource<T> source();
}
複製程式碼

饒了半天,ObservableSubscribeOn 原來和上一篇說的ObservableCreate一樣,也是Observable的一個子類。只不過比ObservableCreate多實現了一個介面HasUpstreamObservableSource,這個介面很有意思,他的source()方法返回型別是ObservableSource(還記得這個類的角色嗎?)。也就是說ObservableSubscribeOn這個Observable是一個擁有上游的Observable。他有一個非常關鍵的屬性source,這個source就代表了他的上游。

我們接著看ObservableSubscribeOn的具體實現。

public final class ObservableSubscribeOn<T> extends AbstractObservableWithUpstream<T, T> {
    final Scheduler scheduler;

    public ObservableSubscribeOn(ObservableSource<T> source, Scheduler scheduler) {
        super(source);
        this.scheduler = scheduler;
    }

    @Override
    public void subscribeActual(final Observer<? super T> s) {
        final SubscribeOnObserver<T> parent = new SubscribeOnObserver<T>(s);
		// observer 呼叫onSubscribe方法,獲取上游的控制權
        s.onSubscribe(parent);

        parent.setDisposable(scheduler.scheduleDirect(new SubscribeTask(parent)));
    }
}
複製程式碼
  • 首先看他的建構函式,引數source就是我們之前提到過的this,scheduler就是Schedulers.newThread()。同時呼叫了父類AbstractObservableWithUpstream的建構函式,這裡結合之前的結論,我們可以確定通過這個建構函式,就建立出來了一個包含上游的ObservableSubscribeOn例項。
  • 再看實現訂閱關係的關鍵方法subscribeActual(),在這裡建立了一個SubscribeOnObserver的例項,SubscribeOnObserver 是AtomicReference的子類(保證原子性),同時實現了 Observer介面 和 Disposable 介面;你可以把他理解成一個Observer。

我們之前說過,subscribeActual()是實現上下游之間訂閱關係的重要方法。因為只有真正實現了訂閱關係,上下游之間才能連線起來。我們看這個方法的最後一句程式碼。

 parent.setDisposable(scheduler.scheduleDirect(new SubscribeTask(parent)));
複製程式碼

這句程式碼,可以說就是非常關鍵,因為從這裡開始了一系列的連鎖反應。首先看一下SubscribeTask

    final class SubscribeTask implements Runnable {
        private final SubscribeOnObserver<T> parent;

        SubscribeTask(SubscribeOnObserver<T> parent) {
            this.parent = parent;
        }

        @Override
        public void run() {
            source.subscribe(parent);
        }
    }
複製程式碼

看到這句 source.subscribe(parent),是不是覺得似曾相識呢?

SubscribeTask 實現了是Runnable介面,在其run方法中,定義了一個需要線上程中執行的任務。按照類的繼承關係,很明顯source 就是ObservableSubscribeOn 的上游Observable,parent是一個Observer。也就是說這個run方法要執行的內容就是實現ObservableSubscribeOn的上游和Observer的訂閱。一旦某個執行緒執行了這個Runnable(SubscribeTask),就會觸發了這個run方法,從而實現訂閱,而一旦這個訂閱實現,那麼後面的流程就是上節所說的事情了。

這裡可以解答第三個問題了,上游事件是怎麼給弄到子執行緒裡去的,這裡很明顯了,就是直接把訂閱方法放在了一個Runnable中去執行,這樣就一旦這個Runnable在某個子執行緒執行,那麼上游所有事件只能在這個子執行緒中執行了。

好了,執行緒要執行的任務似乎建立完了,下面就接著找看看子執行緒是怎麼建立的。回過頭繼續看剛才的方法,

scheduler.scheduleDirect(new SubscribeTask(parent))
複製程式碼

Scheduler.java----scheduleDirect


    public Disposable scheduleDirect(@NonNull Runnable run) {
        return scheduleDirect(run, 0L, TimeUnit.NANOSECONDS);
    }


    public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
        final Worker w = createWorker();
		// 對run進行了一次裝飾
        final Runnable decoratedRun = RxJavaPlugins.onSchedule(run);

        DisposeTask task = new DisposeTask(decoratedRun, w);

        w.schedule(task, delay, unit);

        return task;
    }

	@NonNull
	// 抽象方法
    public abstract Worker createWorker();
複製程式碼

首先看一下Worker類

    /**
     * Sequential Scheduler for executing actions on a single thread or event loop.
     * <p>
     * Disposing the {@link Worker} cancels all outstanding work and allows resource cleanup.
     */
    public abstract static class Worker implements Disposable {
  
        @NonNull
        public Disposable schedule(@NonNull Runnable run) {
            return schedule(run, 0L, TimeUnit.NANOSECONDS);
        }

  
        @NonNull
        public abstract Disposable schedule(@NonNull Runnable run, long delay, @NonNull TimeUnit unit);

        
    }

複製程式碼

Worker是Scheduler內部的一個靜態抽象類,實現了Disposable介面,其schedule()方法也是抽象的。

再看一下DisposeTask

static final class DisposeTask implements Runnable, Disposable {
        final Runnable decoratedRun;
        final Worker w;

        Thread runner;

        DisposeTask(Runnable decoratedRun, Worker w) {
            this.decoratedRun = decoratedRun;
            this.w = w;
        }

        @Override
        public void run() {
            runner = Thread.currentThread();
            try {
                decoratedRun.run();
            } finally {
                dispose();
                runner = null;
            }
        }

        @Override
        public void dispose() {
            if (runner == Thread.currentThread() && w instanceof NewThreadWorker) {
                ((NewThreadWorker)w).shutdown();
            } else {
                w.dispose();
            }
        }

        @Override
        public boolean isDisposed() {
            return w.isDisposed();
        }
    }

複製程式碼

DisposeTask 又是一個Runnable,同時也實現了Disposable介面。可以看到在他的run方法中會執行decoratedRun的run方法,這個decoratedRun其實就是引數中傳遞進來的run,也就是說,執行了這個DisposeTask的run方法,就會觸發SubscribeTask中的run方法,因此,我們就要關注是誰執行了這個DisposeTask。

回到scheduleDirect()方法

    public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
        final Worker w = createWorker();
		// 對run進行了一次裝飾
        final Runnable decoratedRun = RxJavaPlugins.onSchedule(run);

        DisposeTask task = new DisposeTask(decoratedRun, w);

        w.schedule(task, delay, unit);

        return task;
    }
複製程式碼

scheduleDirect()方法的實現我們總結一下:

  1. 建立一個Worker物件w,而在Scheduler類中createWorker()方法被定義為抽象方法,因此我們需要去Scheduler的具體實現中瞭解這個Worker的具體實現。
  2. 對引數run通過RxJavaPlugins進行一次裝飾,生成一個decoratedRun的Runnable(通過原始碼可以發現,其實什麼也沒幹,就是原樣返回)
  3. 通過decoratedRun和w生成一個DisposeTask物件task
  4. 通過Worker的schedule方法開始執行這個task。

ε=(´ο`*)))唉,說了這麼久,子執行緒是如何建立的依然不清楚,無論是SubscribeTask還是DisposeTask只是定義會在某個子執行緒中執行的任務,並不代表子執行緒已被建立。但是通過以上程式碼,我們也可以收穫一些有價值的結論:

  • 最終的Runnable任務,將由某個具體的Worker物件的scheduler()方法執行。
  • 這個scheduleDirect會返回一個Disposable物件,這樣我們就可以通過Observer去控制整個上游的執行了。

好了,到這裡對於subscribeOn()方法的分析已經到了盡頭,我們找了最終需要執行子任務的物件Worker,而這個Worker是個抽象類,因此我們需要關注Worker的具體實現了。

下面我們就從剛才丟下的Schedulers.newThread() 換個角度來分析,看看能不能找到這個Worker的具體實現。

Schedulers.newThread()

前面說了subscribeOn()方法需要一個Scheduler 型別的引數,然而通過前面的分析我們知道Scheduler是個抽象類,是無法被例項化的。因此,這裡就從Schedulers類出發。

/**
 * Static factory methods for returning standard Scheduler instances.
 */
public final class Schedulers {
}
複製程式碼

註釋很清楚,這個Schedulers就是一個用於生成Scheduler例項的靜態工廠。

下面我們就來看看,在這個工廠中newThread() 生成了一個什麼樣的Scheduler例項。

    @NonNull
    public static Scheduler newThread() {
        return RxJavaPlugins.onNewThreadScheduler(NEW_THREAD);
    }

	NEW_THREAD = RxJavaPlugins.initNewThreadScheduler(new NewThreadTask());

    static final class NewThreadTask implements Callable<Scheduler> {
        @Override
        public Scheduler call() throws Exception {
            return NewThreadHolder.DEFAULT;
        }
    }

    static final class NewThreadHolder {
        static final Scheduler DEFAULT = new NewThreadScheduler();
    }
複製程式碼

newThread() 方法經過層層委託處理(最終的建立方式,有點單例模式的意味),最終我們需要的就是一個NewThreadScheduler的例項。

NewThreadScheduler.java

public final class NewThreadScheduler extends Scheduler {

    final ThreadFactory threadFactory;

    private static final String THREAD_NAME_PREFIX = "RxNewThreadScheduler";
    private static final RxThreadFactory THREAD_FACTORY;

    /** The name of the system property for setting the thread priority for this Scheduler. */
    private static final String KEY_NEWTHREAD_PRIORITY = "rx2.newthread-priority";

    static {
        int priority = Math.max(Thread.MIN_PRIORITY, Math.min(Thread.MAX_PRIORITY,
                Integer.getInteger(KEY_NEWTHREAD_PRIORITY, Thread.NORM_PRIORITY)));

        THREAD_FACTORY = new RxThreadFactory(THREAD_NAME_PREFIX, priority);
    }

    public NewThreadScheduler() {
        this(THREAD_FACTORY);
    }

    public NewThreadScheduler(ThreadFactory threadFactory) {
        this.threadFactory = threadFactory;
    }

    @NonNull
    @Override
    public Worker createWorker() {
        return new NewThreadWorker(threadFactory);
    }
}
複製程式碼

不出所料NewThreadScheduler 是Scheduler的一個子類,在他的靜態程式碼塊中構造了一個Priority=5的執行緒工廠。而在我們最最關注的createWorker()方法中他又用這個執行緒工廠建立了一個NewThreadWorker 的例項。下面就讓我們看看最終的NewThreadWorker 做了些什麼工作。

NewThreadWorker.java(節選關鍵內容)


public class NewThreadWorker extends Scheduler.Worker implements Disposable {
    private final ScheduledExecutorService executor;

    volatile boolean disposed;

    public NewThreadWorker(ThreadFactory threadFactory) {
        executor = SchedulerPoolFactory.create(threadFactory);
    }

    @NonNull
    @Override
    public Disposable schedule(@NonNull final Runnable run) {
        return schedule(run, 0, null);
    }

    

    @Override
    public void dispose() {
        if (!disposed) {
            disposed = true;
            executor.shutdownNow();
        }
    }

}
複製程式碼

眾裡尋他千百度,終於找到了Worker的實現了,同時再一次不出所料的又一次實現了Disposable介面,o(╥﹏╥)o。

在其建構函式中,通過NewThreadScheduler中提供的執行緒工廠threadFactory建立了一個ScheduledExecutorService。

ScheduledExecutorService.java ---create


    public static ScheduledExecutorService create(ThreadFactory factory) {
        final ScheduledExecutorService exec = Executors.newScheduledThreadPool(1, factory);
        if (PURGE_ENABLED && exec instanceof ScheduledThreadPoolExecutor) {
            ScheduledThreadPoolExecutor e = (ScheduledThreadPoolExecutor) exec;
            POOLS.put(e, exec);
        }
        return exec;
    }
複製程式碼

用大名鼎鼎的Executors(Executor的工具類),建立了一個核心執行緒為1的執行緒。

至此,我們終於找到了第一個問題的答案,子執行緒是誰如何建立的;在NewThreadScheduler的createWorker()方法中,通過其構建好的執行緒工廠,在Worker實現類的建構函式中建立了一個ScheduledExecutorService的例項,是通過SchedulerPoolFactory建立的。

同時可以看到,通過執行dispose 方法,可以使用ScheduledExecutorService的shutdown()方法,停止執行緒的執行。

執行緒已經建立好了,下面就來看看到底是誰啟動了這個執行緒。前面我們說過,Worker的schedule()方法如果執行了,就會執行我們定義好的Runnable,通過這個Runnable中run方法的執行,就可以實現上下游訂閱關係。下面就來看看這個scheduler()方法。

@NonNull
    @Override
    public Disposable schedule(@NonNull final Runnable action, long delayTime, @NonNull TimeUnit unit) {
        if (disposed) {
            return EmptyDisposable.INSTANCE;
        }
        return scheduleActual(action, delayTime, unit, null);
    }

    @NonNull
    public ScheduledRunnable scheduleActual(final Runnable run, long delayTime, @NonNull TimeUnit unit, @Nullable DisposableContainer parent) {
        Runnable decoratedRun = RxJavaPlugins.onSchedule(run);

        ScheduledRunnable sr = new ScheduledRunnable(decoratedRun, parent);

        if (parent != null) {
            if (!parent.add(sr)) {
                return sr;
            }
        }

        Future<?> f;
        try {
            if (delayTime <= 0) {
                f = executor.submit((Callable<Object>)sr);
            } else {
                f = executor.schedule((Callable<Object>)sr, delayTime, unit);
            }
            sr.setFuture(f);
        } catch (RejectedExecutionException ex) {
            if (parent != null) {
                parent.remove(sr);
            }
            RxJavaPlugins.onError(ex);
        }

        return sr;
    }
複製程式碼

到這裡,已經很明顯了,在schedulerActual方法中,會通過剛才建立好的子執行緒物件executor通過submit或schedule執行一個Runnable任務(雖然這個Runnable物件再一次經過了各種裝飾和包裝,但其本質沒有發生變化),並將執行結果封裝後返回。而這個Runnable物件追根溯源來說,就是我們在ObservableSubscribeOn類中建立的一個SubscribeTask物件。因此,當這個子執行緒開始執行的時候就是執行SubscribeTask中run()方法的時機;一旦這個run方法執行,那麼

source.subscribe(parent)
複製程式碼

這句最關鍵的程式碼就開始執行了,一切的一切又回到了我們上一篇那熟悉的流程了。

好了,按照上面的流程捋下來,感覺還是有點分散,那麼就用UML圖看看整體的結構。

RxJava 是如何實現執行緒切換的(上)

我們看最下面的ObservableSubscribeOn,他是subscribeOn 返回的Observable物件,他持有一個Scheduler 例項的引用,而這個Scheduler例項就是NewThreadScheduler(即Schedulers.newThreade())的一個例項。ObservableSubscribeOn 的subscribeActual方法,會觸發NewThreadScheduler去執行SubscribeTask中定義的任務,而這個具體的任務又將由Worker類建立的子執行緒去執行。這樣就把上游事件放到了一個子執行緒中實現。

至於最後一個問題,多次用 subscribeOn 指定上游執行緒為什麼只有第一次有效?,看完通篇其實也很好理解了,因為上游Observable只有一個任務,就是subscribe(準確的來說是subscribeActual()),而subscribeOn 要做的事情就是把上游任務切換到一個指定執行緒裡,那麼一旦被切換到了某個指定的執行緒裡,後面的切換不就是沒有意義了嗎。

好了,至此上游事件切換到子執行緒的過程我們就明白了。下游事件又是如何切換的且聽下回分解,本來想一篇寫完的,結果發現越寫越多,只能分成兩篇了!!!o(╯□╰)o。

寫在後面的話

關於Disposable

在RxJava的分析中,我們經常會遇到Disposable這個單詞,確切的說是介面,這裡簡單說一說這個介面。

/**
 * Represents a disposable resource.
 */
public interface Disposable {
    void dispose();
    boolean isDisposed();
}
複製程式碼

我們知道,在Java中,類實現某個介面,通俗來說就是代表這個類多了一項功能,比如一個類實現Serializable介面,代表這個類是可以序列化的。這裡Disposable也是代表一種能力,這個能力就是Disposable,就是代表一次性的,用後就丟棄的,比如一次性筷子,還有那啥。

在RxJava中很多類都實現了這個介面,這個介面有兩個方法,isDisposed()顧名思義返回當前類是否被拋棄,dispose()就是主動拋棄。因此,所有實現了這個介面的類,都擁有了這樣一種能力,就是可以判斷自己是否被拋棄,同時也可以主動拋棄自己。

上一篇我們說了,Observer通過onSubscribe(@NonNull Disposable d),會獲得一個Disposable,這樣就有能力控制上游的事件傳送了。這樣,我們就不難理解,為什麼那麼多類實現了這個介面,因為下游獲取到的是一個擁有Disposable的物件,而一旦擁有了一個這樣的物件,那麼就可以通過下游控制上游了。可以說,這是RxJava對常規的觀察者模式所做的最給力的改變。

關於各種ObservableXXX ,subscribeXXX,ObserverXXX

在檢視RxJava的原始碼時,可能很多人都和我一樣,有一個巨大的困擾,就是這些類的名字好他媽難記,感覺長得都差不多,關鍵念起來好像也差不多。但其實本質上來說,RxJava對類的命名還是非常規範的,只是我們不太習慣而已。按照英文單詞翻譯:

  • Observable 可觀察的
  • Observer 觀察者
  • Subscribe 訂閱

其實就這麼三個主語,其他的什麼ObservableCreate,ObservableSubscribeOn,AbstractObservableWithUpstream,還有上面提到的Disposable,都是對各種各樣的Observable和Observer的變形和修飾結果,只要理解這個類的核心含義是什麼,就不會被這些名字搞暈了。

RxJava 可以說是博大精深,以上所有分析完全是個人平時使用時的總結與感悟,有任何錯誤之處,還望各位讀者提出,共同進步。

關於RxJava 這裡牆裂推薦一篇文章一篇不太一樣的RxJava介紹,感覺是自扔物線那篇之後,對RxJava思想感悟最深的一篇了。對RxJava 有興趣的同學,可以多度幾遍,每次都會有收穫!!


相關文章