從頂層設(shè)計的角度對Android ANR機制的一些思考

“不能在子線程中更新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)截圖

這是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操作是即時反饋


模擬線程執(zhí)行任務(wù)

如圖 我們假設(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線程。

Android單線程模式

這也就回答了開頭"為什么不能在子線程中更新UI?為什么主線程不能做耗時操作"的迷之疑問...但似乎有相當(dāng)一部分開發(fā)者只是機械型的遵循這一規(guī)則。

谷歌為了最大化提升用戶體驗 讓開發(fā)者都遵守這個規(guī)則,保證規(guī)則的良性循環(huán) ,ANR機制就誕生了,Android在主線程之間會設(shè)置一個5s——20s不等的時間閥值(產(chǎn)生ANR的上下文不同,超時時間也會不同),如果主線程中的程序運行/阻塞的時間超出了這個閥值,就會拋出ANR異常,如下圖


開發(fā)者官網(wǎng)截圖

所以這也就是Android為什么會有ANR這么一個機制,ANR的必要性也由此顯現(xiàn)。

掌握原理及其設(shè)計思想之后 寫起代碼來才能舉一反三更加得心應(yīng)手,不會再為某一個莫名其妙的bug頭疼半天,但不得不說 谷歌工程師代碼設(shè)計的還是非常之精妙的,其中的思路非常寶貴值得我們借鑒。

但以上僅是個人對ANR機制狹義上的理解(也算是自問自答式的圈地自嗨???哈哈),旨在集思廣益交流分享,如有不同觀點非常歡迎與鄙人探討學(xué)習(xí),如果此文章對你或多或少有些啟發(fā),那就點個愛心加關(guān)注吧~ 后續(xù)會盡可能的分享更多高質(zhì)量的文章于大家交流學(xué)習(xí)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容