Android 要點(diǎn)學(xué)習(xí)筆記(一)Handler消息機(jī)制

一衙伶、什么是進(jìn)程蕴潦、什么是線程,有和區(qū)別汪榔?

進(jìn)程和線程的基本定義是:進(jìn)程是分配資源的基本單位蒲拉,線程是獨(dú)立運(yùn)行和獨(dú)立調(diào)度的基本單位;
通俗的來講一個應(yīng)用程序一般就是一個進(jìn)程(進(jìn)程名:默認(rèn)就是包名)痴腌,我們訪問數(shù)據(jù)是以進(jìn)程為單位的(一般情況下進(jìn)程之間是不允許直接訪問到對方的數(shù)據(jù)雌团,除非使用跨進(jìn)程間通信)
而線程是cpu執(zhí)行的基本,一個進(jìn)程可以包含一個或多個線程

二士聪、為什么主線程不能執(zhí)行耗時操作锦援?

我們在打開一個應(yīng)用程序時,AMS的startProcessLocked()方法中啟動進(jìn)程時剥悟,會為進(jìn)程創(chuàng)建好主線程灵寺,也就是下圖:


1.png

傳遞的"android.app.ActivityThread"即為我們通俗意義上的“主線程”
進(jìn)程啟動的時候,會通過創(chuàng)建ActivityThread并調(diào)用其main(String[] args)方法開啟主線程区岗,因此進(jìn)程和主線程是一 一對應(yīng)的略板;
大家都知道主線程是用來處理UI渲染繪制的,同時主線程也負(fù)責(zé)處理AMS對四大組件的調(diào)度分發(fā)處理慈缔,最終完成執(zhí)行叮称;
如果在主線程中執(zhí)行耗時操作,線程里的loop循環(huán)會阻塞藐鹤,導(dǎo)致事件停止分發(fā)瓤檐,最直觀的就是界面卡住了,阻塞時間超過5秒直接ANR了娱节;

三挠蛉、為什么子線程無法更新UI界面?

由于Android的UI控件不是“線程安全”的括堤,如果多線程并發(fā)訪問碌秸,可能導(dǎo)致UI控件出現(xiàn)未知問題
(線程安全是指绍移,多線程編程中線程安全的代碼會通過同步機(jī)制悄窃,保證各個線程都能正確執(zhí)行)
那么為什么UI控件不加同步機(jī)制,讓它本省線程安全呢蹂窖?
加鎖會使UI訪問邏輯變得很復(fù)雜轧抗,加鎖會降低UI的訪問效率,因?yàn)榧渔i會阻塞某些線程的執(zhí)行

四瞬测、簡述Handler機(jī)制横媚?Handler機(jī)制的4個要素纠炮?

Handler是跨線程通信機(jī)制,因?yàn)橹骶€程不能執(zhí)行耗時操作灯蝴,需要在子線程中執(zhí)行耗時操作恢口,而子線程中不能直接對UI進(jìn)行操作;所以當(dāng)有耗時操作后的UI界面更新時穷躁,需要使用線程和Handler跨線程通信機(jī)制更新主線程UI界面耕肩;

四個要素:
Message(消息):需要被傳遞的消息
MessageQueue(消息隊(duì)列):負(fù)責(zé)存儲管理消息,單鏈表維護(hù)问潭,插入和刪除有優(yōu)勢猿诸;
Handler(消息處理器):負(fù)責(zé)發(fā)送、處理消息
Looper(消息池):負(fù)責(zé)關(guān)聯(lián)線程和消息的分發(fā)狡忙,Looper創(chuàng)建時會創(chuàng)建一個 MessageQueue梳虽;
Looper.loop()后創(chuàng)建循環(huán)持續(xù)從MessageQueue那新的消息傳遞給Handler處理;

五灾茁、 Handler機(jī)制的流程窜觉?

1、應(yīng)用程序創(chuàng)建進(jìn)程時北专,主線程ActivityThread也被創(chuàng)建
在主線程的main()函數(shù)中創(chuàng)建mainLooper并執(zhí)行Looper.loop();

1.png

2竖螃、四大組件里的Handler創(chuàng)建時
會獲取主線程的Looper并拿到Looper的MessageQueue

2.png

3、Handler通過sendMessage發(fā)送消息逗余,最終調(diào)用到queue.enqueueMessage
在消息隊(duì)列中添加該消息

3.png
------------------------enqueueMessage()源碼及分析如下:------------------------
boolean enqueueMessage(Message msg, long when) {
    if (msg.isInUse()) {//消息是否占用
        throw new AndroidRuntimeException(msg + " This message is already in use.");
    } else if (msg.target == null) {//msg.target即Handler特咆,不能為空
        throw new AndroidRuntimeException("Message must have a target.");
    } else {
        synchronized(this) {
            if (this.mQuitting) {//消息隊(duì)列的持有者Looper已經(jīng)quit了
                RuntimeException e = new RuntimeException(msg.target + " sending message to a Handler on a dead thread");
                Log.w("MessageQueue", e.getMessage(), e);
                return false;
            } else {
                msg.when = when;//延遲時間賦值給msg
                Message p = this.mMessages;//消息隊(duì)列
                boolean needWake;
                if (p != null && when != 0L && when >= p.when) {
                    //消息隊(duì)列不為空,延時不為零录粱,延時大于隊(duì)列第一個消息的延時時長
                    needWake = this.mBlocked && p.target == null && msg.isAsynchronous();
                    //while循環(huán)依次比較延時時長腻格,時長越長,插入的位置越靠后
                    while(true) {
                        Message prev = p;
                        p = p.next;
                        if (p == null || when < p.when) {
                            msg.next = p;
                            prev.next = msg;
                            break;
                        }

                        if (needWake && p.isAsynchronous()) {
                            needWake = false;
                        }
                    }
                } else {
                    //反之就是:消息隊(duì)列為空 或 延時為零 或延時時長小于隊(duì)列第一個消息的延時時長
                    msg.next = p;
                    this.mMessages = msg;
                    needWake = this.mBlocked;
                }

                if (needWake) {
                    nativeWake(this.mPtr);
                }

                return true;
            }
        }
    }
}

4啥繁、主線程執(zhí)行了Looper.loop()菜职,開始消息循環(huán)不斷輪詢調(diào)用MessageQueue.next()
取得隊(duì)列中下一個消息Message,并調(diào)用Handler的dispatchMessage(msg)傳遞給Handler

4.png

MessageQueue.next()函數(shù)從消息隊(duì)列取出消息源碼分析如下:


Message next() {
    int pendingIdleHandlerCount = -1;
    int nextPollTimeoutMillis = 0;

    while(true) {
        while(true) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            nativePollOnce(this.mPtr, nextPollTimeoutMillis);
            synchronized(this) {
                long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = this.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) {
                        this.mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            this.mMessages = msg.next;
                        }

                        msg.next = null;
                        msg.markInUse();
                        return msg;
                    }

                    nextPollTimeoutMillis = (int)Math.min(msg.when - now, 2147483647L);
                } else {
                    nextPollTimeoutMillis = -1;
                }
                
                if (this.mQuitting) {//Looper.quit(),也就是退出程序了
                    this.dispose();
                    return null;
                }
                
                ………………此處省略………………
                this.mBlocked = true;//msg為空旗闽,且沒有退出程序酬核,Blocked在next()循環(huán)中
            }
        }

………此處省略………
}

5、Handler最終調(diào)用重寫的handleMessage處理消息

5.png

六适室、Looper.loop()是死循環(huán)嫡意,為什么不會造成應(yīng)用卡死?

這是一個很有意思的問題捣辆,經(jīng)過Handler消息機(jī)制的深入學(xué)習(xí)蔬螟,我們已經(jīng)知道:

1、一個程序能正常運(yùn)行汽畴、組件能正常調(diào)度旧巾、界面能正常刷新耸序,后面是消息機(jī)制不停在起作用;
2鲁猩、Looper.loop()循環(huán)和MessageQueue.next()循環(huán)坎怪,就是為了不斷把消息拿出,讓消息機(jī)制不會停止廓握;
3芋忿、當(dāng)消息隊(duì)列為空時,只是界面和組件暫時不需要更新疾棵,從MessageQueue.next()代碼可以看到戈钢,消息隊(duì)列為空循環(huán)不會停止,只有循環(huán)不停止等到有新的消息入列時才會及時取出來是尔;
4殉了、也就是說,恰恰是Looper.loop()的死循環(huán)拟枚,才能保證消息的及時發(fā)出薪铜,才能保持消息機(jī)制的正常運(yùn)行;

通過上面的描述我們其實(shí)已經(jīng)知道了恩溅,即應(yīng)用卡死(即ANR)和主線程阻塞是沒什么關(guān)系的隔箍;
ANR全稱Application Not Responding,即應(yīng)用程序無響應(yīng)脚乡,指的是界面阻塞蜒滩,即消息隊(duì)列阻塞;

七奶稠、那么在主線程里做耗時操作俯艰,會造成ANR呢?

ANR锌订,Application Not Responding即應(yīng)用程序無響應(yīng)竹握,是界面阻塞

1、應(yīng)用程序的界面更新辆飘,是由主線程的消息處理機(jī)制完成的
2啦辐、主線程的消息處理機(jī)制,是由Looper.loop()和MessageQueue.next()的死循環(huán)不斷取出消息蜈项,來維持消息機(jī)制不斷更新的
3芹关、主線程中新增耗時操作,就會把Looper.loop()的循環(huán)暫停住战得,等新增的耗時操作完成后Looper.loop()的循環(huán)才會繼續(xù)走下去
4充边、loop循環(huán)停止一段時間庸推,相當(dāng)于消息機(jī)制就停止了一段時間常侦,這段時間內(nèi)無法響應(yīng)用戶操作浇冰,界面UI無法更新
5、當(dāng)這個時間超過5s聋亡,即引發(fā)ANR

八肘习、一個線程能否有多個Looper,能否有多個Handler坡倔,Handler和Looper之間關(guān)系漂佩?

1、一個線程只能有一個Looper罪塔,并且只有一個MessageQueue被Looper持有
2投蝉、一個線程可以有多個Handler,Handler發(fā)送的Message會被標(biāo)記上Target

Handler和Looper是多對一的關(guān)系征堪,各個Handler發(fā)送的Message會被標(biāo)記上Target瘩缆,根據(jù)延時長短被先后放入到一個MessageQueue中,在Looper.loop()時依次取出msg佃蚜,根據(jù)msg的target發(fā)送回給指定的Handler處理

九庸娱、在子線程直接new Handler可以么?需要怎么做谐算?

不可以熟尉,看源碼可以知道

直接new Handler,會報(bào)RuntimeException
"Can't create handler inside thread that has not called Looper.prepare()"
就是洲脂,無法在沒有 Looper.prepare()之前就在線程中創(chuàng)建Handler

public class Handler {
    
    public Handler() {
        this((Handler.Callback)null, false);
    }
    
    public Handler(Handler.Callback callback, boolean async) {
        this.mLooper = Looper.myLooper();
        if (this.mLooper == null) {
            throw new RuntimeException("Can't create handler inside thread that has not called Looper.prepare()");
        } else {
            this.mQueue = this.mLooper.mQueue;
            this.mCallback = callback;
            this.mAsynchronous = async;
        }
    }
    …………
}

在四大組件中可以直接new Handler是因?yàn)榻锒骶€程ActivityThread的main()函數(shù)中會自動創(chuàng)建Looper對象無需我們自己管理;
在子線程中恐锦,正確的方法是:
先Looper.prepare()
然后new Handler()
最后Looper.loop()讓消息隊(duì)列運(yùn)行起來
不過我們不建議子線程中再加Handler
因?yàn)長ooper.loop()直接子線程就阻塞了雇毫,無法再做其他的耗時操作


6.png

十、Message要如何創(chuàng)建踩蔚?哪種創(chuàng)建方法最好棚放?

有三種方式:
第一種是新建了實(shí)例
后兩種方式比較好,直接復(fù)用了消息池中已有的Message實(shí)例

Message msg1 = new Message(); 
Message msg2 = Message.obtain(); 
Message msg3 = handler.obtainMessage();

十一馅闽、Handler的PostDelay()方法使用后飘蚯,消息隊(duì)列是如何處理的?

查看源碼:
postDelayed()直接調(diào)用的sendMessageDelayed()
并通過getPostMessage(Runnable r)獲取Message實(shí)例
最后還是調(diào)用到MessageQueue.enqueueMessage()函數(shù)將消息添加到消息鏈表


public final boolean postDelayed(Runnable r, long delayMillis) {
    return this.sendMessageDelayed(getPostMessage(r), delayMillis);
}

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

public final boolean sendMessageDelayed(Message msg, long delayMillis) {
    if (delayMillis < 0L) {
        delayMillis = 0L;
    }

    return this.sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = this.mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    } else {
        return this.enqueueMessage(queue, msg, uptimeMillis);
    }
}

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

    return queue.enqueueMessage(msg, uptimeMillis);
}

十二福也、Handler使用時應(yīng)注意哪些問題局骤?有什么解決方法?

Handler在使用時很容易遇到內(nèi)存泄漏問題

在發(fā)送消息是無論調(diào)用的是handler.sendMessage或handler.sendMessageDelayed最終都會調(diào)用到
enqueueMessage()將msg添加到MessageQueue中暴凑,如下圖源碼:
發(fā)送出去的msg都會持有handler實(shí)例
(這是因?yàn)橄?zhí)行時峦甩,也會根據(jù)msg持有的handler實(shí)例將msg發(fā)回給對應(yīng)的Handler執(zhí)行)


package android.os;

import android.os.IMessenger.Stub;
import android.util.Log;
import android.util.Printer;

public class Handler {
    ………… 
    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;//msg將會持有Handler實(shí)例
        if (this.mAsynchronous) {
            msg.setAsynchronous(true);
        }
    
        return queue.enqueueMessage(msg, uptimeMillis);
    }
}

所以Handler在使用時遇到內(nèi)存泄漏問題的原因是:
handler發(fā)送的延時消息會持有handler實(shí)例,而handler我們很多時候是使用的匿名內(nèi)部類,
java的特性匿名內(nèi)部類會隱式持有外部類對象的引用
也就是說凯傲,如果在延時過程中Activity等組件退出了犬辰,msg持有handler,handler會持有activity冰单,
這就造成了activity的內(nèi)存無法回首幌缝,造成activity內(nèi)存泄漏

解決辦法:
Handler聲明時使用靜態(tài)內(nèi)部類,持有外部類的若應(yīng)用

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末诫欠,一起剝皮案震驚了整個濱河市涵卵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌荒叼,老刑警劉巖轿偎,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異被廓,居然都是意外死亡贴硫,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進(jìn)店門伊者,熙熙樓的掌柜王于貴愁眉苦臉地迎上來英遭,“玉大人,你說我怎么就攤上這事亦渗⊥谥睿” “怎么了?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵法精,是天一觀的道長多律。 經(jīng)常有香客問我,道長搂蜓,這世上最難降的妖魔是什么狼荞? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮帮碰,結(jié)果婚禮上相味,老公的妹妹穿的比我還像新娘。我一直安慰自己殉挽,他們只是感情好丰涉,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著斯碌,像睡著了一般一死。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上傻唾,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天投慈,我揣著相機(jī)與錄音,去河邊找鬼。 笑死伪煤,一個胖子當(dāng)著我的面吹牛加袋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播带族,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼锁荔,長吁一口氣:“原來是場噩夢啊……” “哼蟀给!你這毒婦竟也來了蝙砌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤跋理,失蹤者是張志新(化名)和其女友劉穎择克,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體前普,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡肚邢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了拭卿。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片骡湖。...
    茶點(diǎn)故事閱讀 40,742評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖峻厚,靈堂內(nèi)的尸體忽然破棺而出响蕴,到底是詐尸還是另有隱情,我是刑警寧澤惠桃,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布浦夷,位于F島的核電站,受9級特大地震影響辜王,放射性物質(zhì)發(fā)生泄漏劈狐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一呐馆、第九天 我趴在偏房一處隱蔽的房頂上張望肥缔。 院中可真熱鬧,春花似錦汹来、人聲如沸辫继。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽姑宽。三九已至,卻和暖如春闺阱,著一層夾襖步出監(jiān)牢的瞬間炮车,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瘦穆,地道東北人纪隙。 一個月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像扛或,于是被迫代替她去往敵國和親绵咱。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,747評論 2 361

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