1. 程式人生 > >Android訊息機制之Looper、Handler、MessageQueen

Android訊息機制之Looper、Handler、MessageQueen

Android訊息機制之Looper、Handler、MessageQueen


本篇文章包括以下內容:

前言

Android訊息機制可以說是我們android工程師面試題中的必考題,弄懂它的原理是我們避不開的任務,所以長痛不如短痛,花點時間幹掉他,廢話不多說,開車啦

Android訊息機制的簡介

安卓開發中,常常會遇到獲取資料後更新UI的問題,比如:在獲取網路資訊後,需要彈出一個Toast

HttpUtils.doGet("https://www.so.com/", new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {

    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        Toast.makeText(OkHttpActivity.this
,"",Toast.LENGTH_SHORT).show(); } });

這個時候程式就會報以下的錯誤

java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()

這是因為Android規定了只允許UI執行緒修改Activity裡的UI元件,而我們剛才的操作在子執行緒中修改Activity裡的UI元件,才會導致UI操作的執行緒不安全,並報出錯誤。為了保證Android的UI操作是執行緒安全的,Android提供了Handler訊息傳遞機制來解決這個問題

Android訊息機制的使用

在獲取網路資訊後,需要彈出一個Toast,正確做法是

private static final int MAKE_TOAST = 0x01;

private Handler mHandler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what){
            case MAKE_TOAST:
                Toast.makeText(OkHttpActivity.this,"",Toast.LENGTH_SHORT).show();
                break;
        }
    }
};

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_gallery);

    HttpUtils.doGet("https://www.so.com/", new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {

        }

        @Override
        public void onResponse(Call call, Response response) throws IOException {
            mHandler.sendEmptyMessageDelayed(MAKE_TOAST,200);
        }
    });
}

在子執行緒中通過Handler傳送訊息,該訊息會在Hanlder中的handleMessage()中被解析,並進行相對應的UI元件更新

Android訊息機制的相關概念

一、相關概念的解釋

  • 主執行緒(UI執行緒)  
    1. 定義:當程式第一次啟動時,Android會同時啟動一條主執行緒(Main Thread) 
    2. 作用:主執行緒主要負責處理與UI相關的事件
  • Message(訊息) 
    1. 定義:Handler接收和處理的訊息物件(Bean物件)
    2. 作用:通訊時相關資訊的存放和傳遞
  • ThreadLocal 
    1. 定義:執行緒內部的資料儲存類
    2. 作用:負責儲存和獲取本執行緒的Looper
  • Message Queue(訊息佇列) 
    1. 定義:採用單鏈表的資料結構來儲存訊息列表
    2. 作用:用來存放通過Handler發過來的Message,按照先進先出執行
  • Handler(處理者) 
    1. 定義:Message的主要處理者 
    2. 作用:負責傳送Message到訊息佇列&處理Looper分派過來的Message
  • Looper(迴圈器) 
    1. 定義:扮演Message Queue和Handler之間橋樑的角色
    2. 作用: 
      訊息迴圈:迴圈取出Message Queue的Message 
      訊息派發:將取出的Message交付給相應的Handler

二、圖片解讀它們之間的關係

三、文字解讀它們之間的關係

Looper中存放有MessageQueen,MessageQueen中又有很多Message,當我們的Handler傳送訊息的時候,會獲取當前的Looper,並在當前的Looper的MessageQueen當中存放我們傳送的訊息,而我們的MessageQueen也會在Looper的帶動下,一直迴圈的讀取Message資訊,並將Message資訊傳送給Handler,並執行HandlerMessage()方法

其實這是一個迴圈的過程,讀懂這句話和看懂圖解很重要,會給我們下面的原始碼分析帶來很大的幫助,所以建議大家先讀懂前面的內容

Android訊息機制的通訊流程

這裡採用網上的一張圖進行說明,個人感覺圖片概括得很好,就沒必要再去造同樣的輪子了,在新視窗開啟可瀏覽大圖

Looper原始碼分析

一、根據上面的例子,為什麼Handler可以在主執行緒中直接可以使用呢?

因為主執行緒(UI執行緒)的Looper在應用程式開啟時建立好了,即在ActivityThread.main方法中建立的,該函式為Android應用程式的入口

public static void main(String[] args) {
    ...
    Process.setArgV0("<pre-initialized>");
    //1. 建立訊息迴圈Looper
    Looper.prepareMainLooper();

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

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
    ...
    //2. 執行訊息迴圈
    Looper.loop();
    throw new RuntimeException("Main thread loop unexpectedly exited");
}
  •  

Looper中最為重要的兩個方法:

  • Looper.prepareMainLooper():該方法是Looper物件的初始化
  • Looper.loop():該方法會迴圈取出Message Queue的Message,將取出的Message交付給相應的Handler(Looper的作用就體現在這裡)

二、Looper.prepareMainLooper()

//在主執行緒中初始化Looper
public static void prepareMainLooper() {
    //在這裡會呼叫prepare(boolean quitAllowed)方法
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}

//看下prepare(boolean quitAllowed)方法
private static void prepare(boolean quitAllowed) {
    //判斷sThreadLocal是否為null,否則丟擲異常
    //即Looper.prepare()方法不能被呼叫兩次
    //也就是說,一個執行緒中只能對應一個Looper例項
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    //初始化Looper物件設定到ThreadLocal中
    sThreadLocal.set(new Looper(quitAllowed));
}

//看下Looper的構造方法
private Looper(boolean quitAllowed) {
    //建立了一個MessageQueue(訊息佇列)
    //這說明,當建立一個Looper例項時,會自動建立一個與之配對的MessageQueue(訊息佇列)
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

 

整個Looper的初始化準備工作就完了,這裡做了哪幾件事:

  • Looper的建立會關聯一個MessageQueen的建立
  • Looper物件只能被建立一次
  • Looper物件建立後被存放在sThreadLocal中

三、Looper.loop()

public static void loop() {
    //myLooper()方法作用是返回sThreadLocal儲存的Looper例項,如果me為null,loop()則丟擲異常
    //也就是說loop方法的執行必須在prepare方法之後執行
    //也就是說,訊息迴圈必須要先線上程當中建立Looper例項
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    //獲取looper例項中的mQueue(訊息佇列)
    final MessageQueue queue = me.mQueue;

    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();
    //進入訊息迴圈
    for (;;) {
        //next()方法用於取出訊息佇列裡的訊息
        //如果取出的訊息為空,則執行緒阻塞
        Message msg = queue.next(); 
        if (msg == null) {

            return;
        }

        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }

        final long traceTag = me.mTraceTag;
        if (traceTag != 0) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }
        try {
            //訊息派發:把訊息派發給msg的target屬性,然後用dispatchMessage方法去處理
            //Msg的target其實就是handler物件,下面會繼續分析
            msg.target.dispatchMessage(msg);
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }

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

        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和MessageQueen
  • 進入訊息迴圈,有訊息則分發出去
  • 訊息資源的回收

四、Looper的退出

當然Looper也提供了兩個方法可以退出一個Looper:

  • quit():quit會直接退出Looper
  • quitSafety():quitSafety只是設定一個退出標記,然後把訊息佇列中的已有訊息處理完畢後退出Looper

MessageQueen原始碼分析

一、由於MessageQueen是用來存放Message的,那麼是如何儲存Message的呢?

由於Handler使用Post()方法將Message傳遞到MessageQueen中,在MessageQueen中會使用enqueueMessage()方法儲存Message,其實現的方式是通過單鏈表的資料結構來儲存訊息列表

boolean enqueueMessage(Message msg, long when) {
    ...
    synchronized (this) {
        ...
        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {    
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    break;
                }
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            msg.next = p; 
            prev.next = msg;
        }

        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

 

整個進佇列的過程就完了,這裡做了哪幾件事:

  • 首先判斷訊息佇列裡有沒有訊息,沒有的話則將當前插入的訊息作為隊頭,並且這時訊息佇列如果處於等待狀態的話則將其喚醒
  • 若是在中間插入,則根據Message建立的時間進行插入

二、既然MessageQueen存了訊息之後,是如何提供取出來的方法的呢?

我們知道存訊息是Handler存進來的,那麼取訊息就應該是Looper中取了,從Looper的原始碼可以看出,訊息就是在Looper中取出的,其實現是用MessageQueen裡面的next()方法

Message next() {
    ......
    int nextPollTimeoutMillis = 0;
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }
        // nativePollOnce方法在native層,若是nextPollTimeoutMillis為-1,這時候訊息佇列處於等待狀態。   
        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;

            if (msg != null && msg.target == null) {
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            //按照我們設定的時間取出訊息
            if (msg != null) {
                if (now < msg.when) {
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    mBlocked = false;
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                    msg.markInUse();
                    return msg;
                }
            } else {
                // 如果訊息佇列中沒有訊息,將nextPollTimeoutMillis設為-1,下次迴圈訊息佇列則處於等待狀態
                nextPollTimeoutMillis = -1;
            }

            //退出訊息佇列,返回null,這時候Looper中的訊息迴圈也會終止。 
            if (mQuitting) {
                dispose();
                return null;
            }
            ......
        }
        .....
    }
}

三、在MessageQueen存訊息的媒介當然是通過Message物件啦,那這個Message物件又是什麼呢?

其實這個Message就是用來儲存Message中各種資訊的Bean物件,從原始碼中可以其屬性,這裡例舉我們常用的幾個

public int what;
public int arg1;
public int arg2;
public Object obj;
public Messenger replyTo;
int flags;
long when;   
Bundle data;    
Handler target;
Runnable callback;
  •  

Handler原始碼分析

一、Handler的建立

Handler的建立會關聯一個Looper物件,而Looper物件是關聯著MessageQueen物件,所以在Handler建立時候,取出Looper和MessageQueen

public Handler(Callback callback, boolean async) {
    ...
    //取出Looper
    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");
    }
    //取出Looper中的MessageQueen
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}
  •  

前面我們也說過了Looper是存放在ThreadLocal裡面的,可以看到下面的原始碼就知道了

public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}
  •  

整個建立的過程就完了,這裡做了哪幾件事:

  • 取出Looper
  • 取出Looper中的MessageQueen

二、Handler傳送訊息

第一種方式:sendMessage(Message msg)

//從這裡開始
public final boolean sendEmptyMessage(int what)
{
    return sendEmptyMessageDelayed(what, 0);
}

//往下追蹤
public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
    Message msg = Message.obtain();
    msg.what = what;
    return sendMessageDelayed(msg, delayMillis);
}

//往下追蹤
public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

//往下追蹤
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    //直接獲取MessageQueue
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}

//呼叫sendMessage方法其實最後是呼叫了enqueueMessage方法
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    //為msg.target賦值為this,也就是把當前的handler作為msg的target屬性
    //如果大家還記得Looper的loop()方法會取出每個msg然後執行msg.target.dispatchMessage(msg)去處理訊息,其實就是派發給相應的Handler
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    //最終呼叫queue的enqueueMessage的方法,也就是說handler發出的訊息,最終會儲存到訊息佇列中去
    return queue.enqueueMessage(msg, uptimeMillis);
}
  •  

第二種方式:post(Ruunable r)

public final boolean post(Runnable r)
{
   return  sendMessageDelayed(getPostMessage(r), 0);
}
  •  

其實post()方法最終也會儲存到訊息佇列中去,和上面不同的是它傳進來的一個Runnable物件,執行了getPostMessage()方法,我們往下追蹤

private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}
  •  

實質上就是將這個Runnable儲存在Message的變數中,這就導致了我們下面處理訊息的時候有兩種不同方案

三、Handler處理訊息

你還記得前面所說Looper中msg.target.dispatchMessage()方法嗎?這個方法就是呼叫Handler的dispatchMessage()

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        //1. post()方法的處理方法
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        //2. sendMessage()方法的處理方法
        handleMessage(msg);
    }
}

//1. post()方法的最終處理方法
private static void handleCallback(Message message) {
    message.callback.run();
}

//2. sendMessage()方法的最終處理方法
public void handleMessage(Message msg) {
}

 

整個處理的過程就完了,這裡做了哪幾件事:

  • post()方法的處理方法就是將傳進來的Runnable執行run()方法
  • sendMessage()方法的處理方法就是執行handleMessage()空方法,這也是我們為什麼要在Handler重寫這個方法的原因

面試題

一、請解釋下Android通訊機制中Message、Handler、MessageQueen、Looper的之間的關係?

首先,是這個MessageQueen,MessageQueen是一個訊息佇列,它可以儲存Handler傳送過來的訊息,其內部提供了進隊和出隊的方法來管理這個訊息佇列,其出隊和進隊的原理是採用單鏈表的資料結構進行插入和刪除的,即enqueueMessage()方法和next()方法。這裡提到的Message,其實就是一個Bean物件,裡面的屬性用來記錄Message的各種資訊。

然後,是這個Looper,Looper是一個迴圈器,它可以迴圈的取出MessageQueen中的Message,其內部提供了Looper的初始化和迴圈出去Message的方法,即prepare()方法和loop()方法。在prepare()方法中,Looper會關聯一個MessageQueen,而且將Looper存進一個ThreadLocal中,在loop()方法中,通過ThreadLocal取出Looper,使用MessageQueen的next()方法取出Message後,判斷Message是否為空,如果是則Looper阻塞,如果不是,則通過dispatchMessage()方法分發該Message到Handler中,而Handler執行handlerMessage()方法,由於handlerMessage()方法是個空方法,這也是為什麼需要在Handler中重寫handlerMessage()方法的原因。這裡要注意的是Looper只能在一個執行緒中只能存在一個。這裡提到的ThreadLocal,其實就是一個物件,用來在不同執行緒中存放對應執行緒的Looper。

最後,是這個Handler,Handler是Looper和MessageQueen的橋樑,Handler內部提供了傳送Message的一系列方法,最終會通過MessageQueen的enqueueMessage()方法將Message存進MessageQueen中。我們平時可以直接在主執行緒中使用Handler,那是因為在應用程式啟動時,在入口的main方法中已經預設為我們建立好了Looper。

這段面試題答案是自己寫的,如果有什麼不妥,歡迎留言一起學習

結語

學習完了訊息機制,回過頭來看還是挺簡單的。由於每個人都有懼怕困難的天性,一開始我也是看不懂,很怕看原始碼,但是我還是堅持了2天時間,藉助書本知識和網上的知識,將這個訊息機制給克服了。慢慢的,我們會將養成這種習慣,那麼你離大神的腳步就不遠了,加油吧