Android線程間通信基礎(chǔ)——Handler,Looper,MessageQueue

Android單線程模型

??我們知道進(jìn)程是cpu資源分配的最小單位,線程是cpu調(diào)度的最小單位辖源。早期的操作系統(tǒng)里進(jìn)程既是資源分配也是調(diào)度的最小單位,后來隨著cpu速度越來越快驮配,為了更合理的使用cpu熏纯,減少進(jìn)程切換的開銷同诫,才將資源分配和調(diào)度分開,就有了線程樟澜。線程是建立在進(jìn)程的基礎(chǔ)上的一次程序運行單位误窖。
??當(dāng)我們第一次打開一個App時,系統(tǒng)就會給這個App分配一個進(jìn)程秩贰,并且啟動一個main thread線程霹俺,主線程主要負(fù)責(zé)處理與UI相關(guān)的事件,如:用戶的按鍵事件毒费,用戶接觸屏幕的事件以及屏幕繪圖事件丙唧,并把相關(guān)的事件分發(fā)到對應(yīng)的組件進(jìn)行處理。所以主線程通常又被叫做UI線程觅玻。
??在開發(fā)Android 應(yīng)用時必須遵守單線程模型的原則: Android UI操作并不是線程安全的并且這些操作必須在UI線程中執(zhí)行想际。

疑問

??既然UI操作只能在UI線程里更新,那么可不可以把所有操作都放在UI線程里面呢溪厘?答案是不可能的沼琉,可能會導(dǎo)致ANR。所以一些常用的耗時操作只能在非UI線程里執(zhí)行桩匪,比如網(wǎng)絡(luò)打瘪,數(shù)據(jù)庫,IO操作等傻昙。那在非UI線程執(zhí)行完后我們想把處理結(jié)果通知給UI線程怎么辦闺骚,這就涉及到線程間通信的問題。
??傳統(tǒng)的Java線程間通信包括volatile妆档,synchronized僻爽,CountDownLatch等不適合于Android,因為AndroidUI線程是消息驅(qū)動模式贾惦,主線程在啟動時會初始化一個Looper胸梆,并調(diào)用loop()方法開啟死循環(huán),在循環(huán)里執(zhí)行處理消息的操作须板。

大致流程圖如下:

handler_watermark.png

UML類圖

uml_watermark.png

流程講解

??接下來我們以handler的創(chuàng)建為起始點碰镜,結(jié)合源碼開始講解。

hander創(chuàng)建(UI線程中)

handler = new Handler();

查看構(gòu)造函數(shù):

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

這里可以傳入兩個參數(shù)习瑰,一個為callback绪颖,他是Handler的內(nèi)部接口,里面只有一個方法:

public interface Callback {
        /**
         * @param msg A {@link android.os.Message Message} object
         * @return True if no further handling is desired
         */
        public boolean handleMessage(Message msg);
}

也就是我們初始化Handler的時候可以傳入一個Callback甜奄,之后Looper會回調(diào)這個Callback柠横。
另一個參數(shù)表示是否是異步消息

Handler.java

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();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
}

??可以看見這里面會獲取Looper窃款,如果Looper為空,則會報錯牍氛;由于Handler實在主線程里面創(chuàng)建的晨继,默認(rèn)用的是主線程的Looper,而主線程的Looper實在ActivityThread的main方法中創(chuàng)建的搬俊。所以如果在其他線程創(chuàng)建Handler必須顯示的創(chuàng)建Looper紊扬。

??我們進(jìn)入Looper.myLooper()方法里面看看

Handler.java

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
/**
     * 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();
    }

??ThreadLocal是一個類似于HashMap的數(shù)據(jù)結(jié)構(gòu),它主要是用于保存線程的局部變量悠抹,ThreadLocal為變量在每個線程中都創(chuàng)建了一個副本,那么每個線程可以訪問自己內(nèi)部的副本變量扩淀。
??也就是myLooper()會獲取當(dāng)前線程的Looper楔敌,那么主線程的Looper實在哪創(chuàng)建的呢?答案是在ActivityThread中

ActivityThread.java

    public static void main(String[] args) {
        ...
        Looper.prepareMainLooper();
        ...
        Looper.loop();
    }

Looper.java

/**
     * Initialize the current thread as a looper, marking it as an
     * application's main looper. The main looper for your application
     * is created by the Android environment, so you should never need
     * to call this function yourself.  See also: {@link #prepare()}
     */
    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }

調(diào)用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));
    }

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

所以就是在這創(chuàng)建完Looper的驻谆,并且創(chuàng)建想贏的MessageQueue卵凑,然后把Looper保存到ThreadLocal中
結(jié)論:一個線程對應(yīng)一個Looper對應(yīng)一個MessageQueue

發(fā)送消息

?? Handler創(chuàng)建完畢后,我們就可以發(fā)送消息了胜臊,發(fā)送消息有幾種方式勺卢,如post(),sendMessage();最終都會調(diào)用handler的sendMessageAtTime()方法(post 的Runnnale也會包裝成Message)

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);
}

mQueue就是在創(chuàng)建Handler時賦值的,它會調(diào)用MessageQueue的enqueueMessage方法

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

這里要注意的是把msg.target指向了自己象对,這是為了Looper處理完消息后回調(diào)自己的相關(guān)方法黑忱。
接下來進(jìn)入MessageQueue里面

boolean enqueueMessage(Message msg, long when) {
        ...
        //防止多個子線程同時發(fā)送消息,導(dǎo)致不可預(yù)知的錯誤
        synchronized (this) {
             ...
            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);
            }
            ....
        }
        ....

}

??mMessages是當(dāng)前Looper正在處理的消息勒魔,即消息隊列的隊頭甫煞,賦值的地方是在next()方法里面;即如果當(dāng)前隊頭的消息為空或者待入隊的消息延時為0或者待入隊的消息的延時小于隊頭的延時冠绢,則把待入隊的消息插入到隊的頭部抚吠;
??這可以看出MessageQueue是一個按when順序排列的優(yōu)先級隊列,隊頭的when是最小弟胀;
??同時加入之前是延遲消息楷力,會阻塞當(dāng)前隊列,所以還需要喚醒

??else里面會開啟for循環(huán)孵户,這個循環(huán)的目的是插入消息到鏈表的中間:如果插入消息到鏈表頭部的條件不具備萧朝,則依次循環(huán)消息鏈表比較觸發(fā)時間的長短,然后將消息插入到消息鏈表的合適位置夏哭。接著如果需要喚醒線程處理則調(diào)用C++中的nativeWake()函數(shù)剪勿。

這樣handler插入消息的流程就完畢了

Looper消息循環(huán)

??當(dāng)消息隊列中有消息的時候,Looper就會去取出消息并執(zhí)行方庭,具體在Looper的loop()方法中厕吉。


public static void loop() {
        final Looper me = myLooper();
        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.dispatchMessage(msg);
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
          
            ...

            

            msg.recycleUnchecked();
        }
    
}

??這里面主要就是從MessageQueue中取出Message執(zhí)行酱固,即調(diào)用queue.next(),然后handler的dispatchMessage()方法头朱,前面提到過msg.target執(zhí)行的就是我們的handler运悲。然后回收message,可以復(fù)用项钮;這也是為什么建議用Message.obtain()來生成message的原因班眯。
??接下來看MessageQueue的next方法

Message next() {
        ...
        for (;;) {
               ...
              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);
            }
        }
        ...
}

??nativePollOnce(ptr, nextPollTimeoutMillis);是一個native方法,它的作用是在native層阻塞烁巫,對用nativeWake()喚醒署隘,接下通過do while循環(huán)在MessageQueue中找message,如果Handler傳入了async參數(shù)為true亚隙,這里的msg.isAsynchronous()為true磁餐,循環(huán)退出,即找出第一個不為空的同步message或者異步message阿弃;
??找到后會計算該message的執(zhí)行時間是不是現(xiàn)在這個時間點诊霹,如果還沒到它該執(zhí)行的時間點,則計算剩余的時間 nextPollTimeoutMillis渣淳,否則的話該message就是我們要找的message脾还,然后取出該message,并改變鏈表的指針入愧。
??值得一提的是MessageQueue有個有趣的接口IdleHandler鄙漏,看名字就知道它是個空的handler,當(dāng)MessageQueue中沒有消息的時候棺蛛,如果有IdleHandler泥张,則會調(diào)用queueIdle()方法,關(guān)于它的用法之后我們會講到鞠值。

Message走了一圈又回到了Handler

Message從子線程走到了主線程媚创,走了一圈又回到了Handler

/**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

??如果Message設(shè)置了回調(diào)方法的話,則回調(diào)該方法彤恶,這個是當(dāng)我們調(diào)用handler.post(Runnable able)時設(shè)置的钞钙,即這個callback就是runnable,他會調(diào)用用runnable的run方法声离;
??如果Message沒設(shè)置回調(diào)芒炼,并且handler設(shè)置了callback,這個callback是在構(gòu)造方法里面設(shè)置的术徊,之前講到過本刽,然后回調(diào)callback的handleMessage();
??如果前兩步都沒設(shè)置回調(diào),則會調(diào)用自身的handleMessage(msg)方法子寓,這個就是我們熟悉的暗挑,經(jīng)常復(fù)寫的方法。

總結(jié)

??終上所述斜友,一個message從子線程走到了主線程炸裆,這其中都是Handler的功勞,handler負(fù)責(zé)發(fā)送消息入隊鲜屏,然后處理消息烹看,這樣完成了一個操作從子線程到主線程的切換,其本質(zhì)就是把一個操作從子線程傳遞給主線程洛史。關(guān)于子線程更新UI的相關(guān)有趣操作我們會在另外的文章里講惯殊。
??Tips1: handler在哪個線程創(chuàng)建,持有的就是哪個線程的Looper也殖,MessageQueue土思。當(dāng)然你也可以在構(gòu)造函數(shù)中顯示的指定哪個線程的Looper。比如主線程創(chuàng)建的默認(rèn)是主線程的mainLooper毕源。
??Tips2: 子線程創(chuàng)建Handler浪漠,由于子線程沒有自己的Looper陕习,所以必須顯示調(diào)用Looper.prepare()創(chuàng)建Looper霎褐,并且顯示的調(diào)用Looper.loop()方法開啟消息循環(huán)。
??Tips3: handler的底層用的是Linux的管道通信该镣,至于原因冻璃,我們之后再講;

Message流向圖

message_watermark.png

時序圖

time_watermark.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末损合,一起剝皮案震驚了整個濱河市省艳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌嫁审,老刑警劉巖跋炕,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異律适,居然都是意外死亡辐烂,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進(jìn)店門捂贿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來纠修,“玉大人,你說我怎么就攤上這事厂僧】鄄荩” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長辰妙。 經(jīng)常有香客問我鹰祸,道長,這世上最難降的妖魔是什么上岗? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任福荸,我火速辦了婚禮,結(jié)果婚禮上肴掷,老公的妹妹穿的比我還像新娘敬锐。我一直安慰自己,他們只是感情好呆瞻,可當(dāng)我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布台夺。 她就那樣靜靜地躺著,像睡著了一般痴脾。 火紅的嫁衣襯著肌膚如雪颤介。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天赞赖,我揣著相機與錄音滚朵,去河邊找鬼。 笑死前域,一個胖子當(dāng)著我的面吹牛辕近,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播匿垄,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼移宅,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了椿疗?” 一聲冷哼從身側(cè)響起漏峰,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎届榄,沒想到半個月后浅乔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡铝条,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年靖苇,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片攻晒。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡顾复,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鲁捏,到底是詐尸還是另有隱情芯砸,我是刑警寧澤萧芙,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站假丧,受9級特大地震影響双揪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜包帚,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一渔期、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧渴邦,春花似錦疯趟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至瓮床,卻和暖如春盹舞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背隘庄。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工踢步, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人丑掺。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓获印,卻偏偏與公主長得像,于是被迫代替她去往敵國和親吼鱼。 傳聞我的和親對象是個殘疾皇子蓬豁,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,781評論 2 354

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