全網最清晰易懂淺析Handler

来源:http://www.cnblogs.com/xiaoc024/archive/2016/06/22/5606651.html
-Advertisement-
Play Games

前言:本文針對讀者:1.零基礎的。2.看了各種教程依然毫無頭緒的。3.對 Handler 略有瞭解但是思維混沌的。對於想較為深入理解 Handler 的,本文對你幫助不大。本文將以圖文並茂的方式,帶你領略整個 Handler 的工作機制。零基礎的讀起來可能會略有障礙,建議反覆閱讀。是否是全網最清晰, ...


前言:本文針對讀者:1.零基礎的。2.看了各種教程依然毫無頭緒的。3.對 Handler 略有瞭解但是思維混沌的。對於想較為深入理解 Handler 的,本文對你幫助不大。本文將以圖文並茂的方式,帶你領略整個 Handler 的工作機制。零基礎的讀起來可能會略有障礙,建議反覆閱讀。是否是全網最清晰,還請各位看官細細品讀。

首先來看一張圖:

這裡寫圖片描述

註:無論是否零基礎,都建議在新視窗中打開圖片或保存圖片至本地方便隨時查看。下文中將頻繁用到該圖。

一句話概括整張圖的工作流程:

Looper 調用 loop() 函數從 MessageQueue 中依次取出帶有 target 屬性的 Message 並分發給對應的 target 進行處理。

零基礎的同學肯定不知道我在說什麼,且聽我一一道來。

首先我們要知道,什麼是 Handler.簡而言之:

Handler 是 Android 系統提供的一種方便線程間進行通信的機制。我們普遍用 Handler 機制實現在子線程中更新 UI.

這裡插一句,Android 系統設計時將 UI 線程也就是主線程設計成線程不安全的以提高效率,並規定在任何子線程中不可以進行對 UI 修改的操作。想修改 UI ,必須在主線程中。此時我們子線程完成耗時的工作,想把結果呈現到 UI 上,怎麼辦呢?此時就需要用到 Handler與主線程進行通信。那 Handler 整套機制是如何工作的呢?

對 Handler 稍微有些瞭解的同學一定知道,Handler 是與 Looper 和 MessageQueue 協同工作的。下麵我將分別從 Looper, Handler 的視角從零構造一個 Handler 工作環境。

Looper 視角

1.通過在一個線程中調用 Looper.prepare() 函數

private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {//保證唯一性
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));//創建並放入容器
    }

sThreadLocal是 Handler 中 LocalThread 類型的成員變數,你可以簡單的把他理解成一種方便線程間進行通信的容器。

2.既然創建了 Looper 那我們看看被調用 Looper 的構造函數是什麼樣的。

private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

可以看出,這裡new出了Looper最重要的成員變數mQueue,它就是整個消息機制的載體MessageQueue . 此時可以通過觀察圖片看到,在一個線程中三層的 Looper 工作環境已經搭建完畢。從外到內分別是 LocalThread,Looper,MessageQueue.

3.調用 Looper 的 loop() 函數開始工作。對應圖中的鉤子,每次從消息隊列中夾一個消息出來。源代碼不需要全部都懂,只看我寫的中文註釋找到圖中對應動作即可。

public static void loop() {
        final Looper me = myLooper();//myLooper(){return sThreadLocal.get();}通過容器獲得唯一的 Looper 對象
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();
        //整個 loop() 函數的核心
        for (;;) {
            //開始從消息隊列里鉤消息了
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
            /*最重要的一行回調:如果取出的消息不空,調用消息中 target 的 dispatchMessage() 方法分發消息。
            這裡的 target 就是一個 Handler,可以從 Message 的類圖看到這個成員變數。他是 Message 中最重要的成員變數。
            這一步的動作對應圖中將消息鉤到 Handler 等待處理。*/
            msg.target.dispatchMessage(msg);

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            msg.recycleUnchecked();
        }
    }

以上就是Looper視角的從初始化到開始消息輪詢工作的全部過程。零基礎的同學再看看“一句話概括整張圖工作流程”:

Looper 調用 loop() 函數從 MessageQueue 中依次取出帶有 target 屬性的 Message 並分發給對應的 target 進行處理。

是否有了更清晰的認識呢?

Handler 視角

在介紹 Handler 視角之前,我們首先要瞭解兩個前提,這兩個前提很重要,首先要記住然後要理解。

第一個前提:所有Handler的初始化函數都會與一個 Looper 進行關聯。所以在初始化Handler之前請確保你可以提供一個 Looper 供 Handler 關聯。

第二個前提:如你所見,圖中消息隊列中存放的都是Message類型的消息,所以無論 Handler 採用何種方式與消息隊列進行通信,最終通信的內容都會被封裝成Message類型。

1.基於以上兩個前提我們首先看一下Handler的構造函數。常用的基本就三種:

public Handler();
public Handler(Callback callback);
public Handler(Looper looper);

Callback 是 Handler 提供的一個公有介面:

public interface Callback {
        public boolean handleMessage(Message msg);
    }

先不用理解,稍後我會在講解dispatchMessage()函數時對他進行介紹。

有的同學可能會有疑問了:你不是說Handler所有構造函數都會與一個 Looper 進行關聯嗎?上面給出的前兩個,我也沒看出進行關聯啊?

先別急,我們以第一個空參數表的構造函數為例。先來看下源代碼是怎麼寫的。

public Handler() {
        this(null, false);//調用下麵的函數。
    }
public Handler(Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }

        mLooper = Looper.myLooper();//關聯 Looper. 還記得在loop()函數中如何獲得Looper對象嗎?他們調用的是同一個函數。
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");//這個異常保證了第一個前提哦。
        }
        mQueue = mLooper.mQueue;//關聯MessageQueue
        mCallback = callback;//如果是Handler()調用過來的,mCallback就是null
        mAsynchronous = async;
    }

由此可以看到即使調用空的構造函數,在內部也會通過mLooper = Looper.myLooper();自動關聯 Looper .所以我們在調用空的構造函數初始化Handler前,一定要先調用 Looper.prepare() 來把 Looper 放到 LocalThread 里。大概就是這樣一個效果:

class LooperThread extends Thread {

        public Handler mHandler;

        public void run() {
            Looper.prepare();//不調用會出異常哦
            mHandler = new Handler() {
                public void handleMessage(Message msg) {
                    // process incoming messages here
                }
            };
            Looper.loop();
        }
    }

所以在一個線程中,我們可以通過這三種常見的構造函數構造許多個 Handler .但是一個線程中因為 Looper.prepare() 方法保證了一個線程中 Looper 的唯一性。這好比一個小部門中,有一個部門經理(Looper),手下眾多形態各異的員工(Handler)。

2.構造完了 Handler ,也就代表了一個 Handler 與一個 Looper 進行了關聯。通過 Handler 的名字我們可以看出,他的本質是一個處理者,處理 Looper 給他鉤過去的消息。這引申出了兩個問題:

I: 消息是怎麼進到 MessageQueue 中等待被鉤的?
II: Handler是如何處理鉤過來的消息的?

對於

I: 消息是怎麼進到 MessageQueue 中等待被鉤的?

由圖可知,以發送者身份劃分,有兩大類方式。一類是 Handler 對象調用三種常用函數進行發送。另一類是 Message 對象調用 sendToTarget() 函數。

先說Handler的三種常用函數:

public final boolean post(Runnable r);
public final boolean sendMessage(Message msg);
public final boolean sendEmptyMessage(int what);

雖然這三種函數傳入的參數類型並不統一,不過不要忘了前面說的第二個前提:

無論 Handler 採用何種方式與消息隊列進行通信,最終通信的內容都會被封裝成Message類型。

從圖中可以看到,將消息封裝成 Message 類型後會先調用 Handler 的 enqueueMessage() 方法,最終調用 MessageQueue 的 enqueueMessage() 方法入隊列。在調用這兩個 enqueueMessage() 方法前會經過一系列的封裝和傳遞,感興趣的同學可以自行查看源碼,這裡就不再詳細介紹了。不過這裡有一點需要註意,之前我們提到過, Message 最重要的屬性是什麼?你寫一封信,內容再豐滿,你不寫地址的話,也是一張廢紙。所以 Message 最重要的成員變數是 target,也就是指定一個消息處理者Handler.可封裝好的 Message 指定了誰來處理呢?我們來看 Handler 中的enqueueMessage()源碼:

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

我們看到了他將自己指定成了target。這也符合邏輯:誰發送的請求(消息),誰處理。你總不能跟Looper(領導)說我要上廁所,然後領導同意後讓小王(另一個Handler)來處理這條消息吧。所以對於使用 Handler 的這三種常用函數來發送消息,不是預設而是強制性的指定了消息的target是Handler自己,所以當使用這三種常用函數的第二種時:

public final boolean sendMessage(Message msg);

即使你自己封裝的消息中target是另一個Handler,最終也會在 enqueueMessage()中被修改。

再來說說第二類發送消息的方式:Message對象調用自己的sendToTarget()方式。這種方式工作原理很簡單,看圖就懂了。源碼就一行:

public void sendToTarget() {
        target.sendMessage(this);
    }

說完這兩種發送消息的方式,有一點需要註意的。當我們需要寫一封信的時候,首先得有一個信封吧。正常情況下,我們都會 new 一個信封出來,然後再往裡添加信息。但是Android 系統中的 Message 可不簡單,我們不光可以 new 一個Message出來,我們還可以通過靜態方法:

public static Message obtain();

獲得一個 Message. 原來 Message 內部維護了一個消息池,obtain()函數前的註釋是這樣說的:

Return a new Message instance from the global pool. Allows us to avoid allocating new objects in many cases.

而 Message 空的構造函數前,文檔也做了說明:

/** Constructor ( but the preferred way to get a Message is to call Message.obtain() ).
    */
    public Message() {
    }

所以,當我們要一個Message時,首選的方式是使用Message.obtain()方法來獲得.

對於:

II: Handler是如何處理鉤過來的消息的?

由圖可知,loop() 函數中鉤到消息後調用 target.dispatchMessage() 方法來讓 Handler 處理,那我們就來到 dispatchMessage() 方法中一探究竟。

public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;//截斷消息
                }
            }
            handleMessage(msg);
        }
    }

首先去看圖,看看 callback, mCallback 都是什麼。我們發現,callback 是 Message 中的成員變數, 類型是 Runnable . mCallback 是 Handler 中的變數,類型是Callback。還記得我們之前說過,Callback是一個Handler中的介面吧:

public interface Callback {
        public boolean handleMessage(Message msg);
    }

我們回來繼續分析源碼,源碼結構很清晰,兩大分支:如果 Message 中的 Runnable 類型變數不為空,調用 handleCallback() 方法。

 private static void handleCallback(Message message) {
        message.callback.run();
    }

可以看到這裡讓這個Runnable跑起來了。這樣我們就處理了一個包含Runnable請求的消息。

如果 消息中 Runnable 為空呢? 這時就要檢查 Handler 的 Callback 介面類型的成員變數是否為空了。如果不為空則調用 mCallback 的handleMessage()方法。關於Callback介面,一會兒再說。

繼續往下看,最後一條語句才是 Handler 的handleMessage()方法。註意兩個 handleMessage() 方法, 不是同一個對象的調用哦。

現在說說Callback介面是做什麼的。還記得之前講過的 Handler 三種常用構造函數中的第二種嗎?

public Handler(Callback callback);

通過傳入一個 Callback 來初始化 Handler. 簡而言之

Callback是一種可以選擇性截斷消息處理的機制。

假定如下場景:

class LooperThread extends Thread {

        public Handler mHandler;
        private Handler.Callback mCallback;

        public void run() {
            Looper.prepare();
            
            mCallback = new Handler.Callback() {
                //強制性實現 Callback 介面中的handleMessage(),返回值是布爾類型
                @Override
                public boolean handleMessage(Message msg) {
                    Toast.makeText(MainActivity.this, "callbackHandleMSG", Toast.LENGTH_SHORT)
                            .show();
                    return msg.what == 1;// msg.what等於1則截斷消息
                }
            };
            mHandler = new Handler(mCallback) {
                //選擇性重寫 Handler 中的 handleMessage(),不重寫函數體為空
                @Override
                public void handleMessage(Message msg) {
                    Toast.makeText(MainActivity.this, "handleMessage", Toast.LENGTH_SHORT).show();
                }
            };
            
            Looper.loop();
        }
    }

截斷機制是如何工作的呢?

if (mCallback.handleMessage(msg)) {
                    return;//截斷消息
                }

沒錯,如果 Callback 的 handleMessage() 方法返回真,則 return,便不會繼續調用Hanlder 中重寫的 handleMessage() 方法了。

以上就是 Handler 和 Looper 協同工作的整個流程。

接下來看一下我們是如何通過 Handler 來更新 UI 的。

我們平時寫 Android 應用時,會把一個應用的入口認為是 onCreate() ,其實 Android 和其他Java程式一樣,他也是有主函數的。這個主函數位於 ActivityThread 這個類中。
函數的樣子就是public static void main(String[] args){...},我截取一段源碼大家看一下:

public static void main(String[] args) {
        .
        .
        .
        Looper.prepareMainLooper();//會調用Looper.prepare()

        ActivityThread thread = new ActivityThread();
        thread.attach(false);

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

        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }

        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();//Looper開始工作
        ...
    }

這就是我們常說的 Android 應用程式的主線程也就是UI線程。可以看到,他幫我們初始化了一個 Looper 並調用了 loop() 函數。這就是為什麼我們可以在一個 Activity 中,直接創建 Handler 對象而不用初始化 Looper 的原因。(但在我們自己創建的子線程中必須手動初始化 Looper 並調用loop()函數)。於是當我們在子線程中完成耗時工作後,使用主線程中的 Handler 向主線程中的 MessageQueue 發送消息,由主線程中的 loop() 方法從 MessageQueue 中取出消息,並調用消息中 target 的 dispatchMessage() 方法,執行消息。這就保證了主線程既沒有加鎖機制的效率低下問題,也沒有因為不使用加鎖機制而帶來的許多子線程併發更改 UI 導致的界面混亂問題。通過主線程中 Looper 對象,井井有條得管理了其他子線程與主線程間的通信。

最後,說一下什麼是 HandlerThread. 還記得文章開頭給出的Handler定義嗎?

Handler 是 Android 系統提供的一種方便線程間進行通信的機制。我們普遍用 Handler 機制實現在子線程中更新 UI.

Handler是Google設計的一種線程間的通信機制,更新 UI 只是這種機制功能的體現。如果我們應用中有很多子線程,他們之間需要通信怎麼辦呢? 這時候我們就可以直接用這套機制而不用自己設計了。HandlerThread 正是這樣一個方便的類,他繼承自Thread類,並自帶了Handler工作環境中的Looper.源碼中是這樣介紹這個類的:

Handy class for starting a new thread that has a looper. The looper can then be used to create handler classes. Note that start() must still be called.

於是當我們想創建一個 Handler 並關聯一個我們自己的子線程中的 Looper 時,可以直接通過創建一個 HandlerThread 作為我們的子線程,並通過 HandlerThread中的 getLooper() 方法獲得 Looper 。這是程式結構更為緊湊並且避免了一些安全問題。

以上就是我對 Handler 的一些不是很深入的總結。雖然畫圖很辛苦,碼字很辛苦,但我相信我做到了全網最清晰易懂。希望對奮鬥中的你有所幫助。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • App Store: 天的故事 1. 改回經典圖標和名稱 2. 界面優化,統一風格 3. 今日100載入故事更加智能 4. 性能優化,修複bug App Store: 天的故事 ...
  • 前段時間開發遇到webView 高度自適應問題,用最初的方法無效,找了些資料,記錄下。 1、若網頁中含有< !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/x ...
  • 一、什麼是RunLoop? RunLoop是運行迴圈,每個Cocoa應用程式都由一個處於阻塞狀態的do/while迴圈驅動,當有事件發生時,就把事件分派給合適的監聽器,如此反覆直到迴圈停止。處理分派的對象就叫做“運行迴圈”。 基本作用:1、保持程式的持續運行 2、處理App中的各種事件(比如觸摸事件 ...
  • 現實情況如上所示 我出現這種情況的原因有兩種: 其一:沒有給textview對齊方式; 其二:沒有將BOOL類型的“ automaticallyAdjustsScrollViewInsets ”屬性置為yes ...
  • Android動態調試七武器系列文章——Hooking下篇,分享一些常用或原創的調試工具以及手段,希望能對國內移動安全的研究起到一些催化劑的作用。 ...
  • 安卓使用SQlite3資料庫無法id主鍵無法自動增加?不是的。 要這樣寫:id integer primary key ,要寫integer而不是int所以會報錯! http://blog.csdn.net/zhaocundang SQLite幫助類中,這樣寫就可以id號自動增加 ...
  • 科大訊飛的語音識別功能用在安卓代碼中,我把語音識別寫成了Service,然後在Fragment直接調用service服務。科大訊飛語音識別用的是帶對話框的那個,直接調用科大訊飛的語音介面,代碼採用鏈表結果集的方式獲取數據。 這個語音識別需要在官網申請APPID 本博來自:http://blog.cs ...
  • 安卓Socket連接實現連接實現發送接收數據,openwrt wifi轉串口連接單片機實現控制 socket 連接採用流的方式進行發送接收數據,採用thread線程的方式。 什麼是線程? 詳細代碼介紹: 博文來源:http://blog.csdn.net/zhaocundang 如果你對代碼有些疑問 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...