騰訊Android面試:Handler中有Loop死循環,爲什麼沒有阻塞主線程,原理是什麼

面試官: Handler中有Loop死循環,爲什麼沒有阻塞主線程,原理是什麼
心理分析:該問題很難被考到,但是如果一旦問到,100%會回答不上來。開發者很難注意到一個主線程的四循環居然沒有阻塞住主線程

求職者:應該從 主線程的消息循環機制 與Linux的循環異步等待作用講起。最後將handle引起的內存泄漏,內存泄漏一定是一個加分項

先上一份整理好的面試目錄

騰訊Android面試:Handler中有Loop死循環,爲什麼沒有阻塞主線程,原理是什麼

前言

Android的消息機制主要是指Handler的運行機制,對於大家來說Handler已經是輕車熟路了,可是真的掌握了Handler?本文主要通過幾個問題圍繞着Handler展開深入並拓展的瞭解。

站在巨人的肩膀上會看的更遠。大家有興趣的也可以到Gityuan的博客上多瞭解瞭解,全部都是乾貨。而且他寫的東西比較權威,畢竟也是小米系統工程師的骨幹成員。

Questions

  1. Looper 死循環爲什麼不會導致應用卡死,會消耗大量資源嗎?

  2. 主線程的消息循環機制是什麼(死循環如何處理其它事務)?

  3. ActivityThread 的動力是什麼?(ActivityThread執行Looper的線程是什麼)

  4. Handler 是如何能夠線程切換,發送Message的?(線程間通訊)

  5. 子線程有哪些更新UI的方法。

  6. 子線程中Toast,showDialog,的方法。(和子線程不能更新UI有關嗎)

  7. 如何處理Handler 使用不當導致的內存泄露?

回答一: Looper 死循環爲什麼不會導致應用卡死?

線程默認沒有Looper的,如果需要使用Handler就必須爲線程創建Looper。我們經常提到的主線程,也叫UI線程,它就是ActivityThread,ActivityThread被創建時就會初始化Looper,這也是在主線程中默認可以使用Handler的原因。

首先我們看一段代碼

 new Thread(new Runnable() {
        @Override
        public void run() {
            Log.e("qdx", "step 0 ");
            Looper.prepare();

            Toast.makeText(MainActivity.this, "run on Thread", Toast.LENGTH_SHORT).show();

            Log.e("qdx", "step 1 ");
            Looper.loop();

            Log.e("qdx", "step 2 ");

        }
    }).start();

我們知道Looper.loop();裏面維護了一個死循環方法,所以按照理論,上述代碼執行的應該是 step 0 –>step 1 也就是說循環在Looper.prepare();與Looper.loop();之間。

在子線程中,如果手動爲其創建了Looper,那麼在所有的事情完成以後應該調用quit方法來終止消息循環,否則這個子線程就會一直處於等待(阻塞)狀態,而如果退出Looper以後,這個線程就會立刻(執行所有方法並)終止,因此建議不需要的時候終止Looper。

執行結果也正如我們所說,這時候如果瞭解了ActivityThread,並且在main方法中我們會看到主線程也是通過Looper方式來維持一個消息循環

public static void main(String[] args) {
    Looper.prepareMainLooper();//創建Looper和MessageQueue對象,用於處理主線程的消息

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

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }

    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    Looper.loop();

    //如果能執行下面方法,說明應用崩潰或者是退出了...
    throw new RuntimeException("Main thread loop unexpectedly exited");
}

那麼回到我們的問題上,這個死循環會不會導致應用卡死,即使不會的話,它會慢慢的消耗越來越多的資源嗎?

對於線程即是一段可執行的代碼,當可執行代碼執行完成後,線程生命週期便該終止了,線程退出。而對於主線程,我們是絕不希望會被運行一段時間,自己就退出,那麼如何保證能一直存活呢?簡單做法就是可執行代碼是能一直執行下去的,死循環便能保證不會被退出,例如,binder線程也是採用死循環的方法,通過循環方式不同與Binder驅動進行讀寫操作,當然並非簡單地死循環,無消息時會休眠。但這裏可能又引發了另一個問題,既然是死循環又如何去處理其他事務呢?通過創建新線程的方式。真正會卡死主線程的操作是在回調方法onCreate/onStart/onResume等操作時間過長,會導致掉幀,甚至發生ANR,looper.loop本身不會導致應用卡死。

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

回答二:主線程的消息循環機制是什麼?

事實上,會在進入死循環之前便創建了新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");
}

Activity的生命週期都是依靠主線程的Looper.loop,當收到不同Message時則採用相應措施:一旦退出消息循環,那麼你的程序也就可以退出了。 從消息隊列中取消息可能會阻塞,取到消息會做出相應的處理。如果某個消息處理時間過長,就可能會影響UI線程的刷新速率,造成卡頓的現象。
thread.attach(false)方法函數中便會創建一個Binder線程(具體是指ApplicationThread,Binder的服務端,用於接收系統服務AMS發送來的事件),該Binder線程通過Handler將Message發送給主線程。「Activity 啓動過程」

比如收到msg=H.LAUNCH_ACTIVITY,則調用ActivityThread.handleLaunchActivity()方法,最終會通過反射機制,創建Activity實例,然後再執行Activity.onCreate()等方法;

再比如收到msg=H.PAUSE_ACTIVITY,則調用ActivityThread.handlePauseActivity()方法,最終會執行Activity.onPause()等方法。

主線程的消息又是哪來的呢?當然是App進程中的其他線程通過Handler發送給主線程

system_server進程

system_server進程是系統進程,java framework框架的核心載體,裏面運行了大量的系統服務,比如這裏提供ApplicationThreadProxy(簡稱ATP),ActivityManagerService(簡稱AMS),這個兩個服務都運行在system_server進程的不同線程中,由於ATP和AMS都是基於IBinder接口,都是binder線程,binder線程的創建與銷燬都是由binder驅動來決定的。

App進程

App進程則是我們常說的應用程序,主線程主要負責Activity/Service等組件的生命週期以及UI相關操作都運行在這個線程; 另外,每個App進程中至少會有兩個binder線程 ApplicationThread(簡稱AT)和ActivityManagerProxy(簡稱AMP),除了圖中畫的線程,其中還有很多線程

Binder

Binder用於不同進程之間通信,由一個進程的Binder客戶端向另一個進程的服務端發送事務,比如圖中線程2向線程4發送事務;而handler用於同一個進程中不同線程的通信,比如圖中線程4向主線程發送消息。

騰訊Android面試:Handler中有Loop死循環,爲什麼沒有阻塞主線程,原理是什麼

結合圖說說Activity生命週期,比如暫停Activity,流程如下:

1.線程1的AMS中調用線程2的ATP;(由於同一個進程的線程間資源共享,可以相互直接調用,但需要注意多線程併發問題)
2.線程2通過binder傳輸到App進程的線程4;
3.線程4通過handler消息機制,將暫停Activity的消息發送給主線程;
4.主線程在looper.loop()中循環遍歷消息,當收到暫停Activity的消息時,便將消息分發給 ActivityThread.H.handleMessage()方法,再經過方法的調用,
5.最後便會調用到Activity.onPause(),當onPause()處理完後,繼續循環loop下去。

補充:

ActivityThread的main方法主要就是做消息循環,一旦退出消息循環,那麼你的程序也就可以退出了。
從消息隊列中取消息可能會阻塞,取到消息會做出相應的處理。如果某個消息處理時間過長,就可能會影響UI線程的刷新速率,造成卡頓的現象。

最後通過《Android開發藝術探索》的一段話總結 :

ActivityThread通過ApplicationThread和AMS進行進程間通訊,AMS以進程間通信的方式完成ActivityThread的請求後會回調ApplicationThread中的Binder方法,然後ApplicationThread會向H發送消息,H收到消息後會將ApplicationThread中的邏輯切換到ActivityThread中去執行,即切換到主線程中去執行,這個過程就是。主線程的消息循環模型

另外,ActivityThread實際上並非線程,不像HandlerThread類,ActivityThread並沒有真正繼承Thread類

那麼問題又來了,既然ActivityThread不是一個線程,那麼ActivityThread中Looper綁定的是哪個Thread,也可以說它的動力是什麼?

回答三:ActivityThread 的動力是什麼?

進程每個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時間片。

其實承載ActivityThread的主線程就是由Zygote fork而創建的進程。

回答四:Handler 是如何能夠線程切換

其實看完上面我們大致也清楚線程間是共享資源的。所以Handler處理不同線程問題就只要注意異步情況即可。

這裏再引申出Handler的一些小知識點。Handler創建的時候會採用當前線程的Looper來構造消息循環系統Looper在哪個線程創建,就跟哪個線程綁定**,並且Handler是在他關聯的Looper對應的線程中處理消息的。(敲黑板)

那麼Handler內部如何獲取到當前線程的Looper呢—–ThreadLocal。ThreadLocal可以在不同的線程中互不干擾的存儲並提供數據,通過ThreadLocal可以輕鬆獲取每個線程的Looper。當然需要注意的是①線程是默認沒有Looper的,如果需要使用Handler,就必須爲線程創建Looper。我們經常提到的主線程,也叫UI線程,它就是ActivityThread,②ActivityThread被創建時就會初始化Looper,這也是在主線程中默認可以使用Handler的原因。

系統爲什麼不允許在子線程中訪問UI?(摘自《Android開發藝術探索》) 這是因爲Android的UI控件不是線程安全的,如果在多線程中併發訪問可能會導致UI控件處於不可預期的狀態,那麼爲什麼系統不對UI控件的訪問加上鎖機制呢?缺點有兩個: ①首先加上鎖機制會讓UI訪問的邏輯變得複雜 ②鎖機制會降低UI訪問的效率,因爲鎖機制會阻塞某些線程的執行。 所以最簡單且高效的方法就是採用單線程模型來處理UI操作。

那麼問題又來了,子線程一定不能更新UI?

看到這裏,又留下兩個知識點等待下篇詳解:View的繪製機制與Android Window內部機制。

回答五:子線程有哪些更新UI的方法

主線程中定義Handler,子線程通過mHandler發送消息,主線程Handler的handleMessage更新UI。 用Activity對象的runOnUiThread方法。 創建Handler,傳入getMainLooper。 View.post(Runnable r) 。
runOnUiThread 第一種咱們就不分析了,我們來看看第二種比較常用的寫法。

先重新溫習一下上面說的

Looper在哪個線程創建,就跟哪個線程綁定,並且Handler是在他關聯的Looper對應的線程中處理消息的。(敲黑板)

   new Thread(new Runnable() {
        @Override
        public void run() {

            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    //DO UI method

                }
            });

        }
    }).start();

final Handler mHandler = new Handler();

public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);//子線程(非UI線程)
    } else {
        action.run();
    }
}

進入Activity類裏面,可以看到如果是在子線程中,通過mHandler發送的更新UI消息。 而這個Handler是在Activity中創建的,也就是說在主線程中創建,所以便和我們在主線程中使用Handler更新UI沒有差別。 因爲這個Looper,就是ActivityThread中創建的Looper(Looper.prepareMainLooper())。

創建Handler,傳入getMainLooper 那麼同理,我們在子線程中,是否也可以創建一個Handler,並獲取MainLooper,從而在子線程中更新UI呢? 首先我們看到,在Looper類中有靜態對象sMainLooper,並且這個sMainLooper就是在ActivityThread中創建的MainLooper。

private static Looper sMainLooper;  // guarded by Looper.class

public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}

所以不用多說,我們就可以通過這個sMainLooper來進行更新UI操作。

 new Thread(new Runnable() {
        @Override
        public void run() {

            Log.e("qdx", "step 1 "+Thread.currentThread().getName());

            Handler handler=new Handler(getMainLooper());
            handler.post(new Runnable() {
                @Override
                public void run() {

                    //Do Ui method
                    Log.e("qdx", "step 2 "+Thread.currentThread().getName());
                }
            });

        }
    }).start();

View.post(Runnable r) 老樣子,我們點入源碼

//View

/**
 * <p>Causes the Runnable to be added to the message queue.
 * The runnable will be run on the user interface thread.</p>
 *
 * @param action The Runnable that will be executed.
 *
 * @return Returns true if the Runnable was successfully placed in to the
 *         message queue.  Returns false on failure, usually because the
 *         looper processing the message queue is exiting.
 *
 */
public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action); //一般情況走這裏
    }

    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().post(action);
    return true;
}

    /**
     * A Handler supplied by a view's {@link android.view.ViewRootImpl}. This
     * handler can be used to pump events in the UI events queue.
     */
    final Handler mHandler;

居然也是Handler從中作祟,根據Handler的註釋,也可以清楚該Handler可以處理UI事件,也就是說它的Looper也是主線程的sMainLooper。這就是說我們常用的更新UI都是通過Handler實現的。

另外更新UI 也可以通過AsyncTask來實現,難道這個AsyncTask的線程切換也是通過 Handler 嗎? 沒錯,也是通過Handler……

Handler實在是......

回答六:子線程中Toast,showDialog,的方法

可能有些人看到這個問題,就會想: 子線程本來就不可以更新UI的啊 而且上面也說了更新UI的方法

兄臺且慢,且聽我把話寫完

  new Thread(new Runnable() {
        @Override
        public void run() {

            Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show();//崩潰無疑

        }
    }).start();

看到這個崩潰日誌,是否有些疑惑,因爲一般如果子線程不能更新UI控件是會報如下錯誤的(子線程不能更新UI)

所以子線程不能更新Toast的原因就和Handler有關了,據我們瞭解,每一個Handler都要有對應的Looper對象,那麼。 滿足你。

   new Thread(new Runnable() {
        @Override
        public void run() {

            Looper.prepare();
            Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show();
            Looper.loop();

        }
    }).start();

這樣便能在子線程中Toast,不是說子線程…? 老樣子,我們追根到底看一下Toast內部執行方式。

//Toast

/**
 * Show the view for the specified duration.
 */
public void show() {
INotificationManager service = getService();//從SMgr中獲取名爲notification的服務
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;

try {
    service.enqueueToast(pkg, tn, mDuration);//enqueue? 難不成和Handler的隊列有關?
} catch (RemoteException e) {
    // Empty
}

}

在show方法中,我們看到Toast的show方法和普通UI 控件不太一樣,並且也是通過Binder進程間通訊方法執行Toast繪製。這其中的過程就不在多討論了,有興趣的可以在NotificationManagerService類中分析。

現在把目光放在TN 這個類上(難道越重要的類命名就越簡潔,如H類),通過TN 類,可以瞭解到它是Binder的本地類。在Toast的show方法中,將這個TN對象傳給NotificationManagerService就是爲了通訊!並且我們也在TN中發現了它的show方法。

private static class TN extends ITransientNotification.Stub {//Binder服務端的具體實現類

/**
 * schedule handleShow into the right thread
 */
@Override
public void show(IBinder windowToken) {
    mHandler.obtainMessage(0, windowToken).sendToTarget();
}

final Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        IBinder token = (IBinder) msg.obj;
        handleShow(token);
    }
};

}

看完上面代碼,就知道子線程中Toast報錯的原因,因爲在TN中使用Handler,所以需要創建Looper對象。 那麼既然用Handler來發送消息,就可以在handleMessage中找到更新Toast的方法。 在handleMessage看到由handleShow處理。

//Toast的TN類

public void handleShow(IBinder windowToken) {

        ``````
        mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);

        mParams.x = mX;
        mParams.y = mY;
        mParams.verticalMargin = mVerticalMargin;
        mParams.horizontalMargin = mHorizontalMargin;
        mParams.packageName = packageName;
        mParams.hideTimeoutMilliseconds = mDuration ==
            Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
        mParams.token = windowToken;
        if (mView.getParent() != null) {
            mWM.removeView(mView);
        }
        mWM.addView(mView, mParams);//使用WindowManager的addView方法
        trySendAccessibilityEvent();
    }
}
看到這裏就可以總結一下:

Toast本質是通過window顯示和繪製的(操作的是window),而主線程不能更新UI 是因爲ViewRootImpl的checkThread方法在Activity維護的View樹的行爲。 Toast中TN類使用Handler是爲了用隊列和時間控制排隊顯示Toast,所以爲了防止在創建TN時拋出異常,需要在子線程中使用Looper.prepare();和Looper.loop();(但是不建議這麼做,因爲它會使線程無法執行結束,導致內存泄露)

Dialog亦是如此。同時我們又多了一個知識點要去研究:Android 中Window是什麼,它內部有什麼機制?

#### 回答七:如何處理Handler 使用不當導致的內存泄露? 首先上文在子線程中爲了節目效果,使用如下方式創建Looper
   Looper.prepare();
    ``````
    Looper.loop();
實際上這是非常危險的一種做法

> 在子線程中,如果手動爲其創建Looper,那麼在所有的事情完成以後應該調用quit方法來終止消息循環,否則這個子線程就會一直處於等待的狀態,而如果退出Looper以後,這個線程就會立刻終止,因此建議不需要的時候終止Looper。(【 Looper.myLooper().quit(); 】)

那麼,如果在Handler的handleMessage方法中(或者是run方法)處理消息,如果這個是一個延時消息,會一直保存在主線程的消息隊列裏,並且會影響系統對Activity的回收,造成內存泄露。

具體可以參考Handler內存泄漏分析及解決

總結一下,解決Handler內存泄露主要2點

1 有延時消息,要在Activity銷燬的時候移除Messages
 2 匿名內部類導致的泄露改爲匿名靜態內部類,並且對上下文或者Activity使用弱引用。

## 總結
想不到Handler居然可以騰出這麼多浪花,與此同時感謝前輩的摸索。

另外Handler還有許多不爲人知的祕密,等待大家探索,下面我再簡單的介紹兩分鐘

- HandlerThread
- IdleHandler

HandlerThread

> HandlerThread繼承Thread,它是一種可以使用Handler的Thread,它的實現也很簡單,在run方法中也是通過Looper.prepare()來創建消息隊列,並通過Looper.loop()來開啓消息循環(與我們手動創建方法基本一致),這樣在實際的使用中就允許在HandlerThread中創建Handler了。
由於HandlerThread的run方法是一個無限循環,因此當不需要使用的時候通過quit或者quitSafely方法來終止線程的執行。

HandlerThread的本質也是線程,所以切記關聯的Handler中處理消息的handleMessage爲子線程。

IdleHandler

/**

  • Callback interface for discovering when a thread is going to block
  • waiting for more messages.
    */
    public static interface IdleHandler {
    /**
    • Called when the message queue has run out of messages and will now
    • wait for more. Return true to keep your idle handler active, false
    • to have it removed. This may be called if there are still messages
    • pending in the queue, but they are all scheduled to be dispatched
    • after the current time.
      */
      boolean queueIdle();
      }
      
      根據註釋可以瞭解到,這個接口方法是在消息隊列全部處理完成後或者是在阻塞的過程中等待更多的消息的時候調用的,返回值false表示只回調一次,true表示可以接收多次回調。

具體使用如下代碼

   Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {

            return false;
        }
    });

另外提供一個小技巧:在HandlerThread中獲取Looper的MessageQueue方法之反射。

因爲

Looper.myQueue()如果在主線程調用就會使用主線程looper 使用handlerThread.getLooper().getQueue()最低版本需要23 //HandlerThread中獲取MessageQueue

     Field field = Looper.class.getDeclaredField("mQueue");
        field.setAccessible(true);
        MessageQueue queue = (MessageQueue) field.get(handlerThread.getLooper());

那麼Android的消息循環機制是通過Handler,是否可以通過IdleHandler來判斷Activity的加載和繪製情況(measure,layout,draw等)呢?並且IdleHandler是否也隱藏着不爲人知的特殊功能?

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