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

面試官: Handler中有Loop死循環,爲何沒有阻塞主線程,原理是什麼
心理分析:該問題很難被考到,可是若是一旦問到,100%會回答不上來。開發者很難注意到一個主線程的四循環竟然沒有阻塞住主線程
求職者:應該從 主線程的消息循環機制 與Linux的循環異步等待做用講起。最後將handle引發的內存泄漏,內存泄漏必定是一個加分項

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

前言

Android的消息機制主要是指Handler的運行機制,對於你們來講Handler已是輕車熟路了,但是真的掌握了Handler?本文主要經過幾個問題圍繞着Handler展開深刻並拓展的瞭解。android

站在巨人的肩膀上會看的更遠。你們有興趣的也能夠到Gityuan的博客上多瞭解瞭解,所有都是乾貨。並且他寫的東西比較權威,畢竟也是小米系統工程師的骨幹成員。git

Questions

  1. Looper 死循環爲何不會致使應用卡死,會消耗大量資源嗎?
  2. 主線程的消息循環機制是什麼(死循環如何處理其它事務)?
  3. ActivityThread 的動力是什麼?(ActivityThread執行Looper的線程是什麼)
  4. Handler 是如何可以線程切換,發送Message的?(線程間通信)
  5. 子線程有哪些更新UI的方法。
  6. 子線程中Toast,showDialog,的方法。(和子線程不能更新UI有關嗎)
  7. 如何處理Handler 使用不當致使的內存泄露?

回答一: Looper 死循環爲何不會致使應用卡死?github

線程默認沒有Looper的,若是須要使用Handler就必須爲線程建立Looper。咱們常常提到的主線程,也叫UI線程,它就是 ActivityThreadActivityThread被建立時就會初始化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層)併發

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

事實上,會在進入死循環以前便建立了新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向主線程發送消息。

結合圖說說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呢—–ThreadLocalThreadLocal能夠在不一樣的線程中互不干擾的存儲並提供數據,經過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,傳入getMainLooperView.post(Runnabler)
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(Runnabler) 老樣子,咱們點入源碼

//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 是由於ViewRootImplcheckThread方法在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`是否也隱藏着鮮爲人知的特殊功能?
**更多面試內容,面試專題,flutter視頻 全套,音視頻從0到高手開發。**
關注**GitHub:**https://github.com/xiangjiana/Android-MS
**免費獲取面試PDF合集**
![](https://upload-images.jianshu.io/upload_images/14140248-c1ea465d47338eed.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
相關文章
相關標籤/搜索