android 非同步通訊機制Handler的分析與運用

germo發表於2021-09-09

當我們應用程式啟動時,Android系統就會建立一個主執行緒即UI執行緒,在這個UI執行緒中進行對UI控制元件的管理,如頁面的重新整理或者事件的響應等過程。同時Android規定在UI主執行緒不能進行耗時操作,否則會出現ANR現象,對此,我們一般是通過開啟子執行緒來進行耗時操作,在子執行緒中通常會涉及到頁面的重新整理問題,這就是如何在子執行緒進行UI更新,對於這個問題,我們一般通過非同步執行緒通訊機制中的Handler來解決,接下來我們就來分析一下Handler機制。

常規用法

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.execute)
    Button execute;

    @BindView(R.id.text)
    TextView text;

    //處理子執行緒發過來的訊息
    Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == 1) {
                text.setText("obj:" + msg.obj.toString() + "\nwhat: " + msg.what + "\narg1: " + msg.arg1);
            }
        }
    };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        //子執行緒傳送訊息
        new Thread(new Runnable() {
            @Override
            public void run() {
                Message message = handler.obtainMessage();
                message.what = 1;
                message.obj = "子執行緒更新UI操作";
                handler.sendMessage(message);
            }
        }).start();

    }

}複製程式碼

以上程式碼就是我們一般會使用到的,子執行緒通過Message,給主執行緒傳送訊息進行UI操作,接下來我們就一步一步進行深究,看看android是如何實現子執行緒和主執行緒如何互動的。

首先,我們在主執行緒中開啟一個子執行緒,我們用了以下方式:

        new Thread(new Runnable() {

            @Override
            public void run() {
               //處理事件
            }
        }).start();複製程式碼

開啟一個執行緒,通常有兩種方式:

  1. 繼承Thread類,覆蓋run方法
  2. 實現runnable介面,實現run方法

對於第一種方法繼承Thread類,覆蓋run方法,我們檢視原始碼就可以知道,最終還是實現runnable介面,所以沒有多大的區別。

public
class Thread implements Runnable {
    //...
}複製程式碼

迴歸正題:

Message message = handler.obtainMessage();
message.what = 1;
message.obj = "子執行緒更新UI操作";
handler.sendMessage(message);複製程式碼

我們在run方法中進行傳送訊息,對於第一行我們獲得一個訊息是通過

handler.obtainMessage();

而不是通過

Message message =new Message();

這兩者有什麼區別呢?還是來進入到原始碼中一窺究竟吧!我們首先進入Handler類中,進行檢視


public final Message obtainMessage()
    {
        return Message.obtain(this);
    }複製程式碼

繼續進入

 public static Message obtain(Handler h) {
        Message m = obtain();
        m.target = h;

        return m;
    }複製程式碼

最終來到了Message類中

/**
     * Return a new Message instance from the global pool. Allows us to
     * avoid allocating new objects in many cases.
     */
    public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }複製程式碼

我們仔細觀察一下sPool ,這個sPool 是什麼東西呢?pool是池的意思,執行緒有執行緒池,那麼我們也可以認為Message也有一個物件池,我們分析一下原始碼可以得知:

如果Message物件池中還存在message的話,我們直接使用message,而不是建立一個新的Message

接下來就是對message進行一些常規的設定,如要傳遞的訊息內容之類的,最後進行訊息 的傳送。
我們進入到訊息的最後一步原始碼中進行檢視:

handler.sendMessage(message);

會呼叫sendMessageDelayed方法

//Handler類

public final boolean sendMessage(Message msg)
    {
        return sendMessageDelayed(msg, 0);
    }複製程式碼

繼續深入檢視

//Handler.java 
 public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        //定時傳送訊息
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }複製程式碼

如果我們設定了延時時間,那麼會計算相應的傳送時間,當前時間加上延時就是最終的訊息傳送時間。

//Handler.java 定時傳送訊息
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        //訊息佇列
        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);
    }複製程式碼

我們來看看最後一步,將訊息插入隊裡中是如何實現的。

//訊息入隊操作
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {

        //msg.target實際上是Handler
        msg.target = this;

        //非同步
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }

        //訊息入隊
        return queue.enqueueMessage(msg, uptimeMillis);
    }複製程式碼

我們來看看大頭,訊息是如何入隊的。

//MessageQueue.java 訊息入隊,佇列的實現其實單連結串列的插入和刪除操作
boolean enqueueMessage(Message msg, long when) {

        //指的是Handler
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }

        //如果訊息正在使用中
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            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; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }複製程式碼

當Handler將訊息插入到訊息佇列後,那麼重要的問題來了,子執行緒是如何和主執行緒通訊的呢?按道理講,既然可以將插入到佇列中,那麼肯定有一個東西可以從訊息佇列中去訊息然後進行處理,對於這個東西,就是有Looper來承擔了。

我們首先來看下Looper這個類:

public final class Looper {


    // sThreadLocal.get() will return null unless you've called prepare().
    //用於存放當前執行緒的looper物件
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

    //主執行緒的Looper也就是UI執行緒Looper
    private static Looper sMainLooper;  // guarded by Looper.class

    //當前執行緒的訊息佇列
    final MessageQueue mQueue;

    //當前執行緒
    final Thread mThread;

    //...
}複製程式碼

我們對Looper這個類進行了簡單的介紹,對於訊息的獲取並處理我們得進入到主執行緒中即ActivityThread.java類中去

public static void main(String[] args) {
        //省略部分程式碼...
        Looper.prepareMainLooper(); ------------------(1)

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

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

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

        //省略部分程式碼...
        Looper.loop(); ------------------------(2throw new RuntimeException("Main thread loop unexpectedly exited");
    }複製程式碼

對於Looper.prepareMainLooper()我們進行分析看看,到底是什麼?

public static void prepareMainLooper() {
        //不允許退出
        prepare(false);
        synchronized (Looper.class) {
            //一個執行緒只能有一個Looper物件
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }

            //主執行緒的looper
            sMainLooper = myLooper();
        }
    }複製程式碼

對於myLoop()是什麼東東?

/**
     * Return the Looper object associated with the current thread.  Returns
     * null if the calling thread is not associated with a Looper.
     */
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }複製程式碼

是我們一開始介紹的looper類中的相關變數,也就是儲存Looper物件的東西,類似於一個容器。這裡是取的Looper物件,那麼我們在哪裡進行存呢?我們進入到prepare方法中:

//儲存當前執行緒的Looper物件
 private static void prepare(boolean quitAllowed) {

        //一個執行緒只允許一個Looper物件
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }

        //存入Looper物件
        sThreadLocal.set(new Looper(quitAllowed));
    }複製程式碼

接下來我們看一下Looper.loop()的方法:

/**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    public static void loop() {

        //獲取當前執行緒的Looper物件
        final Looper me = myLooper();

        //主執行緒中不需要手動呼叫Looper.prepare()方法,
        //當我們使用子執行緒時需要手動呼叫Looper.prepare()方法,否則會報異常。
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }

        //當前執行緒中的訊息佇列
        final MessageQueue queue = me.mQueue;

        //省略部分程式碼...

        //死迴圈,不斷處理訊息
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

             //省略部分程式碼...
            try {
                //msg.target就是Handler,Handler處理訊息
                msg.target.dispatchMessage(msg);
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }

            //省略部分程式碼...

            //訊息回收
            msg.recycleUnchecked();
        }
    }複製程式碼

Looper.loop()其實就是不斷的從佇列中獲取訊息,然後進行處理。

在上面方法中,有一行程式碼:msg.target.dispatchMessage(msg);我們進入內部去詳細檢視一下實現:

//Handler.java 分發訊息
/**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {

        // msg.callback== Runnable callback;
        if (msg.callback != null) {
            //如果有runnable,那麼則實現它的run方法裡面的內容
            handleCallback(msg);
        } else {
            //否則處理handleMessage方法
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }複製程式碼

handleCallback(msg);方法實現

private static void handleCallback(Message message) {
        //msg.callback== Runnable callback;
        message.callback.run();
    }複製程式碼

handleMessage方法實現

public interface Callback {
        public boolean handleMessage(Message msg);
    }複製程式碼

對於上面那個方法,其實就是我們在主執行緒中實現的方法:

//訊息處理
Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == 1) {
                text.setText("obj:" + msg.obj.toString() + "\nwhat: " + msg.what + "\narg1: " + msg.arg1);
            }
        }
    };複製程式碼

到此,基本上就已經分析了大概,不過,我們在實際的開發過程中有時候會碰到這個問題:

Can't create handler inside thread that has not called Looper.prepare()複製程式碼

android 非同步通訊機制Handler的分析與運用

而我們的程式碼是如何寫的呢?

//自定義一個Thread繼承自Thread
private class MyThread extends Thread {
        private Handler myThreadHandler;

        @Override
        public void run() {

            myThreadHandler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                    if (msg.what == 1) {
                      //todo...
                    }
                }
            };


            Message message = new Message();
            message.what = 1;
            message.obj = "MyThread Message Handler";
            myThreadHandler.sendMessage(message);

        }
    }複製程式碼

上述程式碼很簡單,就是自定義一個Thread,然後申明一個Handler來使用,然後通過下面程式碼進行執行緒通訊:

myThreadBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new MyThread().start();
            }
        });複製程式碼

為什麼上面簡單的程式碼會出現這個問題呢?而我們在Activity中申明的Handler就可以直接用,而不會出現上述的error?其實,我們在UI主執行緒中使用Handler時已經呼叫過了Looper.prepare()和Looper.loop(),我們返回到上面的ActivityThread.java類中的main方法處,我們可以發現,其實UI主執行緒已經呼叫過了,

 Looper.prepareMainLooper();
 //...
 Looper.loop();複製程式碼

而在我們子執行緒卻需要我們自己手動呼叫一下,知道了原因所在,我們來修改一下,再次執行,即可得出正確的答案。

 private class MyThread extends Thread {
        private Handler myThreadHandler;

        @Override
        public void run() {

            //注意此處的prepare方法
            Looper.prepare();

            myThreadHandler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                    if (msg.what == 1) {
                     //todo...
                    }
                }
            };


            Message message = new Message();
            message.what = 1;
            message.obj = "MyThread Message Handler";
            myThreadHandler.sendMessage(message);

            //注意此處的loop方法
            Looper.loop();

        }
    }複製程式碼

以上修改內容即可得出正確的答案,接下來我們來總結一下:

為什麼使用非同步訊息處理的方式就可以對UI進行操作了呢?這是由於Handler總是依附於建立時所在的執行緒,比如我們的Handler是在主執行緒中建立的,而在子執行緒中又無法直接對UI進行操作,於是我們就通過一系列的傳送訊息、入隊、出隊等環節,最後呼叫到了Handler的handleMessage()方法中,這時的handleMessage()方法已經是在主執行緒中執行的,因而我們當然可以在這裡進行UI操作了。

除了通過Handler的sendMessage方法來進行子執行緒和主執行緒進行通訊外,我們還可以通過以下的方法來達到相同的效果。

1、handler.post
我們來仔細分析一下原始碼進行說明。

public final boolean post(Runnable r)
    {
       return  sendMessageDelayed(getPostMessage(r), 0);
    }複製程式碼

呼叫handler.post方法,將runnable引數轉化為一條message進行傳送的。接著我們進入getPostMessage(r)中進行分析看看。

private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }複製程式碼

將傳遞進來的runnable引數賦值給Message的callback變數,賦值給它有什麼用呢?我們還記不記得在Handler的dispatchMessage時會做一個判斷???

public void dispatchMessage(Message msg) {
        //判斷Message的callback是否為null,這裡的callback就是runnable
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }複製程式碼

上面的callback是否為null的判斷決定著整個流程,如果callback不等於null的話我們進入handleCallback(msg)方法中一窺究竟。

private static void handleCallback(Message message) {
        message.callback.run();
    }複製程式碼

看到沒?直接呼叫了run方法,其中的message.callback就是Runnable,所以它會直接執行run方法。所以對於在子執行緒中更新UI時使用handler.post 方法時,直接在run方法中進行UI更新如:

mHandler.post(new Runnable() {
            @Override
            public void run() {
                handlerText.setText("result: this is post method");
            }
        });複製程式碼

2、view.post
同理,我們也是進入到原始碼中進行分析一下:

/**
     * <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.
     *
     * @see #postDelayed
     * @see #removeCallbacks
     */
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;
    }複製程式碼

通過該方法的註釋我們就知道,首先會將runnable放進到Message佇列中去,然後在UI主執行緒中執行,呼叫handler的post方法,本質的原理都是一樣的。

3、runOnUiThread
對於runOnUiThread方法,我們從字面上也可以瞭解到是在主執行緒中執行的,我們詳細分析一下:

public final void runOnUiThread(Runnable action) {
        if (Thread.currentThread() != mUiThread) {
            mHandler.post(action);
        } else {
            action.run();
        }
    }複製程式碼

程式碼很簡單,就是先判斷一下當前執行緒是否是UI主執行緒,是的話,直接執行run方法,不是的話通過Handler來實現。

通過以上四種在子執行緒中更新UI的方法,其內在的本質都是一樣的,都是藉助於Handler來實現的,本篇分析了非同步通訊機制中的Handler,通過本篇的學習與瞭解,對於實際專案中如果遇到相似的問題的話,我想應該可以迎刃而解了。知其然而知所以然!

最後我們來總結一下本篇文章中涉及到的各個物件的意思:

1、MessageQueue

訊息佇列,它的內部儲存了一組資料,以佇列的形式向外提供了插入和刪除的工作。但是它的內部實現並不是佇列,而是單連結串列

2、Looper

會不停檢查是否有新的訊息,如果有就呼叫最終訊息中的Runnable或者Handler的handleMessage方法。對應提取並處理訊息。

3、Handler

Handler的工作主要包含訊息的傳送和接收過程。訊息的傳送可以通過post的一系列方法以及send的一系列方法來實現,不過最後都是通過send的一系列方法實現的。對應新增訊息和處理執行緒。

4、Message

封裝了需要傳遞的訊息,並且本身可以作為連結串列的一個節點,方便MessageQueue的儲存。

5、ThreadLocal

一個執行緒內部的資料儲存類,通過它可以在指定的執行緒中儲存資料,而其它執行緒無法獲取到。在Looper、AMS中都有使用。

參考

1、http://www.jianshu.com/p/94ec12462e4e)

2、http://blog.csdn.net/woshiwxw765/article/details/38146185


關於作者

1. 簡書 http://www.jianshu.com/users/18281bdb07ce/latest_articles

2. 部落格 http://crazyandcoder.github.io/

3. github https://github.com/crazyandcoder

相關文章