<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Looper.loop()引發(fā)的慘案

          共 7205字,需瀏覽 15分鐘

           ·

          2020-09-17 17:33

          熱文推薦:

          作者:不怕天黑

          來(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è)MainActivityOnCreate方法,會(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;        
          @Override 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)所在的線程是RxJavaIO線程,而RxJavaIO線程池的配置,卻僅允許一條核心線程執(zhí)行任務(wù),當(dāng)任務(wù)在執(zhí)行,其它任務(wù)過(guò)來(lái)時(shí),必須等待至上一個(gè)任務(wù)結(jié)束。

          IoScheduler類中可以找到靜態(tài)內(nèi)部類ThreadWorkerThreadWorker繼承至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)

          • RxJavaIo線程池,是串行執(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 ConcurrentLinkedQueue expiringWorkerQueue    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ū)?

          5T技術(shù)資源大放送!包括但不限于:Android,Python,Java,大數(shù)據(jù),人工智能,AI等等。關(guān)注公眾號(hào)后回復(fù)「2T」,即可免費(fèi)獲取!
          瀏覽 33
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  嫩操 | 免费A∨在线观看 | 综合二区三区 | 久久九色| 国产做受 网站 |