Android消息機制

来源:http://www.cnblogs.com/qifengshi/archive/2016/04/04/5351454.html
-Advertisement-
Play Games

每一個Android應用在啟動的時候都會創建一個線程,這個線程被稱為主線程或者 I線程,Android應用的所有操作預設都會運行在這個線程中。 但是當我們想要進行數據請求,圖片下載,或者其他耗時操作時,是不可能在這個 線程做的,因為Android在3.0以後的版本已經禁止了這件事情,直接拋出一個異常 ...


每一個Android應用在啟動的時候都會創建一個線程,這個線程被稱為主線程或者UI線程,Android應用的所有操作預設都會運行在這個線程中。
但是當我們想要進行數據請求,圖片下載,或者其他耗時操作時,是不可能在這個UI線程做的,因為Android在3.0以後的版本已經禁止了這件事情,直接拋出一個異常。所以我們需要一個子線程來處理那些除UI操作的事情。
但是這個又會有一個問題,我們只能在UI線程進程UI操作,只能在子線程進行耗時操作,如果我們需要在耗時操作結束後在Android界面上顯示一個View,我們應該怎麼做?我們是不可能在子線程直接刷新UI的。這時我們需要用到Android的消息機制,來實現主線程和子線程的通信。簡單來說,就是子線程獲取到數據之後,不直接進行UI更新,而是把數據裝到消息中發送到主線程,主線程中有一個迴圈輪詢會立即收到子線程發過來的信息,然後拿到消息數據後在主線程更新UI 。說起來比較簡單,我們來仔細的看一下具體是怎麼說的。

處理消息的手段——Handler, Looper, MessageQueue

Handler

我們先講解一下Handler,Handler顧名思義就是處理者,通常對他的用法是在UI線程中新建一個Handler,並覆寫他的handleMessage, 然後再耗時的線程中將消息postUI線程,例子如下:

class MyHandler extends Handler{
    @Override
    public void handleMessage(Message msg){
    //更新UI
    }
}

MyHandler mHandler = new MyHandler();
new Thread(){
    public void run(){
        mHandler.sendEmptyMessage(123);
    };
}.start();

這裡規定了Handler必須在主線程創建,因為只有在UI線程創建才會讓Handler關聯到已有的MessageQueue。而MessageQueue被封裝到Looper中,而Looper又通過ThreadLocal封裝到一個線程中,最後相當於MessageQueue關聯了一個線程。所以簡單來說就是Handler將消息投遞到一個關聯了線程的MessageQueue中,然後Handler在從MessageQueue中取出消息,並且處理它。
我們看一下Handler的2個常用的方法

void handleMessage(Message msg) : 處理消息的方法
final boolean sendMessage(Message msg) : 立即發送消息

第一個方法 我們通常在UI線程中執行,一般用來刷新UI,至於如果創建了一個非靜態內部類產生對記憶體泄漏,建議參考這篇博客Handler引發的記憶體泄漏.第二個方法我們通常在子線程中執行,需要一個Handler的實例化對象,通常是由主線程去去傳遞給子線程。並且需要一個Message對象,指定他的msg.what作為消息的標示,但是如果我們只是用Handler去處理一個消息的時候,選擇post方法是個更好的選擇,例子如下:

private Handler mHandler = new Handler();

new Thread(new Runnable() {
    @Override
    public void run() {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                //UI操作
                ...
            }
        });
    }
}).start();

下麵我們接著討論下消息的迴圈隊列MessageQueue與包裝他的Looper迴圈

Looper和MessageQueue

上面提到了在UI線程中創建並實例化Handler對象不需要LooperMessageQueue,因為我們的應用在啟動的時候先執行了ActivityThreadMain,在這個方法就是Java語言運行的入口public static void main(String [] args) 在這裡面創建了一個MainLooper,創建的過程如下:

public static void main(string[] args){
    //初始化
    Looper.prepareMainLooper();
    ActivityThread thread = new ActivityThread();
    thread.attach(false);
    if(sMainThreadHandler == null){
        sMainThreadHandler = thread.getHandler();
    }
    AsyncTask.init();
    //動起來
    Looper.loop();
}

這裡面並沒有MessageQueue的出現,我們可以看一看Looper類的源碼,來瞭解在初始化的時候發生了什麼有趣的事情。

public class Looper {
    private static final ThreadLocal sThreadLocal = new ThreadLocal();
    // Looper內的消息隊列
    final MessageQueue mQueue;
    // 當前線程
    Thread mThread;
    // 。。。其他屬性
    // 每個Looper對象中有它的消息隊列,和它所屬的線程
    private Looper() {
        mQueue = new MessageQueue();
        mRun = true;
        mThread = Thread.currentThread();
    }
    // 我們調用該方法會在調用線程的TLS中創建Looper對象
    public static final void prepare() {
        if (sThreadLocal.get() != null) {
            // 試圖在有Looper的線程中再次創建Looper將拋出異常
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper());
    }
    // 其他方法
}

我們一行行的看這段代碼,首先是實例化一個ThreadLocal對象,這個用來實現Looper迴圈的本地化存儲,關於ThreadLocal可以看這篇文章為什麼用ThreadLocal,簡而言之就是當多個線程同時訪問Looper對象的時候,我們不用synchronized同步機制來處理他,而是為每個線程創建一個自己的Looper副本,A線程改變了A的looper副本,不影響B線程的Looper,從而比較高效的實現線程安全。後面幾句依次定義了MessageQueue,並對Looper進行了私有化構造,在prepare方法中將Looper對象設置給了sThreadLocal 這樣MessageQueue包裝在了Looper對象中,同時通過ThreadLocal使得線程和Looper關聯上,從而消息隊列與線程關聯上,並且不同的線程就不能訪問對方的消息隊列。
如下圖所示:
Looper,MessageQueue,Thread三者示意圖

接著就是Looper.loop 迴圈執行起來,我們看一下,在loop方法裡面執行了發生了什麼事情

public static final void loop() {
        Looper me = myLooper();  //得到當前線程Looper
        MessageQueue queue = me.mQueue;  //得到當前looper的MQ
        
        while (true) {
            Message msg = queue.next(); // 取出message
            if (msg != null) {
                if (msg.target == null) {
                     return;
                }
              
                msg.target.dispatchMessage(msg);
               
                msg.recycle();
            }
        }
    }

這是省略版的代碼,我們從這裡看出無限迴圈執行,首先從消息隊列中不斷取出消息,然後不斷msg是否為空,msg.target是否為空,不空的話,執行dispatchMessage方法,這個方法是handler的一個方法,由此我們可以看出msg.targethandler的類型,至此,通過Looper.prepareLoop.loop實現了MessageQueue,Looper,Handler三者之間的關聯。而HandlerLooper,和MessageQueue關聯則是在Handler的預設構造器中,通過Looper.getLooper獲取loop對象,從而獲取MessageQueue,其源碼如下:

public Handler(){
    //直接把關聯looper的MQ作為自己的MQ,因此它的消息將發送到關聯looper的MQ上
    mLooper = Looper.myLooper();
    mQueue = mLooper.mQueue;
    mCallback = null;
}

然後我們的流程圖可以多些內容,如下所示:
Handler工作流程

我們接下來看一下dispatchMessage() 方法,在該方法中實際上只是一個分發方法,如果Runable類型的callback為空,則執行handlerMessage來處理消息,該方法為空,需要覆寫。如果不為空,則執行handleCallback。實際上,如果我們用handlepost方法,則就執行了callback,如果用sendMessage,則就執行了handleMessage
這裡無論是post(Runnable callback)還是handlerMessage實際上都是在調用一個方法sendMessageDelayed(Message msg) 只不過handlerMessage是直接接受一個參數,而Runable callback實際上是將這個Runable對象賦給了Message對象的callback成員變數,最後將Message對象插入消息隊列裡面。最後Looper不斷從MessageQueue中讀取消息,並且調用Handler的dispatchMessage消息,在根據callback是否為空,來採用不同的方法執行。Android消息機制分析到此結束。

回到最開始

我們這次知道了為什麼要在主線程中實例化Handler對象才能更新UI刷新,因為只有發送到UI線程的消息,才能被UI線程的handler處理,如果我們要在非UI線程中,實例化Handler,則必須先將線程變成LooperThread,在實例化。也就是說執行如下的代碼:

Loop.prepare();
hander = new Handler;
Loop.loop

至於原因相信讀完上面的講解,應該知道。
現在我們看一下我們最開始的代碼,最後腦補一下Handler的工作流程。

class MyHandler extends Handler{
    @Override
    public void handleMessage(Message msg){
    //更新UI
    }
}

MyHandler mHandler = new MyHandler();
new Thread(){
    public void run(){
        mHandler.sendEmptyMessage(123);
    };
}.start();

Handler實例化成mHandler的時候,系統通過Handler預設的構造函數完成了HandlerLooper的關聯,並通過Looper關聯到了MessageQueue。而主線程的Looper則早在系統啟動的時候通過Loop.prepare就已經構造完成了,並與UI線程通過ThreadLocal關聯起來,然後在新的線程中執行mHandler.sendEmptyMessage,將Message發送給了MessageQueue,Looper.loop在迴圈的時候,不斷取出message,交給Handler處理,在我們覆寫的HandleMessage中,識別出我們發送的消息,將消息處理。當然這裡只是一個Empty消息,所以在handleMessage中沒有去執行msg.what的判斷。


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

-Advertisement-
Play Games
更多相關文章
  • IP:是網路中唯一標識一臺電腦的邏輯標識。 特例:127.0.0.1 localhost 例子:192.168.33.xxx (對應門牌號碼,身份證號碼) 點分十進位形式,分成四段 範圍:0~255.0~255. 0~255. 0~255 DN:Domain Name 功能變數名稱 例子:www.baidu... ...
  • 瀏覽器相容性常見 瀏覽器相容問題⼀:不同瀏覽器的標簽預設的外補丁和內補丁不同 問題癥狀:隨便寫⼏個標簽,不加樣式控制的情況下,各⾃的margin 和padding差異較⼤。 碰到頻率:100% 解決⽅案:CSS⾥ *{margin:0;padding:0;} 備註:這個是最常 的也是最易解決的⼀個瀏 ...
  • 之前在公司做項目的時候,有這麼一個需求,要我寫一個評論框,可以隨著評論的行數增加而自動擴大,最開始我想用textarea實現,但是後來嘗試後發現textarea並不適合,textarea的高度不會隨著輸入行數的增多而增大,於是我上網尋求了下幫助,發現大神張鑫旭的這篇文章《div模擬textarea文 ...
  • 如果你把這些當做文章來看,那你始終是學不會,而是應該當做手冊來看,這些也是自己在寫網站遇到的問題。轉載請出處。 追夢子前端博客。 1. logo添加內容給h1設置text-index:-9999px的時候會把裡面的其他標簽也給定位過去。 解決方法:如果要添加內容,那麼圖片用背景來做。 2. butt ...
  • js面向對象的組成是 1、屬性 2、方法 使用的時候是再構造函數裡面加屬性,在原型裡面加方法。 如果直接在構造函數裡面:傳值、新建對象、增加屬性/方法、返回對象的這種方法,會產生問題,主要是 1、沒有new 2、函數重覆定義,這樣會讓資源浪費 所以,我們要在構造函數裡面使用this構造屬性,把方法放 ...
  • jquery現在的事件API:on,off,trigger支持帶命名空間的事件,當事件有了命名空間,就可以有效地管理同一事件的不同監聽器,在定義組件的時候,能夠避免同一元素應用到不同組件時,同一事件類型之間的影響,還能控制一些意外的事件冒泡。在實際工作中,相信大家都用的很多,但是不一定瞭解它的所有細 ...
  • 軟體測試作為程式員必備的一項技能是決定軟體開發周期長短以及軟體運行成敗的關鍵,可以說好的軟體不是代碼寫得好而是有效的測試決定的。本文將介紹在android下利用eclipse進行開發時如何使用JUnit進行單元測試。 一、測試的分類(僅舉例其中一些方法) 【根據測試是否知道代碼】 1、黑盒測試(測試 ...
  • Http基礎 這篇文章是講 網路請求的先導文章,主要講 工作流程,請求報文和響應報文的格式,以及 和`POST`方法的具體含義。 Http工作流程 是一個客戶端和伺服器端請求和應答的標準( )。客戶端是終端用戶,伺服器端是網站。通過使用 瀏覽器、網路爬蟲或者其它的工具,客戶端發起一個到伺服器上指定端 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...