【Android】Android中的線程間通信(Handler與Looper)

Java中的線程回顧

在操作系統(tǒng)的概念里,進(jìn)程是具有一定獨(dú)立功能的程序、它是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位。
在Java的虛擬機(jī)中,進(jìn)程擁有自己獨(dú)立的虛擬地址空間(準(zhǔn)確的說(shuō)是方法區(qū)和堆內(nèi)存)。
而線程則是CPU調(diào)度的基本單元,它沒(méi)有自己獨(dú)立的虛擬地址空間,它依附于進(jìn)程的虛擬地址空間。在Java虛擬機(jī)中,線程擁有的不過(guò)是PC計(jì)數(shù)器和方法棧而已。
在Android系統(tǒng)中,進(jìn)程之間的通信稱為IPC,它主要由Binder完成,線程之間由于共享地址空間,各線程都可以對(duì)對(duì)象進(jìn)行操作,因此線程間的通信機(jī)制更為簡(jiǎn)單,由輕便的Handler和Looper完成。

線程獨(dú)有數(shù)據(jù)

由于同一個(gè)進(jìn)程的線程共享資源,因此,如何保證數(shù)據(jù)在某線程的私有性變?yōu)闃O為重要,忽略這點(diǎn)會(huì)造成著名的多線程同步問(wèn)題。在Java中,線程被封裝為Thread類,一種很常見(jiàn)的思維是維護(hù)一個(gè)共有的哈希表,然后將線程的私有變量存儲(chǔ)起來(lái),當(dāng)線程需要取用數(shù)據(jù)時(shí),從哈希表中讀取私有數(shù)據(jù)。

val threadData = ConcurrentHashMap<Thread,Any>()

但是如果這樣的話,程序員就需要自己在程序中維護(hù)這么一個(gè)表,且不說(shuō)自己能不能記住到底存儲(chǔ)了什么,光是表的維護(hù)就已經(jīng)很麻煩了,而且表的更新特別耗費(fèi)資源。因此,比較好的方法是將數(shù)據(jù)存儲(chǔ)封裝在Thread這個(gè)類的內(nèi)部,由于每一個(gè)線程都是一個(gè)Thread對(duì)象,它們需要這個(gè)值的時(shí)候直接讀取其內(nèi)部的值即可。Android采用了ThreadLocal這個(gè)類,實(shí)現(xiàn)了在指定線程中存儲(chǔ)數(shù)據(jù)的功能。
首先我們來(lái)看一下ThreadLocal類怎么使用:

fun test()
{
    val localAge = ThreadLocal<Int>() // 申請(qǐng)一個(gè)Int類型的線程私有變量
    localAge.set(10) //主線程中修改值
    Log.d(Thread.currentThread().toString(),localAge.get().toString()+"-> $localAge")
    Thread({
        localAge.set(12345) //其他線程中修改值
        Log.d(Thread.currentThread().toString(),localAge.get().toString()+"-> $localAge")
    }, "Thread#2").start()
    Thread({
        localAge.set(18) //其他線程中修改值
        Log.d(Thread.currentThread().toString(),localAge.get().toString()+"-> $localAge")
    },"Thread#3").start()

    localAge.set(-33) //主線程中第二次修改值
    Log.d(Thread.currentThread().toString(),localAge.get().toString()+"-> $localAge")
}

輸出結(jié)果:

01-09 21:33:50.274 11935-11935/cn.suiseiseki.www.androidstudy D/Thread[main,5,main]: 10-> java.lang.ThreadLocal@4368aaa8
01-09 21:33:50.274 11935-11950/cn.suiseiseki.www.androidstudy D/Thread[Thread#2,5,main]: 12345-> java.lang.ThreadLocal@4368aaa8
01-09 21:33:50.279 11935-11935/cn.suiseiseki.www.androidstudy D/Thread[main,5,main]: -33-> java.lang.ThreadLocal@4368aaa8
01-09 21:33:50.279 11935-11951/cn.suiseiseki.www.androidstudy D/Thread[Thread#3,5,main]: 18-> java.lang.ThreadLocal@4368aaa8
可以看到,在不同線程中訪問(wèn)同一個(gè)Thread對(duì)象,其獲取的值不一樣,這就是ThreadLocal的奇妙之處
下面我們分析一下ThreadLocal的主要方法:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

其實(shí)代碼已經(jīng)很明顯了,就是通過(guò)不同的Thread對(duì)象獲取其Thread內(nèi)部維護(hù)的ThreadLocalMap,然后查詢是否有這個(gè)參數(shù),如果有就返回這個(gè)參數(shù)的值,如果沒(méi)有則返回null.對(duì)于set方法來(lái)說(shuō),首先查找線程是否創(chuàng)建了ThreadLocalMap,如果沒(méi)有則創(chuàng)建之,然后就是正常的map操作了
如果你感興趣的話,可以再深入了解一下ThreadLocalMap這個(gè)類,它內(nèi)部采用弱引用維護(hù)了一個(gè)哈希表,這里不再詳細(xì)介紹。
總之,現(xiàn)在我們已經(jīng)知道,多個(gè)線程之間確實(shí)可以互不干擾地存儲(chǔ)和修改某些數(shù)據(jù),這是線程間通信的基礎(chǔ)。

MessageQueue:消息隊(duì)列

Android中的線程間通信涉及到四個(gè)組件:Handler,Looper,Message,MessageQueue

MessageQueue稱為消息隊(duì)列,得力于上文所述的ThreadLocal類,現(xiàn)在每一個(gè)線程可以擁有自己的MessageQueue了。MessageQueue通過(guò)一個(gè)單鏈表來(lái)維護(hù)一個(gè)消息隊(duì)列,因?yàn)閱捂湵碓诓迦牒蛣h除過(guò)程中有性能優(yōu)勢(shì)。作為一個(gè)隊(duì)列,它自然有兩個(gè)方法,用于入隊(duì)(插隊(duì))的enqueueMessage方法和出隊(duì)的next方法

boolean enqueueMessage(Message msg, long when) {
    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) {
            // New head, wake up the event queue if blocked.
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // Inserted within the middle of the queue.  Usually we don't have to wake
            // up the event queue unless there is a barrier at the head of the queue
            // and the message is the earliest asynchronous message in the queue.
            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;
}

可以看到,enqueueMessage方法先檢查Message的合法性,然后再根據(jù)Message的when屬性將Message插入單鏈表中。

Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}

    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }

        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) {
                // Stalled by a barrier.  Find the next asynchronous message in the queue.
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) {
                    // Next message is not ready.  Set a timeout to wake up when it is ready.
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // Got a message.
                    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 {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }

            // Process the quit message now that all pending messages have been handled.
            if (mQuitting) {
                dispose();
                return null;
            }

            // If first time idle, then get the number of idlers to run.
            // Idle handles only run if the queue is empty or if the first message
            // in the queue (possibly a barrier) is due to be handled in the future.
            if (pendingIdleHandlerCount < 0
                    && (mMessages == null || now < mMessages.when)) {
                pendingIdleHandlerCount = mIdleHandlers.size();
            }
            if (pendingIdleHandlerCount <= 0) {
                // No idle handlers to run.  Loop and wait some more.
                mBlocked = true;
                continue;
            }

            if (mPendingIdleHandlers == null) {
                mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
            }
            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
        }

        // Run the idle handlers.
        // We only ever reach this code block during the first iteration.
        for (int i = 0; i < pendingIdleHandlerCount; i++) {
            final IdleHandler idler = mPendingIdleHandlers[i];
            mPendingIdleHandlers[i] = null; // release the reference to the handler

            boolean keep = false;
            try {
                keep = idler.queueIdle();
            } catch (Throwable t) {
                Log.wtf(TAG, "IdleHandler threw exception", t);
            }

            if (!keep) {
                synchronized (this) {
                    mIdleHandlers.remove(idler);
                }
            }
        }

        // Reset the idle handler count to 0 so we do not run them again.
        pendingIdleHandlerCount = 0;

        // While calling an idle handler, a new message could have been delivered
        // so go back and look again for a pending message without waiting.
        nextPollTimeoutMillis = 0;
    }
}

next方法是一個(gè)無(wú)限循環(huán)的方法,如果MessageQueue隊(duì)列中沒(méi)有消息,此方法會(huì)堵塞,直到新的消息到來(lái)位置,它會(huì)將消息取出,然后再鏈表中刪除它。

Looper:MessageQueue的使用者

有了消息隊(duì)列之后,我們還需要一個(gè)使用消息隊(duì)列的對(duì)象:Looper。從Looper的名字可以看出,它代表某種循環(huán)的意思。為了讓某個(gè)線程能不斷地處理消息(Message),我們就需要為線程創(chuàng)建Looper:

    Thread(
            {
                Looper.prepare()
                val handler = Handler()
                Looper.loop()
            }
    ).start()

注意這里需要調(diào)用Looper的靜態(tài)方法。當(dāng)然,我們也可以隨時(shí)停止Looper,在線程中調(diào)用quit或者quitSatety即可:

                Looper.myLooper().quit()
                Looper.myLooper().quitSafely()

可以看到,Looper的主要方法都是靜態(tài)方法,便于我們?cè)诰€程的任意位置調(diào)用它們
Looper內(nèi)部最重要的方法就是loop()方法,它是一個(gè)死循環(huán),跳出循環(huán)的唯一方式是其MessageQueue的next返回了null(正常情況下應(yīng)該是被堵塞的)。當(dāng)Looper調(diào)用quit或者quitSafely時(shí),它會(huì)通知消息隊(duì)列退出,使其返回null值。
如果MessageQueue的next返回了Message消息,它會(huì)將這個(gè)消息交給Handler去處理。

Handler:消息的發(fā)送與處理者

Handler的工作就是發(fā)送消息和處理消息,它主要通過(guò)兩個(gè)方法:sendMessage和post(runnable)去發(fā)送消息
它們的底層過(guò)程就是向消息隊(duì)列中插入一條消息,通過(guò)Looper接收后又交給了它自己的dispatchMessage方法去處理:

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

首先,它會(huì)檢查消息本身的callback是否為空(這適用于post方法發(fā)送的runnable對(duì)象消息),如果不為空則由這個(gè)runnable對(duì)象接管執(zhí)行。
如果為空,再檢查本Handler自身的mCallback是否為空,如果不為空則由這個(gè)對(duì)象代為處理。
如果mCallback也為空,則在自身的handleMessage方法中執(zhí)行了,這是常規(guī)操作,自己在Handler的子類中實(shí)現(xiàn)這個(gè)方法即可

Handler還有一個(gè)很方便的構(gòu)造方法,傳入一個(gè)指定Looper來(lái)構(gòu)造Looper:

public Handler(Looper looper) {
    this(looper, null, false);
}

比如 val handler = Handler(Looper.getMainLooper()),快速在其他線程中獲取主線程的handler

作者:黑心石
鏈接:http://www.lxweimin.com/p/8cb3c4f8978a
來(lái)源:簡(jiǎn)書(shū)
簡(jiǎn)書(shū)著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請(qǐng)聯(lián)系作者獲得授權(quán)并注明出處。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,673評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 178,610評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,939評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,668評(píng)論 6 412
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 56,004評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評(píng)論 3 449
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 43,173評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,705評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,426評(píng)論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,656評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,833評(píng)論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 35,247評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,580評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,371評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,621評(píng)論 2 380

推薦閱讀更多精彩內(nèi)容