Looper.loop()引發(fā)的慘案
塵埃落定!清華才子王垠加入華為職級(jí)22,前阿里P10趙海平加入字節(jié)跳動(dòng),職級(jí)或?yàn)?+ 百度網(wǎng)盤(pán)“破解版”,Pandownload開(kāi)發(fā)者被抓 
作者:不怕天黑
來(lái)源:https://juejin.im/post/6859156343228792840
案件描述
在一個(gè)安靜的下午,一妹子在RxHttp群里反饋,自己開(kāi)發(fā)的app,賬號(hào)被擠下線時(shí),重新登錄到首頁(yè)后,發(fā)現(xiàn)有一個(gè)請(qǐng)求,代碼執(zhí)行了,卻沒(méi)有任何回調(diào),看得出,妹子很著急。
what ??? 還有這種事?原本安靜的群,一下活躍了起來(lái),男同胞們一頓狂猜,我總結(jié)了下,如下:
會(huì)不會(huì)請(qǐng)求代碼沒(méi)執(zhí)行,妹子自己搞錯(cuò)了吧?
發(fā)請(qǐng)求前,出現(xiàn)異常,代碼被中斷運(yùn)行?
請(qǐng)求過(guò)程伴隨著頁(yè)面跳轉(zhuǎn),導(dǎo)致頁(yè)面銷毀時(shí),請(qǐng)求被自動(dòng)關(guān)閉?
請(qǐng)求過(guò)程出現(xiàn)異常,被RxJava全局異常捕獲了,并吃掉了,所以收不到失敗回調(diào)?
這里解釋下,妹子采用RxHttp+RxJava結(jié)合的方式發(fā)請(qǐng)求
經(jīng)過(guò)第一輪詢問(wèn)后,以上猜想輕而易舉的被推翻了,我也大概知道了案件的細(xì)節(jié),為此,我用代碼來(lái)還原一下,為簡(jiǎn)化案件,還原時(shí),我會(huì)適當(dāng)?shù)淖龀鲂薷模馑歼€是那個(gè)意思。
案件還原
妹子在首頁(yè)MainActivity的OnCreate方法,會(huì)并行3個(gè)請(qǐng)求,如下:
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.main_activity);request1();request2();request3();}public void request1() {RxHttp.get("/service/...").asString().to(RxLife.toMain(this))????????.subscribe(s?->?{},?throwable?->?{???????????});????????????????????????????????????????????????????????????}public void request2() {}public void request3() {}
這段代碼看起來(lái)并沒(méi)有任何問(wèn)題,正常登錄進(jìn)來(lái)后,都是正常的。
但是當(dāng)賬號(hào)被擠下線后(擠到登錄頁(yè)),重新登錄到首頁(yè)后,發(fā)現(xiàn)request1()、request2()、request3()三個(gè)請(qǐng)求方法都執(zhí)行了,可request2()方法卻遲遲收不到回調(diào),不管成功/失敗都收不到。
開(kāi)始辦案
以上猜想全部被推翻,接下來(lái)怎么辦?很明顯,我們要明確一點(diǎn):
請(qǐng)求到底有沒(méi)有發(fā)出去?服務(wù)端有沒(méi)有收到這個(gè)請(qǐng)求?
隨后,妹子用Adnroid Studio自帶的Profiler工具,監(jiān)控了下,發(fā)現(xiàn)請(qǐng)求并未發(fā)出來(lái),接著,又找后臺(tái)人員確認(rèn)了下,后臺(tái)也并未收到這個(gè)請(qǐng)求。
那就更奇怪了,請(qǐng)求代碼執(zhí)行了,請(qǐng)求卻沒(méi)有發(fā)出去?作為程序員的我第一反應(yīng),這怎么可能呢?妹子你用的手機(jī)有問(wèn)題吧?要不換個(gè)手機(jī)試試?顯然換了手機(jī),問(wèn)題一樣存在,這就尷尬了。
接下來(lái),跟妹子不斷的調(diào)試,一而再,再而三的確認(rèn)了,請(qǐng)求代碼沒(méi)有任何問(wèn)題,然而,我卻陷入了沉思之中,很絕望,很無(wú)助,甚至懷疑這是OkHttp的問(wèn)題。
作為一名老鳥(niǎo),最后我還是冷靜了下來(lái),重新整理了線索,發(fā)現(xiàn)又一條線索被遺漏了,那就是賬號(hào)被擠,自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面,為什么只有在賬號(hào)被擠時(shí),才會(huì)出現(xiàn)問(wèn)題?于是乎,我調(diào)整了調(diào)查方向
賬號(hào)是如何被擠?又是如何跳轉(zhuǎn)到登錄頁(yè)面的?
終于,兇手露出了水面,兇手就是Looper,原來(lái)妹子是通過(guò)OkHttp的攔截器來(lái)監(jiān)聽(tīng)賬號(hào)被擠,并通過(guò)Looper來(lái)彈出一個(gè)Toast提示,并且執(zhí)行頁(yè)面跳轉(zhuǎn)邏輯,如下:
public class TokenInterceptor implements Interceptor {private Context context;public Response intercept(Chain chain) throws IOException {Request request = chain.request();Response originalResponse = chain.proceed(request);String code = originalResponse.header("code");if ("-1".equals(code)) {Looper.prepare();Toast.makeText(context, "你的賬號(hào)在其它設(shè)備上登錄", Toast.LENGTH_LONG).show();context.startActivity(new Intent(context, LoginActivity.class));Looper.loop();}return originalResponse;}}
也許你會(huì)問(wèn),這確定有問(wèn)題?通過(guò)Looper在子線程彈出一個(gè)Toast,這不是很正常的一件事?經(jīng)常這么干,從來(lái)沒(méi)出現(xiàn)任何問(wèn)題,為啥到你這就出問(wèn)題?
我讓妹子把Looper及Toast代碼注釋掉,if語(yǔ)句里面只保留一行startActivity,妹子試后開(kāi)心的跟我說(shuō),好了,沒(méi)問(wèn)題了,這怎么解釋?
開(kāi)始破案
Looper一臉委屈的說(shuō)道:你說(shuō)我是兇手,我就是兇手了,證據(jù)呢?
ok,我們就來(lái)尋找證據(jù),我們知道,Looper.loop()方法內(nèi)部,會(huì)開(kāi)啟一個(gè)死循環(huán),如下:
public static void loop() {for (;;) {Message msg = queue.next();if (msg == null) {return;}}}
可以看到,queue.next()這行代碼官方注釋了,有可能會(huì)被堵塞,什么時(shí)候會(huì)堵塞?沒(méi)有消息的時(shí)候,可見(jiàn),調(diào)用Looper.loop()方法所在的線程會(huì)進(jìn)入死循環(huán)。
那這個(gè)和我們的案件有什么關(guān)系呢?
這就要來(lái)說(shuō)說(shuō)RxJava的線程池了,上面TokenInterceptor回調(diào)所在的線程是RxJava的IO線程,而RxJava的IO線程池的配置,卻僅允許一條核心線程執(zhí)行任務(wù),當(dāng)任務(wù)在執(zhí)行,其它任務(wù)過(guò)來(lái)時(shí),必須等待至上一個(gè)任務(wù)結(jié)束。
在IoScheduler類中可以找到靜態(tài)內(nèi)部類ThreadWorker,ThreadWorker繼承至NewThreadWorker,在該類中,我們可以找到線程池對(duì)象,如下:
public class NewThreadWorker extends Scheduler.Worker implements Disposable {private final ScheduledExecutorService executor;public NewThreadWorker(ThreadFactory threadFactory) {executor = SchedulerPoolFactory.create(threadFactory);}}
SchedulerPoolFactory.create方法點(diǎn)進(jìn)去看看
public static ScheduledExecutorService create(ThreadFactory factory) {final ScheduledExecutorService exec = Executors.newScheduledThreadPool(1, factory);return exec;}
可以看到,這里傳了個(gè)1,就是核心線程的數(shù)量,繼續(xù)往下看,最終找到了創(chuàng)建線程池對(duì)象代碼,如下:
public ScheduledThreadPoolExecutor(int corePoolSize,ThreadFactory threadFactory) {super(corePoolSize, Integer.MAX_VALUE,DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,new DelayedWorkQueue(), threadFactory);}
這里簡(jiǎn)單解讀一下,該線程池核心線程數(shù)量為1,非核心線程數(shù)量無(wú)上限,非核心線程閑置時(shí)間超過(guò)10毫秒便會(huì)被回收,并使用了延遲隊(duì)列。
注意注意,前方高能預(yù)警
用簡(jiǎn)單的話來(lái)說(shuō),該線程池,同一時(shí)間,僅會(huì)執(zhí)行一個(gè)任務(wù),也就是串行,這也就解釋Looper與本案的關(guān)系,因?yàn)?code style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;">Looper.loop()所在線程進(jìn)入死循環(huán),該線程所在線程池收到其它任務(wù)時(shí),便必須得等待至上一個(gè)任務(wù)執(zhí)行完畢,然而上一個(gè)任務(wù)在死循環(huán),所以下一個(gè)任務(wù)永遠(yuǎn)得不到執(zhí)行,這也就是為什么請(qǐng)求代碼執(zhí)行了,請(qǐng)求卻沒(méi)發(fā)出去原因。
其它思考
到這,估計(jì)很多人會(huì)有疑問(wèn)
RxJava的Io線程池,是串行執(zhí)行的,那么它又是如何做到并行的呢?難道以前寫(xiě)的并行代碼,其實(shí)都是串行實(shí)現(xiàn)的?線程池已經(jīng)有任務(wù)在執(zhí)行了,為啥還會(huì)拿到該線程池執(zhí)行新的任務(wù)呢?
RxJava為啥不使用OkHttp內(nèi)部的線程池配置,只要有任務(wù)來(lái),都開(kāi)啟非核心線程去執(zhí)行?
ok,接下來(lái)一一解答
首先,第一個(gè),RxJava如何根據(jù)目前的Io線程池,做到并行任務(wù)?
其實(shí)很簡(jiǎn)單,在IoScheduler的靜態(tài)內(nèi)部類CachedWorkerPool中,維護(hù)了一個(gè)線程池隊(duì)列,每次收到新任務(wù),都會(huì)從隊(duì)列里面取出一個(gè)線程池去執(zhí)行任務(wù),如果沒(méi)有,則創(chuàng)建一個(gè)新的線程池,如下:
static final class CachedWorkerPool implements Runnable {private final ConcurrentLinkedQueueexpiringWorkerQueue final CompositeDisposable allWorkers;????private?final?ThreadFactory?threadFactory;????????????????????????ThreadWorker get() {if (allWorkers.isDisposed()) {return SHUTDOWN_THREAD_WORKER;}while (!expiringWorkerQueue.isEmpty()) {ThreadWorker threadWorker = expiringWorkerQueue.poll();if (threadWorker != null) {return threadWorker;}}ThreadWorker w = new ThreadWorker(threadFactory);allWorkers.add(w);return w;}void release(ThreadWorker threadWorker) {threadWorker.setExpirationTime(now() + keepAliveTime);expiringWorkerQueue.offer(threadWorker);}}
通過(guò)多個(gè)線程池,就達(dá)到了并行的效果;上面代碼release方法中,我們注意到,被回收的線程池,存活時(shí)間為60s,在CachedWorkerPool?構(gòu)造方法中,會(huì)開(kāi)啟一個(gè)定時(shí)任務(wù),每間隔60s,就會(huì)去檢查線程池隊(duì)列,如果線程池閑置超過(guò)60s,便會(huì)將線程池關(guān)閉,并從隊(duì)列中移除。
接著,回答第二個(gè)問(wèn)題,線程池已經(jīng)有任務(wù)在執(zhí)行了,為啥還會(huì)拿到該線程池執(zhí)行新的任務(wù)?
看了上面的代碼,其實(shí)就很好回答了,回收線程池有兩個(gè)條件會(huì)觸發(fā),一是任務(wù)正常執(zhí)行完畢,這個(gè)好理解,不做解釋,另外一個(gè)就是,任務(wù)被取消,比如,調(diào)用Disposable#isDisposed()方法取消任務(wù),但是該方法不會(huì)取消線程池里的任務(wù),這就導(dǎo)致了,線程池雖然被回收了,但線程池里的任務(wù)依然在執(zhí)行,所以下次拿到該線程池的任務(wù),只能等待。
最后,就是RxJava為何要如此設(shè)計(jì)線程池?
原因很簡(jiǎn)單,防止線程資源被浪費(fèi),如上面說(shuō)到的,線程池雖然被回收了,但里面的線程卻依然在執(zhí)行任務(wù),這樣的線程多了,無(wú)疑是一種浪費(fèi),怎么辦?依靠定時(shí)器,讓被回收的線程池在一定時(shí)間后,關(guān)閉任務(wù),并從隊(duì)列中移除。而如果直接通過(guò)線程池去回收線程,那么被Looper.loop()?的線程,進(jìn)入死循環(huán)后,將永遠(yuǎn)得不到回收。
到這,我也丟個(gè)問(wèn)題給大家,RxJava在將線程池丟進(jìn)緩存隊(duì)列時(shí),為啥不將線程池關(guān)閉掉?歡迎評(píng)論群留言討論
總結(jié)
回顧下案件,從妹子反饋的問(wèn)題,賬號(hào)被擠,重新登錄到首頁(yè)后,request2()方法內(nèi)的請(qǐng)求代碼執(zhí)行了,卻收不到回調(diào),線程池的原因請(qǐng)求壓沒(méi)有得到執(zhí)行,故收不到回調(diào),那為啥就request2()方法會(huì)出問(wèn)題呢?其實(shí)這是一種假象,只要被回收的線程池里還有未完成的任務(wù),那么該線程池再次執(zhí)行請(qǐng)求,都必須得等待。如果賬號(hào)在60s內(nèi)重復(fù)被擠3次,那么登錄到首頁(yè)后,3個(gè)請(qǐng)求都將得不到執(zhí)行,因?yàn)榛厥粘氐?個(gè)線程池都不能再執(zhí)行任務(wù)了,直到60s后,被計(jì)時(shí)器強(qiáng)制關(guān)閉并移除。
最后,提醒大家,一定要慎用Looper,不是任何時(shí)候都適合用Looper的,像妹子遇到的這種場(chǎng)景,完全可以用主線程的Handler post一個(gè)消息出去,然后處理業(yè)務(wù),亦或者通過(guò)EventBus、LiveData等發(fā)送消息到主線程,再處理相關(guān)邏輯。

如有收獲,歡迎「分享?
」
「點(diǎn)贊
」「評(píng)論?
」
看完本文有收獲?請(qǐng)轉(zhuǎn)發(fā)分享給更多人
? 開(kāi)發(fā)者全社區(qū)?
