“不能在子線程中更新UI”
“主線程不能做耗時操作”
這些話被我們奉為圭臬,但有多少人想過為什么不能在子線程中更新UI?為什么主線程不能做耗時操作?
首先先要從Android中的一個叫ANR的機制一點一點說起!
在說ANR之前 我們還是先要了解一下什么是ANR
ANR全名:Application Not Responding 即“應(yīng)用程序無響應(yīng)”。首先我們嘗試去Android Developers官網(wǎng)尋找一下對于ANR的權(quán)威解釋

這是Android開發(fā)者官網(wǎng)開發(fā)者指南中對ANR以及觸發(fā)機制的描述, 文檔第一段可以看到這句話:
"系統(tǒng)會通過顯示一個說明您的應(yīng)用已停止的響應(yīng)的對話框來防范一段時間內(nèi)的響應(yīng)不足的應(yīng)用程序”。
讀起來雖然很晦澀...但我們還是會有一個疑問:文中所說的防范 具體是防范哪些應(yīng)用程序? 該應(yīng)用程序又是執(zhí)行在哪個線程?。不急,我們嘗試從第二段文檔中找下答案,注意這句話:
“在您的應(yīng)用程序執(zhí)行可能冗長的操作的任何情況下, 您不應(yīng)該在UI線程上執(zhí)行工作”。
其中您不應(yīng)該在UI線程上執(zhí)行工作這句話還被黑體加粗。
由此,結(jié)合文檔我們大致明白,之前提到的“防范的應(yīng)用程序”指的是耗時操作,“執(zhí)行的線程“是UI線程。再結(jié)合第二段文檔中的話可以得出一個結(jié)論:當(dāng)你在UI線程當(dāng)中執(zhí)行耗時操作的時候,會觸發(fā)Android防范機制:ANR。
那么今天我們討論的重點并不在于此,今天我們要從設(shè)計師緯度去分析:
為什么要有ANR機制以及ANR機制在Android程序中存在的必要性
那好,我們根據(jù)開發(fā)經(jīng)驗 大膽想象一下是否可以把App的代碼宏觀上分為兩種類型:
一種是 UI操作(即時反饋)
UI操作(包括但不限于):
- 界面的渲染
- View的綁定,刷新
- 動畫
一種是 業(yè)務(wù)邏輯(耗時操作)
業(yè)務(wù)邏輯相關(guān)(包括但不限于):
- 網(wǎng)絡(luò)數(shù)據(jù)的收發(fā)和上傳
- 數(shù)據(jù)庫的CRUD操作
-
IO的讀寫
圖片
如圖,現(xiàn)在我們做一個假設(shè):假設(shè)我們的這些代碼共用一個線程,會發(fā)生什么問題呢?
首先 默認(rèn)情況下代碼執(zhí)行規(guī)則是從左至右,從上至下同步執(zhí)行,那么如果有耗時操作,代碼就會阻塞。反饋到界面上最直觀的感受就是響應(yīng)延遲卡頓,如果你代碼書寫順序顛倒 控件拿不到數(shù)據(jù),還會報出空指針等等的異常。用戶體驗不能用糟糕形容,簡直就是毫無用戶體驗。
顯然 谷歌工程師不會如此愚蠢。工程師們?yōu)榱艘?guī)避這個問題,這就要引入Android中一個重要概念叫做:異步
異步是目的,想實現(xiàn)這個目的就需要用到多線程,那么多線程狀態(tài)下Android的代碼又是如何去執(zhí)行?
首先我們要知道多線程執(zhí)行的本質(zhì)是:CPU在多條線程之間做快速的切換,它是隨機并發(fā)執(zhí)行
我們的代碼要想在這種環(huán)境下異步執(zhí)行會面臨以下問題:
- UI操作的同步問題
- UI操作(View)的不可預(yù)期性
解釋一下這些問題,我們知道UI操作是即時反饋

如圖 我們假設(shè)有三條線程,線程一給控件賦值,線程一的控件值依賴于線程二網(wǎng)絡(luò)請求拿到的數(shù)據(jù),線程二的控件賦值又依賴于線程三中數(shù)據(jù)庫查詢到的值,請問我如何確保多線程并發(fā)訪問時一定先執(zhí)行線程三 再執(zhí)行線程二 最后執(zhí)行線程一呢?換句話說我如何解決同步問題?
Ok 肯定有小伙伴說 想那么復(fù)雜干嘛,加個鎖不就完事兒了嘛...加鎖是能保證線程安全,互斥,這當(dāng)然沒問題, 重點是,你如何確定加鎖的位置?如果盲目加鎖還可能會讓控件(View)處于一個不可預(yù)期的狀態(tài),一不小心還可能造成多線程死鎖的現(xiàn)象。
既然無法用代碼的方式去解決這個問題,那我們就做減法,從書寫方式上尋找突破點 既然不能加鎖也不能讓線程同步,那我們就索性把UI操作單拎出來,你們玩你們的,事成之后告訴我一個結(jié)果就行,控件不就是想要這么一個結(jié)果渲染界面嘛。
按著這個思路,其實這個問題很好解決 我們只需要在編寫代碼的時候遵循一個規(guī)則就可以完全規(guī)避Android中UI操作在多線程異步之間的沖突。
這個規(guī)則就是:只要是涉及到UI操作的代碼,我們都單獨的放到一個線程中,這個存放UI的線程要想滿足UI控件的正常工作必須要滿足:線程非安全,不能加鎖,不能阻塞!
到此 這個存放UI的線程想必大家都知道了,對!沒錯!就是我們口口相傳大名鼎鼎的:"主線程"又稱之為UI線程。

這也就回答了開頭"為什么不能在子線程中更新UI?為什么主線程不能做耗時操作"的迷之疑問...但似乎有相當(dāng)一部分開發(fā)者只是機械型的遵循這一規(guī)則。
谷歌為了最大化提升用戶體驗 讓開發(fā)者都遵守這個規(guī)則,保證規(guī)則的良性循環(huán) ,ANR機制就誕生了,Android在主線程之間會設(shè)置一個5s——20s不等的時間閥值(產(chǎn)生ANR的上下文不同,超時時間也會不同),如果主線程中的程序運行/阻塞的時間超出了這個閥值,就會拋出ANR異常,如下圖

所以這也就是Android為什么會有ANR這么一個機制,ANR的必要性也由此顯現(xiàn)。
掌握原理及其設(shè)計思想之后 寫起代碼來才能舉一反三更加得心應(yīng)手,不會再為某一個莫名其妙的bug頭疼半天,但不得不說 谷歌工程師代碼設(shè)計的還是非常之精妙的,其中的思路非常寶貴值得我們借鑒。
但以上僅是個人對ANR機制狹義上的理解(也算是自問自答式的圈地自嗨???哈哈),旨在集思廣益交流分享,如有不同觀點非常歡迎與鄙人探討學(xué)習(xí),如果此文章對你或多或少有些啟發(fā),那就點個愛心加關(guān)注吧~ 后續(xù)會盡可能的分享更多高質(zhì)量的文章于大家交流學(xué)習(xí)。
