天天看點

Android的消息循環機制:Handler

前言

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

Questions

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

1. 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");

    }

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

摘自:Gityuan

對于線程即是一段可執行的代碼,當可執行代碼執行完成後,線程生命周期便該終止了,線程退出。而對于主線程,我們是絕不希望會被運作一段時間,自己就退出,那麼如何保證能一直存活呢?簡單做法就是可執行代碼是能一直執行下去的,死循環便能保證不會被退出,例如,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層)

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");

    }

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的消息時,便将消息分發給 
  5. ActivityThread.H.handleMessage()方法,再經過方法的調用, 
  6. 最後便會調用到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,也可以說它的動力是什麼?

3. 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而建立的程序。

4. Handler 是如何能夠線程切換

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

這裡再引申出Handler的一些小知識點。 

Handler建立的時候會采用目前線程的Looper來構造消息循環系統,Looper在哪個線程建立,就跟哪個線程綁定,并且Handler是在他關聯的Looper對應的線程中處理消息的。(敲黑闆)

那麼Handler内部如何擷取到目前線程的Looper呢—–ThreadLocal。ThreadLocal可以在不同的線程中互不幹擾的存儲并提供資料,通過ThreadLocal可以輕松擷取每個線程的Looper。當然需要注意的是①線程是預設沒有Looper的,如果需要使用Handler,就必須為線程建立Looper。我們經常提到的主線程,也叫UI線程,它就是ActivityThread,②ActivityThread被建立時就會初始化Looper,這也是在主線程中預設可以使用Handler的原因。

系統為什麼不允許在子線程中通路UI?

這是因為Android的UI控件不是線程安全的,如果在多線程中并發通路可能會導緻UI控件處于不可預期的狀态,那麼為什麼系統不對UI控件的通路加上鎖機制呢?缺點有兩個: 

①首先加上鎖機制會讓UI通路的邏輯變得複雜 

②鎖機制會降低UI通路的效率,因為鎖機制會阻塞某些線程的執行。 

是以最簡單且高效的方法就是采用單線程模型來處理UI操作。

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

  1. 主線程中定義Handler,子線程通過mHandler發送消息,主線程Handler的handleMessage更新UI。
  2. 用Activity對象的runOnUiThread方法。
  3. 建立Handler,傳入getMainLooper。
  4. 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();

//Activity

    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

    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;

    }

        final Handler mHandler;

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

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

沒錯,也是通過Handler……

6.子線程中Toast,showDialog,的方法。

可能有些人看到這個問題,就會想: 子線程本來就不可以更新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

    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服務端的具體實作類

        @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();(但是不建議這麼做,因為它會使線程無法執行結束,導緻記憶體洩露)

7. 如何處理Handler 使用不當導緻的記憶體洩露?

首先上文在子線程中為了節目效果,使用如下方式建立Looper

                Looper.prepare();

                ``````

                Looper.loop();

實際上這是非常危險的一種做法 

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

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

總結一下,解決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

    public static interface IdleHandler {

        boolean queueIdle();

    }

根據注釋可以了解到,這個接口方法是在消息隊列全部處理完成後或者是在阻塞的過程中等待更多的消息的時候調用的,傳回值false表示隻回調一次,true表示可以接收多次回調。

具體使用如下代碼

        Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {

            @Override

            public boolean queueIdle() {

                return false;

            }

        });

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

因為

  1. Looper.myQueue()如果在主線程調用就會使用主線程looper
  2. 使用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是否也隐藏着不為人知的特殊功能?

繼續閱讀