Android面試(五):異步消息機(jī)制之Handler面試你所需知道的一切

1. 什么是Handler宪哩,為什么要有Handler?

Android中主線程也叫UI線程,主線程主要是用來創(chuàng)建第晰、更新UI的锁孟,而其他耗時(shí)操作,比如網(wǎng)絡(luò)訪問茁瘦,文件處理品抽、多媒體處理等都需要在子線程中操作,之所以在子線程中操作是為了保證UI的流暢程度甜熔,手機(jī)顯示的刷新頻率是60Hz圆恤,也就是一秒鐘刷新60次,每16.67毫秒刷新一次腔稀,為了不丟失幀盆昙,那么主線程處理代碼最好不要超過16毫秒羽历。當(dāng)子線程處理完數(shù)據(jù)后,為了防止UI處理邏輯的混亂(1.鎖機(jī)制會(huì)讓UI處理邏輯變得混亂淡喜;2.鎖機(jī)制會(huì)降低UI訪問的效率秕磷,因?yàn)殒i機(jī)制會(huì)阻塞某些線程的執(zhí)行),Android只允許主線程修改UI炼团,那么這時(shí)候就需要Handler來充當(dāng)子線程和主線程之間的橋梁了澎嚣。

2. Handler的使用方法:

  1. post(runnable)
  2. sendMessage(message)
    其實(shí)post(runnable)和sendMessage(message)最終底層都是調(diào)用了sendMessageAtTime方法。

3. Handler消息機(jī)制的原理:

在主線程中Android默認(rèn)已經(jīng)調(diào)用了Looper.preperMainLooper方法们镜,調(diào)用該方法的目的是在Looper中創(chuàng)建MessageQueue成員變量并把Looper對(duì)象綁定到當(dāng)前線程中(ThreadLocal)币叹。當(dāng)調(diào)用Handler的sendMessage方法的時(shí)候,就將Message對(duì)象添加到了Looper創(chuàng)建的MessageQueue隊(duì)列中模狭,同時(shí)給Message指定了target對(duì)象颈抚,其實(shí)這個(gè)target對(duì)象就是Handler對(duì)象。主線程默認(rèn)執(zhí)行了Looper.looper()方法嚼鹉,該方法從Looper的成員變量MessageQueue隊(duì)列中調(diào)用Message的target對(duì)象的dispatchMessage方法(也就是msg.target.dispatchMessage方法)取出Message贩汉,然后在dispatchMessage方法中調(diào)用Message的target對(duì)象的handleMessage()方法(也就是msg.target.handleMessag),這樣就完成了整個(gè)消息機(jī)制锚赤。

我們從源碼的角度來分析上述原理匹舞,首先我們來看Handler的構(gòu)造方法:

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

我們發(fā)現(xiàn)Looper.myLooper()內(nèi)部是通過sThreadLocal.get()獲得Looper的,(關(guān)于ThreadLocal:不同的線程訪問同一個(gè)ThreadLocal线脚,不管是get方法還是set方法對(duì)其所做的操作赐稽,僅限于各自線程內(nèi)部。這就是為什么用ThreadLocal來保存Looper浑侥,因?yàn)檫@樣才能使每一個(gè)線程有單獨(dú)唯一的Looper姊舵。)我們可能會(huì)想,這是通過get方法獲得Looper寓落,那么何時(shí)set的呢括丁?
當(dāng)我們觀察Looper這個(gè)類,發(fā)現(xiàn)有一個(gè)方法prepareMainLooper:

public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }

在該方法的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));
    }

哦伶选,原來set方法是在這里調(diào)用的哈史飞,接下來我們還剩下一個(gè)疑問,那就是prepareMainLooper是在哪里調(diào)用的呢仰税?其實(shí)Android主線程對(duì)應(yīng)一個(gè)類ActivityThread构资,而每個(gè)Android應(yīng)用程序都是從該類的main函數(shù)開始的:

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

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

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

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

        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

我們可以看到,Looper.prepareMainLooper就是在這里調(diào)用的陨簇,首先程序是從這個(gè)main開始的吐绵,依次調(diào)用了prepareMainLooper ——> prepare ——> sThreadLocal.set,是不是有種茅塞頓開的感覺呢?我們繼續(xù)看這個(gè)main函數(shù)拦赠,內(nèi)部調(diào)用了Looper.loop,這是Handler機(jī)制很重要的一個(gè)方法葵姥,這也是為什么我們經(jīng)常說Android主線程默認(rèn)給我們調(diào)用了Looper.prepare和Looper.looper的原因荷鼠。
接下來我們?cè)賮砜次覀兪謩?dòng)調(diào)用了handler.sendMessage或者h(yuǎn)andler.postRunnable方法,默認(rèn)底層都是調(diào)用handler.sendMessageAtTime榔幸,該方法內(nèi)部調(diào)用了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指定了一個(gè)this對(duì)象允乐,其實(shí)這個(gè)this就是Handler對(duì)象(因?yàn)檫@是在Handler類中啊,又不是內(nèi)部類其它的)削咆,我們繼續(xù)看到queue.enqueueMessage方法:

boolean enqueueMessage(Message msg, long when) {
           ...
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                //插入消息到鏈表的頭部:如果當(dāng)前MessageQueue消息隊(duì)列為空牍疏,或者插入的消息觸發(fā)時(shí)間為0,
                //亦或是插入消息的觸發(fā)時(shí)間小于現(xiàn)有頭結(jié)點(diǎn)的觸發(fā)時(shí)間
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                //根據(jù)觸發(fā)時(shí)間拨齐,將插入的消息放入到合適的位置
                // 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;
    }

通過調(diào)用此方法將消息發(fā)送到MessageQueue消息隊(duì)列中鳞陨,(這里我一直存在一個(gè)問題,這里明明做了觸發(fā)時(shí)間相關(guān)的排序瞻惋,并不符合隊(duì)列的先進(jìn)先出的特點(diǎn)厦滤,可為什么一直叫做消息隊(duì)列,就連用的類名翻譯過來也是如此歼狼,而不是鏈表呢掏导?還是說這是廣義上的隊(duì)列?如果有知道的大牛羽峰,可以跟我說說L伺亍!)
剛剛說過Android主線程梅屉,也就是ActivityThread的main函數(shù)會(huì)調(diào)用Looper.loop方法:

public static void loop() {
        final Looper me = myLooper();
        ...
        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;
            }

            final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
            ...
            final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            final long end;
            try {
                msg.target.dispatchMessage(msg);
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            ...
            msg.recycleUnchecked();
        }
    }

loop方法中調(diào)用了queue.next()方法:next()方法是一個(gè)無線循環(huán)的方法值纱,如果消息隊(duì)列中沒有消息,那么next()方法會(huì)一直阻塞履植,當(dāng)有新消息到來時(shí)计雌,next()方法會(huì)將這條消息返回同時(shí)也將這條消息從鏈表中刪除。我們主要再來看msg.target.dispatchMessage方法玫霎,從上面的分析可以知道m(xù)sg.target其實(shí)就是Handler對(duì)象凿滤,我們找到dispatchMessage方法:

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

其中調(diào)用了handleMessage,這也就是我們創(chuàng)建Handler類或其子類庶近,所需要重寫的handleMessage方法翁脆。至此,整個(gè)Handler消息機(jī)制就走通了鼻种,面試的時(shí)候反番,我們只需要說上面的原理,看源碼主要是為了更深入的了解,而不是簡單的記憶罢缸、背誦篙贸,要在理解的基礎(chǔ)上去記。

4. Handler引起的內(nèi)存泄漏以及解決辦法

原因:
非靜態(tài)內(nèi)部類持有外部類的強(qiáng)引用枫疆,導(dǎo)致外部Activity無法釋放爵川。

解決辦法:
1.handler內(nèi)部持有外部activity的弱引用
2.把handler改為靜態(tài)內(nèi)部類
3.mHandler.removeCallbacksAndMessage(尤其針對(duì)延時(shí)消息)

5. Handler相關(guān)的問題:

(1) Looper死循環(huán)為什么不會(huì)導(dǎo)致應(yīng)用卡死?

對(duì)于線程即是一段可執(zhí)行的代碼息楔,當(dāng)可執(zhí)行代碼執(zhí)行完成后寝贡,線程生命周期便該終止了,線程退出值依。而對(duì)于主線程圃泡,我們是絕不希望會(huì)被運(yùn)行一段時(shí)間,自己就退出愿险,那么如何保證能一直存活呢颇蜡?簡單做法就是可執(zhí)行代碼是能一直執(zhí)行下去的,死循環(huán)便能保證不會(huì)被退出拯啦,例如澡匪,binder 線程也是采用死循環(huán)的方法,通過循環(huán)方式不同與 Binder 驅(qū)動(dòng)進(jìn)行讀寫操作褒链,當(dāng)然并非簡單地死循環(huán)唁情,無消息時(shí)會(huì)休眠。但這里可能又引發(fā)了另一個(gè)問題甫匹,既然是死循環(huán)又如何去處理其他事務(wù)呢甸鸟?通過創(chuàng)建新線程的方式。真正會(huì)卡死主線程的操作是在回調(diào)方法 onCreate/onStart/onResume 等操作時(shí)間過長兵迅,會(huì)導(dǎo)致掉幀抢韭,甚至發(fā)生 ANR,looper.loop 本身不會(huì)導(dǎo)致應(yīng)用卡死恍箭。

(2) 主線程的死循環(huán)一直運(yùn)行是不是特別消耗 CPU 資源呢刻恭?

其實(shí)不然,這里就涉及到 Linux pipe/epoll機(jī)制扯夭,簡單說就是在主線程的 MessageQueue 沒有消息時(shí)鳍贾,便阻塞在 loop 的 queue.next() 中的 nativePollOnce() 方法里,此時(shí)主線程會(huì)釋放 CPU 資源進(jìn)入休眠狀態(tài)交洗,直到下個(gè)消息到達(dá)或者有事務(wù)發(fā)生骑科,通過往 pipe 管道寫端寫入數(shù)據(jù)來喚醒主線程工作。這里采用的 epoll 機(jī)制构拳,是一種IO多路復(fù)用機(jī)制咆爽,可以同時(shí)監(jiān)控多個(gè)描述符梁棠,當(dāng)某個(gè)描述符就緒(讀或?qū)懢途w),則立刻通知相應(yīng)程序進(jìn)行讀或?qū)懖僮鞫饭。举|(zhì)同步I/O符糊,即讀寫是阻塞的。 所以說呛凶,主線程大多數(shù)時(shí)候都是處于休眠狀態(tài)濒蒋,并不會(huì)消耗大量CPU資源。

(3) 子線程中Toast把兔、showDialog問題

Toast 本質(zhì)是通過 window 顯示和繪制的(操作的是 window),而子線程不能更新UI 是因?yàn)?ViewRootImpl 的 checkThread方法 在 Activity 維護(hù) View樹 的行為瓮顽。
Dialog 亦是如此县好。

參考鏈接:Android 消息機(jī)制——你真的了解Handler?

喜歡本篇博客的簡友們暖混,就請(qǐng)來一波點(diǎn)贊缕贡,您的每一次關(guān)注,將成為我前進(jìn)的動(dòng)力拣播,謝謝晾咪!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市贮配,隨后出現(xiàn)的幾起案子谍倦,更是在濱河造成了極大的恐慌,老刑警劉巖泪勒,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件昼蛀,死亡現(xiàn)場離奇詭異,居然都是意外死亡圆存,警方通過查閱死者的電腦和手機(jī)叼旋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沦辙,“玉大人夫植,你說我怎么就攤上這事∮脱叮” “怎么了详民?”我有些...
    開封第一講書人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長撞羽。 經(jīng)常有香客問我阐斜,道長,這世上最難降的妖魔是什么诀紊? 我笑而不...
    開封第一講書人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任谒出,我火速辦了婚禮隅俘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘笤喳。我一直安慰自己为居,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開白布杀狡。 她就那樣靜靜地躺著蒙畴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪呜象。 梳的紋絲不亂的頭發(fā)上膳凝,一...
    開封第一講書人閱讀 51,562評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音恭陡,去河邊找鬼蹬音。 笑死,一個(gè)胖子當(dāng)著我的面吹牛休玩,可吹牛的內(nèi)容都是我干的著淆。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼拴疤,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼永部!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起呐矾,我...
    開封第一講書人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤苔埋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蜒犯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體讲坎,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年愧薛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了晨炕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡毫炉,死狀恐怖瓮栗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瞄勾,我是刑警寧澤费奸,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站进陡,受9級(jí)特大地震影響愿阐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜趾疚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一缨历、第九天 我趴在偏房一處隱蔽的房頂上張望以蕴。 院中可真熱鬧,春花似錦辛孵、人聲如沸丛肮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宝与。三九已至,卻和暖如春冶匹,著一層夾襖步出監(jiān)牢的瞬間习劫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來泰國打工嚼隘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留榜聂,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓嗓蘑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親匿乃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子桩皿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

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