Android中子線程真的不能更新UI嗎?

【原文地址 點擊打開鏈接

正文

Android的UI訪問是沒有加鎖的,這樣在多個線程訪問UI是不安全的。所以Android中規定只能在UI線程中訪問UI。


但是有沒有極端的情況?使得我們在子線程中訪問UI也可以使程序跑起來呢?接下來我們用一個例子去證實一下。


新建一個工程,activity_main.xml佈局如下所示:




很簡單,只是添加了一個居中的TextView

MainActivity代碼如下所示:



也是很簡單的幾行,在onCreate方法中創建了一個子線程,並進行UI訪問操作。

點擊運行。你會發現即使在子線程中訪問UI,程序一樣能跑起來。結果如下所示: 



咦,那爲嘛以前在子線程中更新UI會報錯呢?難道真的可以在子線程中訪問UI?


先不急,這是一個極端的情況,修改MainActivity如下:




讓子線程睡眠200毫秒,醒來後再進行UI訪問。

結果你會發現,程序崩了。這纔是正常的現象嘛。拋出瞭如下很熟悉的異常:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. 
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6581) 
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)

……

作爲一名開發者,我們應該認真閱讀一下這些異常信息,是可以根據這些異常信息來找到爲什麼一開始的那種情況可以訪問UI的。那我們分析一下異常信息:


首先,從以下異常信息可以知道

at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6581)

這個異常是從android.view.ViewRootImpl的checkThread方法拋出的。

這裏順便鋪墊一個知識點:ViewRootImpl是ViewRoot的實現類。

那現在跟進ViewRootImpl的checkThread方法瞧瞧,源碼如下:




只有那麼幾行代碼而已的,而mThread是主線程,在應用程序啓動的時候,就已經被初始化了。

由此我們可以得出結論: 


在訪問UI的時候,ViewRoot會去檢查當前是哪個線程訪問的UI,如果不是主線程,那就會拋出如下異常:

Only the original thread that created a view hierarchy can touch its views

這好像並不能解釋什麼?繼續看到異常信息

at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)

那現在就看看requestLayout方法,




這裏也是調用了checkThread()方法來檢查當前線程,咦?除了檢查線程好像沒有什麼信息。那再點進scheduleTraversals()方法看看




注意到postCallback方法的的第二個參數傳入了很像是一個後臺任務。那再點進去




找到了,那麼繼續跟進doTraversal()方法。




可以看到裏面調用了一個performTraversals()方法,View的繪製過程就是從這個performTraversals方法開始的。PerformTraversals方法的代碼有點長就不貼出來了,如果繼續跟進去就是學習View的繪製了。而我們現在知道了,每一次訪問了UI,Android都會重新繪製View。這個是很好理解的。


分析到了這裏,其實異常信息對我們幫助也不大了,它只告訴了我們子線程中訪問UI在哪裏拋出異常。 


而我們會思考:當訪問UI時,ViewRoot會調用checkThread方法去檢查當前訪問UI的線程是哪個,如果不是UI線程則會拋出異常,這是沒問題的。但是爲什麼一開始在MainActivity的onCreate方法中創建一個子線程訪問UI,程序還是正常能跑起來呢?? 


唯一的解釋就是執行onCreate方法的那個時候ViewRootImpl還沒創建,無法去檢查當前線程。


那麼就可以這樣深入進去。尋找ViewRootImpl是在哪裏,是什麼時候創建的。好,繼續前進

在ActivityThread中,我們找到handleResumeActivity方法,如下:




可以看到內部調用了performResumeActivity方法,這個方法看名字肯定是回調onResume方法的入口的,那麼我們還是跟進去瞧瞧。




可以看到r.activity.performResume()這行代碼,跟進 performResume方法,如下:




Instrumentation調用了callActivityOnResume方法,callActivityOnResume源碼如下:




找到了,activity.onResume()。這也證實了,performResumeActivity方法確實是回調onResume方法的入口。

那麼現在我們看回來handleResumeActivity方法,執行完performResumeActivity方法回調了onResume方法後, 
會來到這一塊代碼:




activity調用了makeVisible方法,這應該是讓什麼顯示的吧,跟進去探探。




往WindowManager中添加DecorView,那現在應該關注的就是WindowManager的addView方法了。而WindowManager是一個接口來的,我們應該找到WindowManager的實現類才行,而WindowManager的實現類是WindowManagerImpl。這個和ViewRoot是一樣,就是名字多了個impl。


找到了WindowManagerImpl的addView方法,如下:




裏面調用了WindowManagerGlobal的addView方法,那現在就鎖定 
WindowManagerGlobal的addView方法:




終於擊破,ViewRootImpl是在WindowManagerGlobal的addView方法中創建的。


回顧前面的分析,總結一下: 


ViewRootImpl的創建在onResume方法回調之後,而我們一開篇是在onCreate方法中創建了子線程並訪問UI,在那個時刻,ViewRootImpl是沒有創建的,無法檢測當前線程是否是UI線程,所以程序沒有崩潰一樣能跑起來,而之後修改了程序,讓線程休眠了200毫秒後,程序就崩了。很明顯200毫秒後ViewRootImpl已經創建了,可以執行checkThread方法檢查當前線程。


這篇博客的分析如題目一樣,Android中子線程真的不能更新UI嗎?在onCreate方法中創建的子線程訪問UI是一種極端的情況,這個不仔細分析源碼是不知道的。我是最近看了一個面試題,才發現這個。


從中我也學習到了從異常信息中跟進源碼尋找答案,你呢?


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章