Android每日一Looper.loop爲什麼不會阻塞掉UI線程?

基於https://www.wanandroid.com每日一問的筆記,做一些整理,方便自己進行查看和記憶。
原文鏈接:https://www.wanandroid.com/wenda/show/8685


Android中爲什麼主線程不會因爲Looper.loop()裏的死循環卡死?

  • 這裏涉及線程,先說說說進程/線程
    • 進程:每個app運行時前首先創建一個進程,該進程是由Zygote fork出來的,用於承載App上運行的各種Activity/Service等組件。進程對於上層應用來說是完全透明的,這也是google有意爲之,讓App程序都是運行在Android Runtime。大多數情況一個App就運行在一個進程中,除非在AndroidManifest.xml中配置Android:process屬性,或通過native代碼fork進程。
    • 線程:線程對應用來說非常常見,比如每次new Thread().start都會創建一個新的線程。該線程與App所在進程之間資源共享,從Linux角度來說進程與線程除了是否共享資源外,並沒有本質的區別,都是一個task_struct結構體,在CPU看來進程或線程無非就是一段可執行的代碼,CPU採用CFS調度算法,保證每個task都儘可能公平的享有CPU時間片。
  • 有了這麼準備,再說說死循環問題:
    • 對於線程既然是一段可執行的代碼,當可執行代碼執行完成後,線程生命週期便該終止了,線程退出。而對於主線程,我們是絕不希望會被運行一段時間,自己就退出,那麼如何保證能一直存活呢?簡單做法就是可執行代碼是能一直執行下去的,死循環便能保證不會被退出,例如,binder線程也是採用死循環的方法,通過循環方式不同與Binder驅動進行讀寫操作,當然並非簡單地死循環,無消息時會休眠。但這裏可能又引發了另一個問題,既然是死循環又如何去處理其他事務呢?通過創建新線程的方式。
    • 真正會卡死主線程的操作是在回調方法onCreate/onStart/onResume等操作時間過長,會導致掉幀,甚至發生ANR,looper.loop本身不會導致應用卡死

沒看見哪裏有相關代碼爲這個死循環準備了一個新線程去運轉?

  • 事實上,會在進入死循環之前便創建了新binder線程,在代碼ActivityThread.main()中:
public static void main(String[] args) { 
      .... 
      //創建Looper和MessageQueue對象,用於處理主線程的消息 
      Looper.prepareMainLooper(); 

      //創建ActivityThread對象 
      ActivityThread thread = new ActivityThread(); 

      //建立Binder通道 (創建新線程) 
      thread.attach(false); 

      Looper.loop(); //消息循環運行
      throw new RuntimeException("Main thread loop unexpectedly exited"); 
  }
  • thread.attach(false);便會創建一個Binder線程(具體是指ApplicationThread,Binder的服務端,用於接收系統服務AMS發送來的事件),該Binder線程通過Handler將Message發送給主線程,具體過程可查看 startService流程分析,這裏不展開說,簡單說Binder用於進程間通信,採用C/S架構。關於binder感興趣的朋友,可查看我回答的另一個知乎問題:
    爲什麼Android要採用Binder作爲IPC機制? - Gityuan的回答

  • 另外,ActivityThread實際上並非線程,不像HandlerThread類,ActivityThread並沒有真正繼承Thread類,只是往往運行在主線程,該人以線程的感覺,其實承載ActivityThread的主線程就是由Zygote fork而創建的進程。

  • 主線程的死循環一直運行是不是特別消耗CPU資源呢? 其實不然,這裏就涉及到Linux pipe/epoll機制,簡單說就是在主線程的MessageQueue沒有消息時,便阻塞在loop的queue.next()中的nativePollOnce()方法裏,詳情見Android消息機制1-Handler(Java層),此時主線程會釋放CPU資源進入休眠狀態,直到下個消息到達或者有事務發生,通過往pipe管道寫端寫入數據來喚醒主線程工作。這裏採用的epoll機制,是一種IO多路複用機制,可以同時監控多個描述符,當某個描述符就緒(讀或寫就緒),則立刻通知相應程序進行讀或寫操作,本質同步I/O,即讀寫是阻塞的。 所以說,主線程大多數時候都是處於休眠狀態,並不會消耗大量CPU資源。

Activity的生命週期是怎麼實現在死循環體外能夠執行起來的?

  • ActivityThread的內部類H繼承於Handler,通過handler消息機制,簡單說Handler機制用於同一個進程的線程間通信。
  • Activity的生命週期都是依靠主線程的Looper.loop,當收到不同Message時則採用相應措施:在H.handleMessage(msg)方法中,根據接收到不同的msg,執行相應的生命週期。
  • 比如收到msg=H.LAUNCH_ACTIVITY,則調用ActivityThread.handleLaunchActivity()方法,最終會通過反射機制,創建Activity實例,然後再執行Activity.onCreate()等方法;
  • 再比如收到msg=H.PAUSE_ACTIVITY,則調用ActivityThread.handlePauseActivity()方法,最終會執行Activity.onPause()等方法。 上述過程,我只挑核心邏輯講,真正該過程遠比這複雜。
  • 主線程的消息又是哪來的呢?當然是App進程中的其他線程通過Handler發送給主線程,請看接下來的內容:
  • 最後,從進程與線程間通信的角度,通過一張圖加深大家對App運行過程的理解:
    image
  • system_server進程是系統進程,java framework框架的核心載體,裏面運行了大量的系統服務,比如這裏提供ApplicationThreadProxy(簡稱ATP),ActivityManagerService(簡稱AMS),這個兩個服務都運行在system_server進程的不同線程中,由於ATP和AMS都是基於IBinder接口,都是binder線程,binder線程的創建與銷燬都是由binder驅動來決定的。
  • App進程則是我們常說的應用程序,主線程主要負責Activity/Service等組件的生命週期以及UI相關操作都運行在這個線程; 另外,每個App進程中至少會有兩個binder線程 ApplicationThread(簡稱AT)和ActivityManagerProxy(簡稱AMP),除了圖中畫的線程,其中還有很多線程,比如signal catcher線程等,這裏就不一一列舉。
  • Binder用於不同進程之間通信,由一個進程的Binder客戶端向另一個進程的服務端發送事務,比如圖中線程2向線程4發送事務;而handler用於同一個進程中不同線程的通信,比如圖中線程4向主線程發送消息。
  • 結合圖說說Activity生命週期,比如暫停Activity,流程如下:
    • 線程1的AMS中調用線程2的ATP;(由於同一個進程的線程間資源共享,可以相互直接調用,但需要注意多線程併發問題)
    • 線程2通過binder傳輸到App進程的線程4;
    • 線程4通過handler消息機制,將暫停Activity的消息發送給主線程;
    • 主線程在looper.loop()中循環遍歷消息,當收到暫停Activity的消息時,便將消息分發給ActivityThread.H.handleMessage()方法,再經過方法的調用,最後便會調用到Activity.onPause(),當onPause()處理完後,繼續循環loop下去。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章