繼續(xù)上一篇的Android可不可以在子線程中更新UI?http://m.itdecent.cn/p/99a6aeba1161
今天再繼續(xù)深入了解一個(gè)話題Android UI 線程更新UI是否也會(huì)崩潰?
我們先從一個(gè)小需求開始說起:
點(diǎn)擊一個(gè)按鈕,請(qǐng)求服務(wù)端獲取一些文本信息在客戶端用Dialog展示出來,然后再與對(duì)話框一些交互操作。
下面貼一下代碼:



代碼很簡(jiǎn)單,就是點(diǎn)擊按鈕,新啟動(dòng)一個(gè)線程去模擬網(wǎng)絡(luò)請(qǐng)求,結(jié)果拿到后,把問題展示在Dialog。運(yùn)行完畢,點(diǎn)擊按鈕直接崩潰:

說明在子線程執(zhí)行Dialog崩潰:
java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-3,5,main] that has not called Looper.prepare()
這個(gè)有點(diǎn)經(jīng)驗(yàn)的安卓開發(fā)都知道,在子線程彈Dialog,需要初始化Looper,和啟動(dòng)Looper循環(huán)。

此時(shí)的運(yùn)行效果是這樣的,沒有發(fā)生崩潰了。

下面開始引出我們的問題,仔細(xì)發(fā)現(xiàn)

箭頭所指向的mTvTitle.setText(title);是在子線程更新title,居然一點(diǎn)也沒報(bào)錯(cuò)。
boolean isUiThread=Looper.getMainLooper()==Looper.myLooper();? 這句判斷是否在主線程日志中也打印的是false,另外也可以看PID19559-后面的數(shù)字是否和PID相等,不想等的話也是子線程
2022-08-06 01:15:14.190 19559-19689/? D/tanlin: false。
如果說是按照前篇文章所描述的那樣 ViewRootImpl還沒創(chuàng)建的話,這個(gè)時(shí)候肯定是創(chuàng)建的了我認(rèn)為,我們是延遲一秒再更新的title。那么是什么原因?qū)е聸]有報(bào)錯(cuò)呢?
接下來我添加一段代碼邏輯,就是點(diǎn)擊不是的時(shí)候,我就把文案在以前的基礎(chǔ)上加上"猶豫中?"
看看效果。

效果也是正常的。
雖然此時(shí)沒有崩潰,也沒有任何錯(cuò)誤日志,但我還是相信UI只能在UI線程更新以前一直這么認(rèn)為的。

當(dāng)我把它放在主線程中去更新時(shí),我點(diǎn)擊不是的時(shí)候點(diǎn)了三次結(jié)果反而崩潰了。

這就是回到了今天開始的主題:Android UI 線程更新UI也會(huì)崩潰:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
我們都很熟悉了這個(gè)錯(cuò)誤。而且是我點(diǎn)了三下才崩潰的,沒加上handler.post還是好好的。
現(xiàn)在我們來仔細(xì)分析這個(gè)錯(cuò)誤:打開安卓的framework源碼搜

從錯(cuò)誤可以推斷出mThread != Thread.currentThread()的,那么我們來看看mThread是在哪賦值的,

是在ViewRootImpl構(gòu)造的時(shí)候賦值的,賦值的就是當(dāng)前的Thread對(duì)象。
也就是說,你ViewRootImpl是在哪個(gè)線程創(chuàng)建的,你后續(xù)的UI更新就需要在哪個(gè)線程執(zhí)行,跟是不是UI線程毫無關(guān)系。
那么我們的Dialog其實(shí)是在子線程中創(chuàng)建的,Dialog的ViewRootImpl也就是在子線程中創(chuàng)建的,那么后續(xù)Dialog的更新也需要在子線程中。所以導(dǎo)致后續(xù)Dialog更新,執(zhí)行到ViewRootImpl#checkThread的時(shí)候,都在子線程才可以。這就是說明了剛才我加上handler.post到UI線程卻崩潰了,
再來想想,為什么我點(diǎn)第三次才崩潰,前兩次都沒有呢?它們有啥區(qū)別,是觸發(fā)了什么事件嗎?下面這張圖是點(diǎn)擊兩次的時(shí)候,當(dāng)再點(diǎn)一次就崩潰了。

想想第三次和第二次的TextView高度不一樣了改變了。調(diào)用setTextView時(shí)高度發(fā)生改變了,就會(huì)觸發(fā)了requestLayout。(補(bǔ)充一點(diǎn):這里我寬度是match_parent。如果改為wrap_parent。點(diǎn)擊一次也會(huì)崩潰。寬度改變了也會(huì)觸發(fā)requesLayout)

所以當(dāng)你的TextView觸發(fā)requestLayout,會(huì)輾轉(zhuǎn)到ViewRootImpl的requestLayout,然后再找到它的checkThread。而checkThread判斷的mThread和當(dāng)前線程對(duì)比。
具體TextView的寬高改變觸發(fā)requestLayout時(shí)機(jī),下次寫篇文章源碼繼續(xù)分析。