Android消息机制基本原理和使用
在Android開發(fā)過(guò)程中,我們常常遇到子線程更新UI的需求,例如在子線程進(jìn)行耗時(shí)較長(zhǎng)的下載,等下載完成之后,再去更新UI,提示用戶下載完成,直接在子線程里更新UI,會(huì)得到報(bào)錯(cuò)提示:Only the original thread that created a view hierarchy can touch its views。
Android老手知道這是怎么回事,并且知道解決方案,新手只能去網(wǎng)上找答案,網(wǎng)上的答案告訴我們報(bào)錯(cuò)是因?yàn)樽泳€程不能直接更新UI線程,也就是主線程的控件,必須通過(guò)Android消息機(jī)制來(lái)更新。略微遺憾的是,網(wǎng)上的答案要么僅僅是羅列了可用方案的代碼片段,不知道背后的原理是什么,要么是陷入源碼分析的細(xì)節(jié)中,看完之后的感覺作者早就走遠(yuǎn)了,我們還在源代碼細(xì)節(jié)中暈頭轉(zhuǎn)向。
為此,決定自己寫一篇,如果能用通俗語(yǔ)言說(shuō)明白,表明自己也會(huì)了。
2.為什么其他線程不能直接訪問(wèn)UI線程?
要回答這個(gè)問(wèn)題,可以反過(guò)來(lái)想一想,如果子線程能夠訪問(wèn)UI線程會(huì)怎樣?Android的UI線程是非線程安全,非線程安全是***不對(duì)數(shù)據(jù)進(jìn)行加鎖保護(hù),多線程訪問(wèn)數(shù)據(jù)時(shí),導(dǎo)致出現(xiàn)數(shù)據(jù)不一致或者數(shù)據(jù)污染情況***,例如兩個(gè)線程同時(shí)設(shè)置同一個(gè)UIView的背景顏色,那么很有可能渲染顯示的是顏色A,而此時(shí)在UIView邏輯上的背景顏色屬性為B,因此假如子線程能直接修改UI控件的話,會(huì)導(dǎo)致出現(xiàn)不可預(yù)期結(jié)果,那么UI線程為什么不設(shè)置成線程安全呢?要設(shè)置成線程安全需要加鎖,即當(dāng)一個(gè)線程訪問(wèn)該類的某個(gè)數(shù)據(jù)時(shí),會(huì)對(duì)這個(gè)數(shù)據(jù)進(jìn)行保護(hù),其他線程不能對(duì)其訪問(wèn),直到該線程讀取完之后,其他線程才可以使用。但這樣做會(huì)降低執(zhí)行效率,如果一個(gè)子線程長(zhǎng)時(shí)間占據(jù)著,其他線程只能干等,而UI界面直接面對(duì)用戶,是Android的門面,首先要保證響應(yīng)快,越快越好,因此UI線程只能是非線程安全。
既然是非線程安全,又不想讓子線程隨便修改,只能禁止子線程直接訪問(wèn)UI線程的控件。
既然子線程不能直接訪問(wèn)UI,那怎么實(shí)現(xiàn)更新UI呢?這就用到了Android消息機(jī)制。
3.Android消息機(jī)制
什么是Android消息機(jī)制?說(shuō)白了是跨線程傳遞信息機(jī)制,注意這里是跨線程,不是跨進(jìn)程,Android里跨進(jìn)程通信使用Binder,跨線程傳遞消息使用消息機(jī)制,為什么跨線程通信不使用Binder?
我們知道不同進(jìn)程內(nèi)存空間是隔離的,而一個(gè)進(jìn)程里不同線程共享內(nèi)存空間,用日常生活來(lái)理解就是,不同進(jìn)程好比是不同的房子,一個(gè)進(jìn)程是一間房子,而一個(gè)進(jìn)程里包含不同線程,這些線程就像是不同的人,例如你,你父母,你老婆孩子,這些家庭成員同在一個(gè)房子里,也就是共享內(nèi)存空間。
兩個(gè)進(jìn)程兩間房子,相互之間通信使用電話溝通,而同在一個(gè)房間里的不同線程還使用電話溝通效率降低了,這也是為什么不同線程之間不用Binder的原因。
你可能會(huì)問(wèn),既然家庭成員同在一間房里時(shí),直接面對(duì)面喊話不就行了嗎,為什么還設(shè)計(jì)消息機(jī)制來(lái)溝通,不還是降低效率了嗎?這又回到上節(jié)里說(shuō)的,如果直接喊話就響應(yīng),讓子線程直接訪問(wèn)UI線程,會(huì)導(dǎo)致混亂,這可以由日常生活的例子來(lái)理解,假設(shè)由你掌控遙控器,你老婆想看電影頻道,你小孩想看動(dòng)畫頻道,你父母想看戲劇頻道,如果他們同時(shí)提需求,你到底是按哪個(gè)頻道?為了解決這個(gè)混亂問(wèn)題,你可以設(shè)計(jì)一個(gè)消息機(jī)制來(lái)應(yīng)付。
你可以在桌面上放著一個(gè)傳票叉,誰(shuí)想看什么節(jié)目就把需求寫在便箋,然后插到傳票叉上,這樣有個(gè)先后順序,如果叉上有便箋,你取出來(lái),看是寫了什么,例如老婆便箋寫著看10分鐘電影,她先把便箋插到叉上,小朋友便箋寫著看動(dòng)畫1分鐘,也插到叉上,這樣你先取出小朋友的便箋,然后給他看一分鐘動(dòng)畫,一分鐘后,再取出你老婆的便箋,換臺(tái)到電影頻道。這樣雖然效率不高,但保證順序不亂,當(dāng)然傳票叉是后來(lái)居上機(jī)制,這么設(shè)計(jì)容易挨打。
Android消息機(jī)制也是類似操作。
我們先來(lái)看Android消息機(jī)制里的各個(gè)角色名稱。
-
Message
用于傳遞消息的載體,對(duì)應(yīng)于上述例子的便箋;
-
MessageQueue
消息隊(duì)列,對(duì)應(yīng)上述例子的傳票叉,家人看哪個(gè)頻道的需求寫到便箋上,然后插到傳票叉上,形成消息隊(duì)列。其實(shí)我們可以發(fā)現(xiàn),正因?yàn)閭髌辈娴拇嬖?#xff0c;便箋才有先后順序,你處理起來(lái)才不會(huì)亂,如果家人把便箋散放在桌子上,那和直接喊話沒什么區(qū)別。同理,正是MessageQueue的存在,才解決了UI線程能夠挨個(gè)處理每個(gè)子線程的而不錯(cuò)亂的問(wèn)題。需要注意的是,從傳票叉取出便箋的過(guò)程只能由你自己完成,其他人代勞還是會(huì)引發(fā)混亂,同理,消息隊(duì)列也只能由接收消息的線程來(lái)處理;
-
Looper
有部電影英文名是《Looper》,只看英文名可能覺得這電影沒什么知名度,說(shuō)中文名稱你應(yīng)該就想起這部電影,這是由囧瑟夫主演的《環(huán)形使者》。那這部電影和Android消息機(jī)制里的Looper有啥關(guān)系?電影中殺手要干掉的目標(biāo)是未來(lái)的自己,陷入死循環(huán),消息機(jī)制里的Looper,是為了令程序進(jìn)入無(wú)限循環(huán),在這個(gè)循環(huán)里,不斷檢查MessageQueue是否有消息進(jìn)來(lái),如果有消息,則讀取出來(lái),并傳遞到Handler的handleMessage()方法中。注意,每個(gè)線程中只會(huì)有一個(gè)Looper對(duì)象。用遙控器的例子來(lái)理解Looper的話,Looper操作你進(jìn)入不停檢查傳票叉是否有便箋插進(jìn)來(lái)的狀態(tài);
-
Handler
在遙控器的例子中,你代表主線程,你的家人代表子線程,他們只需要在便箋上寫上需求,再自己插到傳票叉上,你取出來(lái),這就模擬了消息的跨線程傳遞。但在程序中,需要使用Handler類來(lái)完成往傳票叉上插便箋的過(guò)程,也就是子線程給主線程傳遞消息。在主線程中構(gòu)建Handler類實(shí)例,子線程再調(diào)用這個(gè)Handler類的sendMessage()方法即可,為什么主線程構(gòu)建的Handler類實(shí)例子線程能夠使用?因?yàn)樗鼈冊(cè)谕粋€(gè)進(jìn)程里,同一個(gè)進(jìn)程內(nèi)不同線程共享資源的,主線程創(chuàng)建的實(shí)例子線程當(dāng)然可以訪問(wèn)到。說(shuō)到這我們還可以這樣理解Handler,Handler相當(dāng)于主線程分發(fā)給不同線程的專用傳話筒,子線程可以通過(guò)這個(gè)傳話筒聯(lián)絡(luò)主線程。我們可以使用一個(gè)示意圖將消息機(jī)制里的幾個(gè)角色的作用展示出來(lái),如圖所示。
其中,環(huán)形使者Looper啟動(dòng)線程進(jìn)入無(wú)限循環(huán),不停查看MessageQueue是否有消息進(jìn)來(lái),有消息進(jìn)來(lái)使用Handler取出,線程1實(shí)例化出Handler,給線程2引用,線程2有消息發(fā)送的話,則使用Handler的sendMessage方法,發(fā)送消息,發(fā)送的消息進(jìn)入MessageQueue里。
4.Android消息機(jī)制使用示例
看了消息機(jī)制的基本原理,現(xiàn)在來(lái)看一下如何使用。
然后,然后沒有然后了,使用消息機(jī)制就這么簡(jiǎn)單,可以看出,因?yàn)槭褂煤?jiǎn)單,開發(fā)者使用消息機(jī)制完成子線程更新主線程其實(shí)沒增加多少工作量。對(duì)于小白而言,雖然使用簡(jiǎn)單,但了解其背后的原理還是有必要的,不然不知道為什么這么使用,我開始接觸消息機(jī)制時(shí),只知道拷貝他人的代碼,不知道原理,導(dǎo)致每次用到的時(shí)候,都是拷貝代碼片段,然后替換成自己的變量,因?yàn)樽约簩懖恢缽哪睦锶胧帧?/p>
本文只是簡(jiǎn)單理解消息機(jī)制的各個(gè)角色,以及它們之間如何配合,下一篇博客,我們分析消息機(jī)制的源代碼。
最后附上Activity的源代碼。
package com.test.threaddemo;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.view.View; import android.widget.Button; import android.widget.TextView;public class MainActivity extends AppCompatActivity {private TextView tshow;private Button button;Handler mHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);switch (msg.what) {case 0://完成主界面更新,拿到數(shù)據(jù)String data = (String)msg.obj;tshow.setText(data);break;default:break;}}};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);tshow=findViewById(R.id.tvshow);button=findViewById(R.id.bClick);button.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {UpdateTextView();}});}@Overrideprotected void onResume() {super.onResume();}private void UpdateTextView() {new Thread(new Runnable(){@Overridepublic void run() {Message msg =new Message();msg.obj = "子線程更新數(shù)據(jù)";//可以是基本類型,可以是對(duì)象,可以是List、map等;//發(fā)送消息mHandler.sendMessage(msg);}}).start();} }總結(jié)
以上是生活随笔為你收集整理的Android消息机制基本原理和使用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Android持久化存储(4)green
- 下一篇: Android权限申请的学习实践