“不能在子線程中更新UI”
“主線程不能做耗時(shí)操作”
這些話被我們奉為圭臬敢茁,但有多少人想過(guò)為什么不能在子線程中更新UI?為什么主線程不能做耗時(shí)操作改淑?
首先先要從Android中的一個(gè)叫ANR的機(jī)制一點(diǎn)一點(diǎn)說(shuō)起!
在說(shuō)ANR之前 我們還是先要了解一下什么是ANR
ANR全名:Application Not Responding 即“應(yīng)用程序無(wú)響應(yīng)”。首先我們嘗試去Android Developers官網(wǎng)尋找一下對(duì)于ANR的權(quán)威解釋
這是Android開(kāi)發(fā)者官網(wǎng)開(kāi)發(fā)者指南中對(duì)ANR以及觸發(fā)機(jī)制的描述, 文檔第一段可以看到這句話:
"系統(tǒng)會(huì)通過(guò)顯示一個(gè)說(shuō)明您的應(yīng)用已停止的響應(yīng)的對(duì)話框來(lái)防范一段時(shí)間內(nèi)的響應(yīng)不足的應(yīng)用程序”壁袄。
讀起來(lái)雖然很晦澀...但我們還是會(huì)有一個(gè)疑問(wèn):文中所說(shuō)的防范 具體是防范哪些應(yīng)用程序? 該應(yīng)用程序又是執(zhí)行在哪個(gè)線程?媚媒。不急嗜逻,我們嘗試從第二段文檔中找下答案,注意這句話:
“在您的應(yīng)用程序執(zhí)行可能冗長(zhǎng)的操作的任何情況下缭召, 您不應(yīng)該在UI線程上執(zhí)行工作”栈顷。
其中您不應(yīng)該在UI線程上執(zhí)行工作這句話還被黑體加粗逆日。
由此,結(jié)合文檔我們大致明白萄凤,之前提到的“防范的應(yīng)用程序”指的是耗時(shí)操作室抽,“執(zhí)行的線程“是UI線程。再結(jié)合第二段文檔中的話可以得出一個(gè)結(jié)論:當(dāng)你在UI線程當(dāng)中執(zhí)行耗時(shí)操作的時(shí)候蛙卤,會(huì)觸發(fā)Android防范機(jī)制:ANR狠半。
那么今天我們討論的重點(diǎn)并不在于此,今天我們要從設(shè)計(jì)師緯度去分析:
為什么要有ANR機(jī)制以及ANR機(jī)制在Android程序中存在的必要性
那好颤难,我們根據(jù)開(kāi)發(fā)經(jīng)驗(yàn) 大膽想象一下是否可以把App的代碼宏觀上分為兩種類型:
一種是 UI操作(即時(shí)反饋)
UI操作(包括但不限于):
- 界面的渲染
- View的綁定神年,刷新
- 動(dòng)畫(huà)
一種是 業(yè)務(wù)邏輯(耗時(shí)操作)
業(yè)務(wù)邏輯相關(guān)(包括但不限于):
- 網(wǎng)絡(luò)數(shù)據(jù)的收發(fā)和上傳
- 數(shù)據(jù)庫(kù)的CRUD操作
-
IO的讀寫(xiě)
圖片
如圖,現(xiàn)在我們做一個(gè)假設(shè):假設(shè)我們的這些代碼共用一個(gè)線程行嗤,會(huì)發(fā)生什么問(wèn)題呢已日?
首先 默認(rèn)情況下代碼執(zhí)行規(guī)則是從左至右,從上至下同步執(zhí)行栅屏,那么如果有耗時(shí)操作飘千,代碼就會(huì)阻塞。反饋到界面上最直觀的感受就是響應(yīng)延遲卡頓栈雳,如果你代碼書(shū)寫(xiě)順序顛倒 控件拿不到數(shù)據(jù)护奈,還會(huì)報(bào)出空指針等等的異常。用戶體驗(yàn)不能用糟糕形容哥纫,簡(jiǎn)直就是毫無(wú)用戶體驗(yàn)霉旗。
顯然 谷歌工程師不會(huì)如此愚蠢。工程師們?yōu)榱艘?guī)避這個(gè)問(wèn)題蛀骇,這就要引入Android中一個(gè)重要概念叫做:異步
異步是目的厌秒,想實(shí)現(xiàn)這個(gè)目的就需要用到多線程,那么多線程狀態(tài)下Android的代碼又是如何去執(zhí)行擅憔?
首先我們要知道多線程執(zhí)行的本質(zhì)是:CPU在多條線程之間做快速的切換鸵闪,它是隨機(jī)并發(fā)執(zhí)行
我們的代碼要想在這種環(huán)境下異步執(zhí)行會(huì)面臨以下問(wèn)題:
- UI操作的同步問(wèn)題
- UI操作(View)的不可預(yù)期性
解釋一下這些問(wèn)題,我們知道UI操作是即時(shí)反饋
如圖 我們假設(shè)有三條線程暑诸,線程一給控件賦值蚌讼,線程一的控件值依賴于線程二網(wǎng)絡(luò)請(qǐng)求拿到的數(shù)據(jù),線程二的控件賦值又依賴于線程三中數(shù)據(jù)庫(kù)查詢到的值个榕,請(qǐng)問(wèn)我如何確保多線程并發(fā)訪問(wèn)時(shí)一定先執(zhí)行線程三 再執(zhí)行線程二 最后執(zhí)行線程一呢啦逆?換句話說(shuō)我如何解決同步問(wèn)題?
Ok 肯定有小伙伴說(shuō) 想那么復(fù)雜干嘛笛洛,加個(gè)鎖不就完事兒了嘛...加鎖是能保證線程安全,互斥乃坤,這當(dāng)然沒(méi)問(wèn)題苛让, 重點(diǎn)是沟蔑,你如何確定加鎖的位置?如果盲目加鎖還可能會(huì)讓控件(View)處于一個(gè)不可預(yù)期的狀態(tài)狱杰,一不小心還可能造成多線程死鎖的現(xiàn)象瘦材。
既然無(wú)法用代碼的方式去解決這個(gè)問(wèn)題,那我們就做減法仿畸,從書(shū)寫(xiě)方式上尋找突破點(diǎn) 既然不能加鎖也不能讓線程同步食棕,那我們就索性把UI操作單拎出來(lái),你們玩你們的错沽,事成之后告訴我一個(gè)結(jié)果就行簿晓,控件不就是想要這么一個(gè)結(jié)果渲染界面嘛。
按著這個(gè)思路千埃,其實(shí)這個(gè)問(wèn)題很好解決 我們只需要在編寫(xiě)代碼的時(shí)候遵循一個(gè)規(guī)則就可以完全規(guī)避Android中UI操作在多線程異步之間的沖突憔儿。
這個(gè)規(guī)則就是:只要是涉及到UI操作的代碼,我們都單獨(dú)的放到一個(gè)線程中放可,這個(gè)存放UI的線程要想滿足UI控件的正常工作必須要滿足:線程非安全谒臼,不能加鎖,不能阻塞耀里!
到此 這個(gè)存放UI的線程想必大家都知道了蜈缤,對(duì)!沒(méi)錯(cuò)冯挎!就是我們口口相傳大名鼎鼎的:"主線程"又稱之為UI線程底哥。
這也就回答了開(kāi)頭"為什么不能在子線程中更新UI?為什么主線程不能做耗時(shí)操作"的迷之疑問(wèn)...但似乎有相當(dāng)一部分開(kāi)發(fā)者只是機(jī)械型的遵循這一規(guī)則织堂。
谷歌為了最大化提升用戶體驗(yàn) 讓開(kāi)發(fā)者都遵守這個(gè)規(guī)則叠艳,保證規(guī)則的良性循環(huán) ,ANR機(jī)制就誕生了易阳,Android在主線程之間會(huì)設(shè)置一個(gè)5s——20s不等的時(shí)間閥值(產(chǎn)生ANR的上下文不同附较,超時(shí)時(shí)間也會(huì)不同),如果主線程中的程序運(yùn)行/阻塞的時(shí)間超出了這個(gè)閥值潦俺,就會(huì)拋出ANR異常拒课,如下圖
所以這也就是Android為什么會(huì)有ANR這么一個(gè)機(jī)制,ANR的必要性也由此顯現(xiàn)事示。
掌握原理及其設(shè)計(jì)思想之后 寫(xiě)起代碼來(lái)才能舉一反三更加得心應(yīng)手早像,不會(huì)再為某一個(gè)莫名其妙的bug頭疼半天,但不得不說(shuō) 谷歌工程師代碼設(shè)計(jì)的還是非常之精妙的肖爵,其中的思路非常寶貴值得我們借鑒卢鹦。
但以上僅是個(gè)人對(duì)ANR機(jī)制狹義上的理解(也算是自問(wèn)自答式的圈地自嗨?劝堪?冀自?哈哈)揉稚,旨在集思廣益交流分享,如有不同觀點(diǎn)非常歡迎與鄙人探討學(xué)習(xí)熬粗,如果此文章對(duì)你或多或少有些啟發(fā)搀玖,那就點(diǎn)個(gè)愛(ài)心加關(guān)注吧~ 后續(xù)會(huì)盡可能的分享更多高質(zhì)量的文章于大家交流學(xué)習(xí)。