背景
最近在做Android
應用線程優化,其中有一個核心指標就是收斂進程中的線程數,這是一段很長的故事,本文只是關於RxJava
的一個方面的優化,其中有些坑值得每位使用RxJava
的筒子注意。背景是這樣的,我們APP在進入之後,通過一些正常業務流程的使用,穩定之後,通過Android Profiler
發現有一類RxComputationScheduler-
的線程,數量爲8個(我的測試機有8個計算核心)。
通過直接搜索關鍵詞,RxComputationScheduler-
很容易就定位到了它是RxJava默認提供的computation
調度器產生的線程。我印象裏面,我們很少使用computation
調度器。這8個線程幾乎沒有任何負載,也就說它們雖然存在,卻一直在睡覺😂,總所周知,Java的線程模型和系統線程模型是1:1映射關係,所以這些睡大覺的傢伙是我這次要幹掉的!
別的不說,至少Rx在使用線程的時候,還是挺規範的,它會給自己使用的線程命名,這樣在進行線程調試的時候,我們能找到對應的線程調起方,至於反面呢?我直接上圖吧
這些線程直接使用Executors.defaultThreadFactory 爲線程池指定ThreadFactory,這樣的後果就是我們這裏看到了一堆pool-${線程池編號}-thread-${線程編號}
的殭屍線程,試問如果我們要去進行線程優化、鎖排查,怎麼去定位問題?
PS:就算他們沒有指定線程名字,也難不住聰明又伶俐的我,後面我會介紹一種定位這種殭屍線程的方法 😊
調度器之殤
既然是computation
調度器產生的殭屍線程,那麼關於computation
調度器,看名字都知道它其實是Rx提供給開發者進行CPU密集型任務的調度器,爲什麼這麼說?因爲computation
調度器內部最多隻會創建當前設備的計算核心個數的線程(注意,它不是採用線程池來實現的)。
CPU密集型任務是和IO密集型任務對應的,所謂CPU密集型,指的是任務是大規模的計算工作,會一直佔用CPU,所以對於這類任務,線程數超過計算核心沒有任何意義,因爲他們很少會把線程掛起,增加線程只會導致線程直接爭搶時間片和上下文切換帶來的開銷,所以一般來說,CPU密集型任務設計的線程池中線程個數都需要嚴格限制(常用計算核心數)
IO密集型任務,是我們最常見的,比如發送個網絡請求,比如讀寫個文件,這類任務的突出特徵就是對於CPU佔用少,一般都會阻塞在IO設備上面,所以對於這些任務,通常我們會設置比較大的線程數量,因爲反正它們執行期間大部分時間都是在睡覺,那麼更多的線程可以提高系統的吞吐量。
一些語言中常見的協程,其實就是爲了解決我們創建過多線程,然後其實對於CPU使用時間很短,很多線程在佔用系統資源,所以在語言層面提供一種新思維,不去阻塞系統線程,在一個線程上面處理多個IO任務;
既然如此,看起來就是業務中使用了computation
調度器,導致系統中產生了8個計算線程,那它們爲什麼不會被回收呢?這就需要看一下代碼了,computation
默認基於EventLoopsScheduler
來實現的,它內部使用自定義的一個類來做線程管理:
static final class FixedSchedulerPool {
// 默認可計算核心數量
final int cores;
//poolworker就是一個NewThreadWorker,直接通過一個線程數組來管理線程
final PoolWorker[] eventLoops;
long n;
FixedSchedulerPool(ThreadFactory threadFactory, int maxThreads) {
// initialize event loops
this.cores = maxThreads;
this.eventLoops = new PoolWorker[maxThreads];
for (int i = 0; i < maxThreads; i++) {
//直接一上來就初始化數組,生成各個NewThreadWorker
this.eventLoops[i] = new PoolWorker(threadFactory);
}
}
//獲取Worker直接是內部計數器 和 cores取餘保證任務在各個Worker來回分配
public PoolWorker getEventLoop() {
int c = cores;
if (c == 0) {
return SHUTDOWN_WORKER;
}
// simple round robin, improvements to come
return eventLoops[(int)(n++ % c)];
}
}
EventLoopsScheduler
創建Worker就簡單了,直接從上面的數據結構中取出一個PoolWorker即可,然後給EventLoopWorker
包裝一下:
public Worker createWorker() {
return new EventLoopWorker(pool.get().getEventLoop());
}
我前面說過了,PoolWorker只是一個普通的NewThreadWorker,所以這個EventLoopWorker
的包裝肯定做了什麼不可告人的祕密:
private static class EventLoopWorker extends Scheduler.Worker {
public void unsubscribe() {
both.unsubscribe();
}
@Override
public Subscription schedule(final Action0 action) {
return poolWorker.scheduleActual(new Action0() {
@Override
public void call() {
if (isUnsubscribed()) {
return;
}
action.call();
}
}, 0, null, serial);
}
}
ok,如你所見,這個EventLoopWorker好像啥都沒幹呀,只是把任務代理給了之前傳進來的Worker,然而你在仔細看看它的unsubscribe
方法,調用了both的反註冊,而這個both僅僅是每次Worker. schedule任務的Subscription,它並沒有去調用Worker的unsubscribe
(Super),那Super中做了什麼呢?NewThreadWorker:
@Override
public void unsubscribe() {
isUnsubscribed = true;
executor.shutdownNow();
deregisterExecutor(executor);
}
所以結論就是EventLoopWorker把unsubscribe的線程關閉代碼給去掉了,😂
直接上結論吧:computation
調度器在使用過程中會創建線程核心數個數的線程,然後這些線程會一直存活。
因爲RxJava並不是一個針對移動端設計的框架,所以在服務端來說,通常準備8個左右線程進行計算工作沒有問題,然而客戶端上業務進行純計算的任務實在是太少了,而且不會存在很高的併發度,所以浪費八個線程一直在這裏睡覺,感覺不太合適,怎麼破?
所以在移動端來說,我覺得通過直接創建線程來處理計算任務是合適的,處理完,直接釋放。
RxJavaPlugins.getInstance().registerSchedulersHook(new RxJavaSchedulersHook() {
@Override
public Scheduler getIOScheduler() {
return new CachedThreadScheduler(new MYRxThreadFactory("MYRxIoScheduler-"));
}
@Override
public Scheduler getComputationScheduler() {
return createNewThreadScheduler(new RxThreadFactory("RxCom"));
}
});
這裏這麼成NewThread其實有問題,後面會解釋,這樣方便現在調試和定位問題
所以我通過RxJavaPlugins中修改computation
默認的行爲,改成每次都創建線程(名稱爲RxCom),這次修復之後,滿心歡喜,build,run,打開Android Profiler:
還是有五位大爺穩坐釣魚臺,然後:
RxBus惹的禍
看到上面還存在5個線程,我內心很崩潰了,不是都NewThreadWorder了麼,怎麼還沒有被回收?看起來我們得找到這5個線程是哪些地方打開的了!這裏我取了點巧,使用了一個hook庫:
Epic,它基於Xposed,用來Hook自己進程,所以我的思路也很清楚,HOOK開啓線程的代碼,加入日誌,存儲對應線程的名稱,然後不就找到罪魁禍首了麼?
so:
這樣就hook住每次打開創建線程的方法(線程的構造函數)了,在hook方法裏面:
通過存儲線程名和當前堆棧,然後在run起來吧~
然後在Android Profiler中找到對應的線程名,它不就是我這個Map裏面的Key嗎?
這樣我就拿到了寶貴的啓動堆棧:
java.lang.Throwable
at com.sankuai.movie.ThreadMethodHook.afterHookedMethod(ThreadMethodHook.java:29)
at com.taobao.android.dexposed.DexposedBridge.handleHookedArtMethod(DexposedBridge.java:273)
at me.weishu.epic.art.entry.Entry.onHookObject(Entry.java:69)
at me.weishu.epic.art.entry.Entry.referenceBridge(Entry.java:186)
at rx.internal.util.RxThreadFactory.newThread(RxThreadFactory.java:39)
at java.util.concurrent.ThreadPoolExecutor$Worker.<init>(ThreadPoolExecutor.java:631)
at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:945)
at java.util.concurrent.ThreadPoolExecutor.ensurePrestart(ThreadPoolExecutor.java:1611)
at java.util.concurrent.ScheduledThreadPoolExecutor.delayedExecute(ScheduledThreadPoolExecutor.java:342)
at java.util.concurrent.ScheduledThreadPoolExecutor.schedule(ScheduledThreadPoolExecutor.java:562)
at java.util.concurrent.ScheduledThreadPoolExecutor.submit(ScheduledThreadPoolExecutor.java:664)
at rx.internal.schedulers.NewThreadWorker.scheduleActual(NewThreadWorker.java:240)
at rx.internal.schedulers.NewThreadWorker.schedule(NewThreadWorker.java:224)
at rx.internal.schedulers.NewThreadWorker.schedule(NewThreadWorker.java:216)
at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.schedule(OperatorObserveOn.java:190)
at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber$1.request(OperatorObserveOn.java:147)
at rx.Subscriber.setProducer(Subscriber.java:209)
at rx.Subscriber.setProducer(Subscriber.java:205)
at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.init(OperatorObserveOn.java:141)
at rx.internal.operators.OperatorObserveOn.call(OperatorObserveOn.java:75)
at rx.internal.operators.OperatorObserveOn.call(OperatorObserveOn.java:40)
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:46)
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30)
at rx.Observable.subscribe(Observable.java:8759)
at rx.Observable.subscribe(Observable.java:8726)
at rx.Observable.subscribe(Observable.java:8581)
at com.dianping.nvnetwork.tunnel2.ConnectionPoolManager.<init>(ConnectionPoolManager.java:105)
at com.dianping.nvnetwork.tunnel2.NIOTunnel.<init>(NIOTunnel.java:58)
at com.dianping.nvnetwork.tunnel2.RxAndroidNIOTunnelService.<init>(RxAndroidNIOTunnelService.java:51)
忽略N行
PS:還記得之前我說過,有一堆殭屍線程沒有命名怎麼查找麼,你GET到方法了嗎?😝
我們發現是某個Manager代碼啓動的這個線程,根據Log點進去看看:
看起來沒有任何毛病呀,一個RxBus訂閱,結果切換到
computation
線程,然後計算工作,不過這裏看着隱隱有點擔心,總所周知,時間總線的訂閱是沒有結束時候的,所以這個流一直在註冊中,現在的問題就簡單了,observeOn到底拿這個computation
調度器做了什麼導致它不能回收了呢?
這裏又涉及到RxJava關於lift和OperatorObserveOn兩個類的講解,但是由於篇幅原因,我這裏不去展開說明了,observeOn中會依據給的Scheduler,create一個Worker,然後在流完成之後,會被反註冊;因爲我目前指定的是NewThread調度器,Worker和Thread一一對應,Thread存活也就不意外啦!
解決方案
本來我是想把computation
調度器直接替換成newThread
,然而我看到下圖的時候還是有些震驚的,就一個啓動,computation
就給我霍霍了66個線程,所以我們還是得考慮線程複用的問題;
直接上最後的方案吧:
核心是我們需要將核心線程數也可以超時,幹掉!
通過Android Profiler再次打開,我們可以看到在最開始的時候,有一些
computation
調度器的線程存活,數量是8個,過一段時間之後,這些線程就自己銷燬啦!
至此,我們項目中又被幹掉了8個殭屍線程!