為啥要有 handle
首先 android UI 線(xiàn)程的類(lèi)型是 ActivityThread皂贩,這可能在這里沒(méi)什么用,湊湊字?jǐn)?shù)吧......
- android 的 UI 控件不是線(xiàn)程安全的十籍, 多線(xiàn)程并發(fā)訪(fǎng)問(wèn) UI 控件時(shí)可能會(huì)產(chǎn)生問(wèn)題。
- 為什么不給 UI 控件加鎖,一是加鎖會(huì)復(fù)雜很多界轩,二是加鎖會(huì)阻塞其他訪(fǎng)問(wèn) UI 的線(xiàn)程盐须,有可能造成別的線(xiàn)程占用 UI 而把 UI 線(xiàn)程阻塞了玩荠,這就肯定會(huì)造成卡頓問(wèn)題了。所以才采用了單線(xiàn)程更新 UI 的模式贼邓,使用 handle 來(lái)切換線(xiàn)程阶冈。
上面的解釋是看:開(kāi)發(fā)藝術(shù)探索 總結(jié)的......
handle 中幾個(gè)角色:
- ThreadLocal
每個(gè)線(xiàn)程中用來(lái)保存私有變量的容器 - Looper
消息隊(duì)列的管理容器,也可以叫輪詢(xún)器 - MessageQueue
消息隊(duì)列 - Message
消息本身 - handle
消息發(fā)送器塑径,和消息消費(fèi)者
說(shuō)下過(guò)程:
- Looper.prepare();
looper 的初始化創(chuàng)建女坑,looper 會(huì)創(chuàng)建自己,每個(gè) Looper 對(duì)象的創(chuàng)建都會(huì)伴隨創(chuàng)建一個(gè)消息隊(duì)列 MessageQueue统舀,并把自己保存在當(dāng)前線(xiàn)程的 ThreadLocal 中匆骗,保證每個(gè)線(xiàn)程中 looper 的唯一性。 - Looper.loop();
looper 開(kāi)始一個(gè)無(wú)限循環(huán)绑咱,從內(nèi)部的 MessageQueue 消息隊(duì)列中循環(huán)取出數(shù)據(jù)挨個(gè)執(zhí)行绰筛,消息隊(duì)列沒(méi)有數(shù)據(jù)了就會(huì)掛起。 - message.getTarget().dispatchMessage(message);
消息最終就是這么被執(zhí)行的描融, message.getTarget() 的返回的對(duì)象就是 handle 铝噩,所以這里由 handle 會(huì)出現(xiàn)內(nèi)存泄露。 - handler.sendMessage();
handle 發(fā)送數(shù)據(jù)窿克,就是把自己傳遞給這個(gè)待處理的消息 message 中骏庸,然后添加到 MessageQueue 消息隊(duì)列里面去。 - Handler handler2 = new Handler(Looper.getMainLooper());
Looper里面有個(gè)靜態(tài)的 looper 年叮,就是當(dāng)前進(jìn)程中 UI 線(xiàn)程的 looper具被,通過(guò)這個(gè) UI 的線(xiàn)程的 looper ,我們可以創(chuàng)建一個(gè) handle 出來(lái)只损,然后添加消息到主進(jìn)程中去執(zhí)行一姿。
為啥 looper 可以切換線(xiàn)程
looper 的都是要求我們?cè)? Looper.prepare() looper 的初始化之后馬上 Looper.loop() 讓 looper 跑起來(lái)的七咧,lopper 本身是一個(gè)無(wú)限循環(huán),會(huì)一直在 looper 所在線(xiàn)程中執(zhí)行叮叹,所以我們通過(guò)不同的 looper 對(duì)象艾栋,創(chuàng)建 handle 對(duì)象發(fā)送消失時(shí)都是把這個(gè)消息發(fā)送到了對(duì)應(yīng) looper 所以在的 MessageQueue 消息隊(duì)列中去,這個(gè)消息隊(duì)列自然的會(huì)在所訴的那個(gè) looper 循環(huán)中執(zhí)行蛉顽,looper 在哪個(gè)線(xiàn)程蝗砾,那么就是在哪個(gè)線(xiàn)程運(yùn)行。所以線(xiàn)程切換就是這么做的携冤。
MessageQueue 的 next() 方法內(nèi)部會(huì)調(diào)用 nativePollOnce() 方法悼粮,該方法會(huì)阻塞線(xiàn)程。該方法的作用簡(jiǎn)單說(shuō)曾棕,就是當(dāng)消息隊(duì)列中沒(méi)消息時(shí)扣猫,阻塞掉當(dāng)前執(zhí)行的線(xiàn)程.避免過(guò)度的cpu消耗。
- 關(guān)于nativePollOnce阻塞線(xiàn)程的解釋
- next() 方法的解釋來(lái)自 一句話(huà)講清楚Android消息機(jī)制
handle.post 干啥了
為什么特別說(shuō)下 handle 的 post 方法睁蕾,因?yàn)橛械娜嗣嬖嚂?huì)問(wèn)苞笨,其實(shí)這個(gè)很簡(jiǎn)單,post 方法會(huì)生成一個(gè) message 對(duì)象子眶,然后把我們 post 方法里面的 runnable 對(duì)象存到 message 的 callback 里面去瀑凝,message 在執(zhí)行時(shí),會(huì)判斷有沒(méi)有 callback 值臭杰,有的話(huà)直接執(zhí)行消費(fèi)本地信息了粤咪,沒(méi)有的話(huà)才會(huì)把這個(gè) message 交給 handle 去執(zhí)行。
private static Message getPostMessage(Runnable r) {
// 1. 創(chuàng)建1個(gè)消息對(duì)象(Message)
Message m = Message.obtain();
// 注:創(chuàng)建Message對(duì)象可用關(guān)鍵字new 或 Message.obtain()
// 建議:使用Message.obtain()創(chuàng)建渴杆,
// 原因:因?yàn)镸essage內(nèi)部維護(hù)了1個(gè)Message池寥枝,用于Message的復(fù)用,使用obtain()直接從池內(nèi)獲取磁奖,從而避免使用new重新分配內(nèi)存
// 2. 將 Runable對(duì)象 賦值給消息對(duì)象(message)的callback屬性
m.callback = r;
// 3. 返回該消息對(duì)象
return m;
} // 回到調(diào)用原處
對(duì)于 looper 的阻塞測(cè)試
其實(shí)看了上面對(duì)于 looper 阻塞的解釋后囊拜,我被這個(gè) Pipe 管道 hold 住了,原來(lái)用到了這么高大上的技術(shù)啊比搭,此時(shí)我非常覺(jué)得我得做點(diǎn)什么才行冠跷,于是不管怎樣樣總得干點(diǎn),干脆咱來(lái)看看這個(gè)阻塞是不是真的身诺,我是不是有點(diǎn)失心瘋啊...........
所以我做了個(gè)測(cè)試明確下思路:
起2個(gè) thread 線(xiàn)程
thread1 里面跑一個(gè) looper 蜜托,然后把這個(gè) looper 拋出去,在 looper 啟動(dòng)之后加一個(gè) 打印循環(huán)
thread2 拿到 thread1 拋出的 looper 構(gòu)建 handle 發(fā)送消息
最后看看這個(gè)在 looper 之后的打印循環(huán)有沒(méi)有執(zhí)行機(jī)會(huì)霉赡。
thread1
public class Thread1 extends Thread {
private Looper looper;
@Override
public void run() {
super.run();
Looper.prepare();
looper = Looper.myLooper();
Looper.loop();
int num = 1;
while (num <= 50) {
Log.d("AAA", Thread.currentThread().getName() + "_執(zhí)行次數(shù) / " + num);
num++;
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public Looper getLooper() {
return looper;
}
}
- thread2
public class Thread2 extends Thread {
private Handler handler;
public void setHandler(Handler handler) {
this.handler = handler;
}
@Override
public void run() {
int num = 1;
while (num <= 3) {
try {
java.lang.Thread.sleep(1000);
if (handler != null) {
Message message = new Message();
message.what = 2;
handler.sendMessage(message);
Log.d("AAA", Thread.currentThread().getName() + "發(fā)送消息 / " + num);
}
num++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- UI 線(xiàn)程啟動(dòng)這 2 個(gè)測(cè)試線(xiàn)程
Thread1 t1 = new Thread1();
Thread2 t2 = new Thread2();
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Handler handler = new Handler(t1.getLooper()) {
int num = 0;
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 2) {
num++;
Log.d("AAA", Thread.currentThread().getName() + "接受到消息 / " + num);
}
}
};
t2.setHandler(handler);
t2.start();
}
UI 線(xiàn)程中中間 sleep 1秒橄务,因?yàn)?looper 初始化需要一點(diǎn)時(shí)間,handle 中傳入的 looper 要是 null 的話(huà)會(huì)拋異常穴亏。
測(cè)試結(jié)果是果然 thread1 后面的打印循環(huán)是出不來(lái)的蜂挪,looper 的確是阻塞了當(dāng)前線(xiàn)程的重挑,looper 之后的代碼都是沒(méi)機(jī)會(huì)執(zhí)行的
- 首先 looper 里面的隊(duì)列是無(wú)限循環(huán)的,這個(gè)循環(huán)出不去后面的代碼肯定不會(huì)執(zhí)行
- 再次 looper 的阻塞會(huì)阻塞這個(gè)無(wú)限循環(huán)棠涮,進(jìn)而阻塞當(dāng)前線(xiàn)程攒驰。
我想說(shuō)這個(gè)測(cè)試有點(diǎn)湊字?jǐn)?shù)的嫌疑,但是我的確在 looper 無(wú)限循環(huán)故爵,阻塞,喚醒的問(wèn)題上找了好多資料隅津,折騰了好幾回诬垂,非常不爽啊
ThreadLocal
ThreadLocal 是一個(gè)存儲(chǔ)器,作用域在單個(gè)線(xiàn)程對(duì)象內(nèi)部伦仍,多線(xiàn)程間是無(wú)法共享的结窘,相當(dāng)于線(xiàn)程對(duì)象私有的變量存儲(chǔ)器。這個(gè)存儲(chǔ)器只能存一個(gè)數(shù)據(jù)充蓝,那么要是想存多個(gè)數(shù)據(jù)的話(huà)隧枫,就需要多個(gè) ThreadLocal 對(duì)象了。
- threadLocal.set("BB") -- > 存數(shù)據(jù)
- threadLocal.get() -- > 取數(shù)據(jù)
通過(guò)這 2 個(gè)方法就知道了吧谓苟,只能存一個(gè)數(shù)據(jù)進(jìn)去
一般我們認(rèn)為官脓,你在一個(gè)類(lèi)里面聲明的一個(gè)對(duì)象,這個(gè)對(duì)象的作用域肯定就是你這個(gè)類(lèi)的對(duì)象啊涝焙。但是注意 ThreadLocal 的特別之處在于即使你在3個(gè)線(xiàn)程之內(nèi)卑笨,操作同一個(gè)ThreadLocal 對(duì)象里面的值,那么你會(huì)發(fā)現(xiàn) 3個(gè)線(xiàn)程所屬的 ThreadLocal 里面對(duì)應(yīng)的值都是不同的仑撞。
下面是一個(gè)測(cè)試赤兴,在 UI 線(xiàn)程啟動(dòng)2個(gè) thread ,這3個(gè)線(xiàn)程一齊修改同一個(gè) ThreadLocal 里面的數(shù)據(jù)
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set("AA");
Thread t1 = new Thread() {
@Override
public void run() {
super.run();
threadLocal.set("BB");
Log.d("AA", Thread.currentThread().getName() + " / ThreadLocal : value = " + threadLocal.get());
}
};
Thread t2 = new Thread() {
@Override
public void run() {
super.run();
threadLocal.set("CC");
Log.d("AA", Thread.currentThread().getName() + " / ThreadLocal : value = " + threadLocal.get());
}
};
t1.start();
t2.start();
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d("AA", Thread.currentThread().getName() + " / ThreadLocal : value = " + threadLocal.get());
}
ThreadLocal 的使用場(chǎng)景并不多隧哮,基本都是用這個(gè)作用域范圍桶良,像 Looper,ActivityThread沮翔,AMS 這些陨帆。
趴趴源碼實(shí)現(xiàn)
為啥 ThreadLocal 這么屌呢,其實(shí)也沒(méi)啥鉴竭,主要我們嘗試看源碼歧譬,就會(huì)發(fā)現(xiàn)很多東西其實(shí)也是很簡(jiǎn)單的,就是設(shè)計(jì)很靈活搏存,很 Nice
- looper 對(duì)象里有一個(gè) static 的 ThreadLocal 對(duì)象瑰步,聲明對(duì)象時(shí)就初始化了
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
- ThreadLocal 的 ge() 干了什么
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();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
獲取當(dāng)前線(xiàn)程對(duì)象,然后從拿到線(xiàn)程對(duì)象里面的參數(shù) ThreadLocalMap 璧眠,ThreadLocal 的 map 集合缩焦,然后以 ThreadLocal 對(duì)象自己為 key 獲取到 value 值读虏,也就是我們儲(chǔ)存的數(shù)據(jù)
- 我們來(lái)看看這個(gè)map 里面的 Entry 的聲明
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
很明顯的可以看到 key 是 ThreadLocal 對(duì)象本身,value 是我們儲(chǔ)存的值袁滥。每個(gè) thread 對(duì)象內(nèi)部都有一個(gè) map 集合的話(huà)盖桥,通過(guò)同一個(gè) key 我們當(dāng)然會(huì)可以存不同的值進(jìn)去了。
其他人的解釋
ThreadLocal是一種保存變量的線(xiàn)程安全的類(lèi).
在多線(xiàn)程環(huán)境下能安全使用變量,最好的方式是在每個(gè)線(xiàn)程中定義一個(gè)local變量.
這樣對(duì)線(xiàn)程中的local變量做修改就只會(huì)影響當(dāng)前線(xiàn)程中的該變量,因此變量也就是線(xiàn)程安全的.
這種保證變量線(xiàn)程安全的思想,實(shí)際上就是ThreadLocal的實(shí)現(xiàn).
Threadlocal在調(diào)用threadLocal.get方法時(shí),會(huì)獲取當(dāng)前thread的threadLocalMap.
threadLocalMap是thread中的一個(gè)屬性.
第一次調(diào)用ThreadLocal.get()方法時(shí),會(huì)先判斷當(dāng)前線(xiàn)程對(duì)應(yīng)的threadlocalMap是否被創(chuàng)建了,
如果沒(méi)創(chuàng)建則會(huì)創(chuàng)建ThreadLocalMap,并把該對(duì)象賦值給thread.sThreadLocal對(duì)象.后續(xù)再獲取當(dāng)前thread的threadLocalMap時(shí),就會(huì)取該賦值對(duì)象.
ThreadLocalMap就是用來(lái)保存線(xiàn)程中需要保存的變量的對(duì)象了.
因?yàn)閠hreadLocalMap是賦值給當(dāng)前thread的,屬于thread的內(nèi)部變量,
所以每個(gè)線(xiàn)程的threadlocalMap就都是不同的對(duì)象,也就是上面說(shuō)的threadlocal是線(xiàn)程安全的原因了.
ThreadLocalMap內(nèi)部實(shí)際上是一個(gè)Entry[],用來(lái)保存Entry對(duì)象的數(shù)組.
Entry對(duì)象是繼承weakReference的,其中Entry的key為T(mén)hreadLocal對(duì)象,value為threadLocal需要保存的變量值.
調(diào)用ThreadLocal.set方法時(shí),會(huì)向threadLocalMap中添加一個(gè)Entry對(duì)象.
調(diào)用get方法時(shí),是通過(guò)將調(diào)用的threadLocal對(duì)象本身作為key,來(lái)遍歷threadLocalMap數(shù)組.
當(dāng)threadLocal等于Entry[]中的key時(shí),則返回該Entry中的value
為什么主線(xiàn)程不會(huì)因?yàn)?Looper.loop() 的死循環(huán)卡死
這是個(gè)經(jīng)典的問(wèn)題了题翻,我們先來(lái)看看主線(xiàn)程啟動(dòng)時(shí)干了什么
public static void main(String[] args) {
....
//創(chuàng)建Looper和MessageQueue對(duì)象揩徊,用于處理主線(xiàn)程的消息
Looper.prepareMainLooper();
//創(chuàng)建ActivityThread對(duì)象
ActivityThread thread = new ActivityThread();
//建立Binder通道 (創(chuàng)建新線(xiàn)程)
thread.attach(false);
Looper.loop(); //消息循環(huán)運(yùn)行
throw new RuntimeException("Main thread loop unexpectedly exited");
}
核心就是給主線(xiàn)程添加了 looper 進(jìn)去,然后啟動(dòng) looper 這個(gè)隊(duì)列嵌赠,looper 實(shí)際是一個(gè)阻塞是死循環(huán)塑荒,簡(jiǎn)單介紹一下,有消息就處理姜挺,沒(méi)有消息就會(huì)休眠齿税,有消息就會(huì)喚醒繼續(xù)處理消息,本質(zhì)上是一套完整的線(xiàn)程通信機(jī)制
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);
}
}
}
看到 loop 這個(gè)方法了沒(méi)炊豪, for (;;) 就是個(gè) while(true) 死循環(huán)凌箕,queue.next() 獲取隊(duì)列的消息,隊(duì)列為空的時(shí)候會(huì)阻塞主線(xiàn)程词渤,這里就是關(guān)鍵了牵舱,queue.next() 的阻塞是不會(huì)造成 ANR 的。
先說(shuō)一點(diǎn)缺虐,為什么主線(xiàn)程是個(gè)死循環(huán)還要要阻塞仆葡,淡村的死循環(huán)空閑時(shí)也不阻塞,會(huì)消耗大量的 CPU 資源志笼,這是非常不值得的操作沿盅,所以在空閑時(shí)要阻塞
java 的阻塞分兩種,阻塞失去鎖纫溃,阻塞不失去鎖腰涧,不丟失鎖的這種阻塞也叫休眠,隨時(shí)可以喚醒紊浩,喚醒就能馬上執(zhí)行任務(wù)窖铡。wait() 是失去鎖的阻塞,sleep() 就是不失去鎖的阻塞也就是休眠了坊谁,只不過(guò) loop 這里的休眠式阻塞是交給 linux 層去實(shí)現(xiàn)的
在添加任務(wù)到隊(duì)列的時(shí)候會(huì)用 linux 的方式 nativeWake 喚醒主線(xiàn)程费彼,nativeWake 是本地 C 的方法
boolean enqueueMessage(Message msg, long when) {
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
private native static void nativeWake(long ptr);
在獲取任務(wù)時(shí)先用 linux 的方式 nativePollOnce 阻塞主線(xiàn)程,nativePollOnce 是本地 C 的方法
Message next() {
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
.......
}
}
private native void nativePollOnce(long ptr, int timeoutMillis);
這里就涉及到Linux pipe/e****poll機(jī)制口芍,簡(jiǎn)單說(shuō)就是在主線(xiàn)程的MessageQueue沒(méi)有消息時(shí)箍铲,便阻塞在loop的queue.next()中的nativePollOnce()方法里,詳情見(jiàn)Android消息機(jī)制1-Handler(Java層)鬓椭,此時(shí)主線(xiàn)程會(huì)釋放CPU資源進(jìn)入休眠狀態(tài)颠猴,直到下個(gè)消息到達(dá)或者有事務(wù)發(fā)生关划,通過(guò)往pipe管道寫(xiě)端寫(xiě)入數(shù)據(jù)來(lái)喚醒主線(xiàn)程工作悄蕾。這里采用的epoll機(jī)制片任,是一種IO多路復(fù)用機(jī)制,可以同時(shí)監(jiān)控多個(gè)描述符拳亿,當(dāng)某個(gè)描述符就緒(讀或?qū)懢途w)资盅,則立刻通知相應(yīng)程序進(jìn)行讀或?qū)懖僮鞯鏖举|(zhì)同步I/O,即讀寫(xiě)是阻塞的呵扛。 所以說(shuō)振峻,主線(xiàn)程大多數(shù)時(shí)候都是處于休眠狀態(tài),并不會(huì)消耗大量CPU資源择份。
thread.attach(false);便會(huì)創(chuàng)建一個(gè)Binder線(xiàn)程(具體是指ApplicationThread烫堤,Binder的服務(wù)端荣赶,用于接收系統(tǒng)服務(wù)AMS發(fā)送來(lái)的事件),該Binder線(xiàn)程通過(guò)Handler將Message發(fā)送給主線(xiàn)程**鸽斟,具體過(guò)程可查看 startService流程分析拔创,這里不展開(kāi)說(shuō),簡(jiǎn)單說(shuō)Binder用于進(jìn)程間通信富蓄,采用C/S架構(gòu)剩燥。關(guān)于binder感興趣的朋友,可查看
為什么Android要采用Binder作為IPC機(jī)制立倍? - Gityuan的回答