本文作者JasonChen,原文地址: chblog.me/2018/12/19/…
ReactiveX 響應式程式設計庫,這是一個程式庫,通過使用可觀察的事件序列來構成非同步和事件驅動的程式。
其簡化了非同步多執行緒程式設計,在以前多執行緒程式設計的世界中,鎖、可重入鎖、同步佇列器、訊號量、併發同步器、同步計數器、並行框架等都是具有一定的使用門檻,稍有不慎或者使用不成熟或對其原始碼理解不深入都會造成相應的程式錯誤和程式效能的低下。
觀察者模型
24種設計模式的一種,觀察者Observer和主題Subject之間建立組合關係:Subject類例項中包含觀察者Observer的引用,增加引用的目的就是為了通知notify,重要點就是要在Subject的notify功能中呼叫Observer的接受處理函式receiveAndHandle。
個人理解:觀察者模型其實是一種非同步回撥通知,將資料的處理者先註冊到資料的輸入者那邊,這樣通過資料輸入者執行某個函式去呼叫資料處理者的某個處理方法。
RxJava2
Rx有很多語言的實現庫,目前比較出名的就是RxJava2。本文主要講Rxjava2的部分原始碼解讀,內部設計機制和內部執行的執行緒模型。
RxJava是近兩年來越來越流行的一個非同步開發框架,其使用起來十分簡單方便,功能包羅永珍,十分強大。
基本使用
使用RxJava2大致分為四個操作:
- 建立資料釋出者
- 新增資料變換函式
- 設定資料釋出執行緒池機制,訂閱執行緒池機制
- 新增資料訂閱者
// 建立flowable
Flowable<Map<String, Map<String,Object>>> esFlowable = Flowable.create(new ElasticSearchAdapter(), BackpressureStrategy.BUFFER);
Disposable disposeable = esFlowable
// map操作 1.採集、2.清洗
.map(DataProcess::dataProcess)
.subscribeOn(Schedulers.single())
//計算任務排程器
.observeOn(Schedulers.computation())
// 訂閱者 consumer 執行運算
.subscribe(keyMaps -> new PredictEntranceForkJoin().predictLogic(keyMaps));
複製程式碼
以上就是一個實際的例子,裡面的ElasticSearchAdapter實際隱藏了一個使用者自定義實現資料生產的subscribe介面:
FlowableOnSubscribe<T> source
複製程式碼
使用者需要實現這個介面函式:
void subscribe(@NonNull FlowableEmitter<T> emitter) throws Exception
複製程式碼
這個介面主要用於內部回撥,後面會有具體分析, emitter 英文翻譯發射器,很形象,資料就是由它產生的,也是業務系統需要對接的地方,一般業務程式碼實現這個介面類然後發射出需要處理的原始資料。
map函式作為資料變換處理的功能函式將原來的資料輸入變換為另外的資料集合,然後設定釋出的執行緒池機制subscribeOn(Schedulers.single())
,訂閱的執行緒池機制observeOn(Schedulers.computation())
,最後新增資料訂閱函式,也就是業務系統需要實現另外一個地方,從而實現資料的自定義處理消費。
rxjava2支援的lambda語法
- 建立操作符:just fromArray empty error never fromIterable timer interval intervalRange range/rangeLong defer
- 變換操作符:map flatMap flatmapIterable concatMap switchmap cast scan buffer toList groupBy toMap
- 過濾操作符:filter take takeLast firstElement/lastElement first/last firstOrError/lastOrError elementAt/elementAtOrError ofType skip/skipLast ignoreElements distinct/distinctUntilChanged timeout throttleFirst throttleLast/sample throttleWithTimeout/debounce
- 合併聚合操作符:startWith/startWithArray concat/concatArray merge/mergeArray concatDelayError/mergeDelayError zip combineLatest combineLatestDelayError reduce count collect
- 條件操作符:all ambArray contains any isEmpty defaultIfEmpty switchIfEmpty sequenceEqual takeUntil takeWhile skipUntil skipWhile
有一篇部落格詳細介紹了rxjava的各種操作符,連結maxwell-nc.github.io/android/rxj…
RxJava2 原始碼解析
閱讀原始碼個人比較喜歡帶著疑惑去看,這樣與目標有方向。接下來的分析以Flowable為例,這裡所有的例子都是按照Flowable為例,因為Flowable在實際專案中比Observable可能用的多,因為實際場景中資料生產速度和資料消費速度都會有一定的不一致甚至資料生產速度遠大於資料消費速度。
資料釋出和訂閱
首先從資料訂閱者開始,點進原始碼看進一步解析,裡面有很多subscribe過載介面:
public final Disposable subscribe(Consumer<? super T> onNext) {
return subscribe(onNext, Functions.ON_ERROR_MISSING,
Functions.EMPTY_ACTION, FlowableInternalHelper.RequestMax.INSTANCE);
}
public final Disposable subscribe(Consumer<? super T> onNext, Consumer<? super Throwable> onError,
Action onComplete, Consumer<? super Subscription> onSubscribe) {
ObjectHelper.requireNonNull(onNext, "onNext is null");
ObjectHelper.requireNonNull(onError, "onError is null");
ObjectHelper.requireNonNull(onComplete, "onComplete is null");
ObjectHelper.requireNonNull(onSubscribe, "onSubscribe is null");
//組裝成FlowableSubscriber
LambdaSubscriber<T> ls = new LambdaSubscriber<T>(onNext, onError, onComplete, onSubscribe);
//呼叫核心的訂閱方法
subscribe(ls);
return ls;
}
public final void subscribe(FlowableSubscriber<? super T> s) {
ObjectHelper.requireNonNull(s, "s is null");
try {
//註冊一些鉤子這裡對此不進行講解,主要不是核心方法
Subscriber<? super T> z = RxJavaPlugins.onSubscribe(this, s);
ObjectHelper.requireNonNull(z, "The RxJavaPlugins.onSubscribe hook returned a null FlowableSubscriber. Please check the handler provided to RxJavaPlugins.setOnFlowableSubscribe for invalid null returns. Further reading: https://github.com/ReactiveX/RxJava/wiki/Plugins");
//核心訂閱方法,從名字也能讀出是指訂閱實際呼叫處
//不同的資料產生類也就是實現Flowable抽象類的類
//比如FlowableCreate,FlowSingle,FlowMap等等去實現自己的實際方法
subscribeActual(z);
} catch (NullPointerException e) { // NOPMD
throw e;
} catch (Throwable e) {
Exceptions.throwIfFatal(e);
// can't call onError because no way to know if a Subscription has been set or not
// can't call onSubscribe because the call might have set a Subscription already
RxJavaPlugins.onError(e);
NullPointerException npe = new NullPointerException("Actually not, but can't throw other exceptions due to RS");
npe.initCause(e);
throw npe;
}
}
複製程式碼
下面選擇FlowCreate的subscribeActual(Subscriber<? super T> t)方法進行剖析。
public void subscribeActual(Subscriber<? super T> t) {
BaseEmitter<T> emitter;
//根據不同的回壓模式選擇不一樣的資料發射類
//神奇的回壓模式其實本質上就是一個個資料發射-消費模式
switch (backpressure) {
case MISSING: {
emitter = new MissingEmitter<T>(t);
break;
}
//...
default: {
emitter = new BufferAsyncEmitter<T>(t, bufferSize());
break;
}
}
//回撥註冊的FlowableSubscriber的onSubscribe方法
//這裡非常重要,因為這裡涉及了rxjava特有的 request請求再消費資料的模式
//也就是說如果沒有request資料,那麼就不會呼叫資料發射(釋出)者的onNext方法,
//那麼資料訂閱者也就不會消費到資料
t.onSubscribe(emitter);
try {
//回撥註冊的FlowableOnSubscribe<T> source的subscribe方法
//這個source其實就是在建立Flow流時註冊的資料產生類,進一步驗證了上文中
//提及的其需要實現FlowableOnSubscribe<T>介面
source.subscribe(emitter);
} catch (Throwable ex) {
Exceptions.throwIfFatal(ex);
emitter.onError(ex);
}
}
//重點分析BufferAsyncEmitter這個類,看字面意思這是一個switch的預設選擇類,
//但其實它是回壓策略為BUFFER時的資料發射類
//首先這個類的建構函式具有兩個引數,很明顯這是 actul就是前面的t這個變數,也就是
//註冊的資料消費(訂閱)者,capacityHint則是設定容量大小的,預設是128,如果需要擴大需要
//自行設定環境變數 rx2.buffer-size
BufferAsyncEmitter(Subscriber<? super T> actual, int capacityHint) {
super(actual);
this.queue = new SpscLinkedArrayQueue<T>(capacityHint);
this.wip = new AtomicInteger();
}
public void onNext(T t) {
if (done || isCancelled()) {
return;
}
if (t == null) {
onError(new NullPointerException("onNext called with null. Null values are generally not allowed in 2.x operators and sources."));
return;
}
// queue 是儲存元素的佇列,也就是buffer的核心儲存。
// 當我們開始向下遊傳送資料的時候首先存入佇列,然後下面的drain則是進行核心的
queue.offer(t);
drain();
}
//核心的類
void drain() {
//關鍵的地方 解決生產速率和消費速率不一致的關鍵地方,也是我們寫併發程式值得借鑑的地方。
//當資料的產生者(釋出)頻繁呼叫onNext方法時,這裡產生併發呼叫關係,wip變數是atomic變數,
//當第一次執行drain函式時,為0繼續執行後面的流程,當快速的繼續呼叫onNext方法時,wip不為0然後返回
//那麼後面的流程我們其實已經很大概率會猜測到應該是去取佇列的資料然後做一些操作
if (wip.getAndIncrement() != 0) {
return;
}
int missed = 1;
//這裡的downstream其實就是註冊的資料訂閱者,它是基類BaseEmitter的變數,前面初始化時呼叫了基類的建構函式
final Subscriber<? super T> a = downstream;
final SpscLinkedArrayQueue<T> q = queue;
for (;;) {
long r = get();
long e = 0L;
while (e != r) {
if (isCancelled()) {
q.clear();
return;
}
boolean d = done;
//取佇列中的資料
T o = q.poll();
boolean empty = o == null;
if (d && empty) {
Throwable ex = error;
if (ex != null) {
error(ex);
} else {
complete();
}
return;
}
if (empty) {
break;
}
//此處回撥訂閱者的onNext方法去真正的執行資料例項程式
//到此資料從產生到消費其生命週期已經走完
a.onNext(o);
e++;
}
if (e == r) {
if (isCancelled()) {
q.clear();
return;
}
boolean d = done;
boolean empty = q.isEmpty();
if (d && empty) {
Throwable ex = error;
if (ex != null) {
error(ex);
} else {
complete();
}
return;
}
}
if (e != 0) {
//標記已經消費的個數
BackpressureHelper.produced(this, e);
}
//前面說過wip會原子性的增加,而且是每呼叫一次onNext增加一次
//missed從其名解釋是指錯過的意思,個人理解是錯過消費的資料個數,錯過消費
//的意思其實就是指沒有進行a.onNext資料消費處理的資料
missed = wip.addAndGet(-missed);
if (missed == 0) {
//如果沒有錯過的資料也就是全部都消費完那就跳出for迴圈
//此處for迴圈方式和JUC原始碼中Doug Lea的做法都有類似之處
break;
}
}
}
複製程式碼
操作符與執行緒池機制原理剖析
首先在進行原始碼分析之前講述一下一種模式:裝飾者模式 24種模式中的一種,在java io原始碼包中廣泛應用 簡單的來說是與被裝飾者具有相同介面父類同時又對被裝飾者進行一層封裝(持有被裝飾者的引用),以此用來加上自身的特性。
迴歸主題,當我們使用操作符和執行緒池機制的時候做法都是在資料釋出者後面進行相應的函式操作:
Disposable disposeable = scheduleObservable
.map(aLong -> dataAdapter.handlerDpti())
.map(DataProcess::dataProcess)
.subscribeOn(Schedulers.single())
複製程式碼
那麼為何這麼做,接下來我們進行原始碼分析:
- subscribeOn map 方法都在Flowable類中:
public final <R> Flowable<R> map(Function<? super T, ? extends R> mapper) {
ObjectHelper.requireNonNull(mapper, "mapper is null");
return RxJavaPlugins.onAssembly(new FlowableMap<T, R>(this, mapper));
}
public final Flowable<T> subscribeOn(@NonNull Scheduler scheduler, boolean requestOn) {
ObjectHelper.requireNonNull(scheduler, "scheduler is null");
return RxJavaPlugins.onAssembly(new FlowableSubscribeOn<T>(this, scheduler, requestOn));
}
複製程式碼
這裡是例項方法呼叫,傳進了this物件這個很關鍵,這裡其實就是我們前面提到的裝修者模式,持有上游物件也就是資料來源source的引用。
以FlowableSubscribeOn為例進行分析,這個類經常會用到,因為其內部設定了執行緒池的機制所以在實際使用專案中會大量使用,那麼是如何做到執行緒池方式的呢?進一步利用原始碼進行分析。
2.裝飾者的內部程式碼分析
以subscribeOn 為例:
//很明顯 實現的抽象類其實是裝修者抽象類
public final class FlowableSubscribeOn<T> extends AbstractFlowableWithUpstream<T , T>
// 這個在前面我們重點分析過這是實際訂閱執行的類方法,其實也就是我們說的裝飾方法,裡面實現了每個類自己的特定“裝修”方法
@Override
public void subscribeActual(final Subscriber<? super T> s) {
// 獲取訂閱者,下一篇文章會重點講述rxjava的執行緒池分配機制
Scheduler.Worker w = scheduler.createWorker();
final SubscribeOnSubscriber<T> sos = new SubscribeOnSubscriber<T>(s, w, source, nonScheduledRequests);
// 跟前面一樣呼叫資料訂閱者的onSubscribe方法
s.onSubscribe(sos);
// 由分配的排程者進行訂閱任務的執行
w.schedule(sos);
}
// 開始分析SubscribeOnSubscriber這個靜態內部類的內部程式碼
// 實現了Runable用來非同步執行
static final class SubscribeOnSubscriber<T> extends AtomicReference<Thread>
implements FlowableSubscriber<T>, Subscription, Runnable
// 下游訂閱引用
final Subscriber<? super T> downstream;
// 上游發射類引用
final AtomicReference<Subscription> upstream;
// 上游資料來源引用 跟上游引用有區別,簡單的說每個上游資料來源引用有自己的上游發射類
Publisher<T> source;
// 這裡是裝飾的核心程式碼
@Override
public void run() {
lazySet(Thread.currentThread());
// source即為上游,表示其所裝飾的源
Publisher<T> src = source;
source = null;
// 呼叫上游的自身的subscribe方法,在上面一開始我們說這個方法內部會去呼叫自身實現的subscribeActual方法
// 從而實現上游自己的特定方法,比如假設source是FlowCreate那麼此處就會呼叫前面一開始我們所講到的資料的發射
src.subscribe(this);
}
// 既然已經保證了資料的發射那麼資料的處理是不是也要處理
// 很明顯這是呼叫了下游訂閱者的onNext方法
@Override
public void onNext(T t) {
downstream.onNext(t);
}
複製程式碼
本文總結
筆者喜歡總結,總結意味著我們反思和學習前面的知識點,應用點以及自身的不足。
- 設計模式:觀察者模式和裝修者模式
- 併發處理技巧:回壓策略(其實本質是快取)的實現原理以及細節點