騰訊面試掛了 | 面試官:說說Android中Looper在主線程中死循環爲什麼沒有導致界面的卡死?

前言

昨天一位朋友向我訴苦,說他騰訊二面掛了,面試官問了一個底層原理,他答不上來。

下面是這位朋友反饋的在騰訊面試的時候遇到的一個原題目:

Looper在主線程中死循環爲什麼沒有導致界面的卡死?

這位朋友直接蒙了,後面的面試就結束的很快,雖然被告知“回去等通知”,但是從面試官的態度很明顯可以看出來:基本上沒戲了。

正文

其實這個問題並不難回答。關鍵點如下:

  • 導致卡死的是在Ui線程中執行耗時操作導致界面出現掉幀,甚至ANR,Looper.loop()這個操作本身不會導致這個情況。
  • 有人可能會說,我在點擊事件中設置死循環會導致界面卡死,同樣都是死循環,不都一樣的嗎?Looper會在沒有消息的時候阻塞當前線程,釋放CPU資源,等到有消息到來的時候,再喚醒主線程。
  • App進程中是需要死循環的,如果循環結束的話,App進程就結束了。

當然,如果想要回答的更好,建議閱讀下文。

這是以前回答過的一個相似的問題,原文奉上。

題目的詳細描述:

app程序入口中爲主線程準備好了消息隊列

而根據Looper.loop()源碼可知裏面是一個死循環在遍歷消息隊列取消息

而且並也沒看見哪裏有相關代碼爲這個死循環準備了一個新線程去運轉,但是主線程卻並不會因爲Looper.loop()中的這個死循環卡死,爲什麼呢?

就像Activity的生命週期這些方法這些都是在主線程裏執行的,那這些生命週期方法是怎麼實現在死循環體外能夠執行起來的?

詳細答案

要完全徹底理解這個問題,需要準備以下4方面的知識:Process/Thread,Android Binder IPC,Handler/Looper/MessageQueue消息機制,Linux pipe/epoll機制。

總結一下,這個問題有3個疑惑:

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

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

3.Activity的生命週期這些方法這些都是在主線程裏執行的吧,那這些生命週期方法是怎麼實現在死循環體外能夠執行起來的?

針對這些疑惑,更進一步詳細地解答:

(1) 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本身不會導致應用卡死。

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

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

(3) 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運行過程的理解:

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. 線程1的AMS中調用線程2的ATP;(由於同一個進程的線程間資源共享,可以相互直接調用,但需要注意多線程併發問題)

  2. 線程2通過binder傳輸到App進程的線程4;

  3. 線程4通過handler消息機制,將暫停Activity的消息發送給主線程;

  4. 主線程在looper.loop()中循環遍歷消息,當收到暫停Activity的消息時,便將消息分發給ActivityThread.H.handleMessage()方法,再經過方法的調用,最後便會調用到Activity.onPause(),當onPause()處理完後,繼續循環loop下去。

最後

如果大家覺得這個回答還滿意,可以點贊支持一下。

學Android的朋友可以進我的GitHub:https://github.com/xieyuliang/Note-Android ,裏面有我自己多年學習Android的經驗,還包含不同方向的自學編程路線、面試題集合/面經、及系列技術文章等,持續更新中...

希望對大家的學習工作以及面試有幫助。

Android這條路,只有堅持學習,才能不斷進步,纔能有所收穫,願你我共勉。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章