Android只在UI主線程修改UI,是個謊言嗎? 為什麼這段代碼能完美運行?

我今天嘗試了下這個代碼,竟然沒報錯。 日誌也顯示這是兩個線程。 那麼其實在非UI線程也可以更新UI是嗎? (我其實是預期crash的, 沒想到連個日誌error都沒有,程序完美運行了)

-------------------------------------------------------------------------------------------------------------------------

多謝大家的回答。 Android確實是推薦在UI線程中更新UI,以免出現同步的問題。 我以前也出過錯,在其它線程上更新UI,結果系統異常了, 給出的error日誌是顯示要在UI線程中更新UI。 但我更驚訝的,也是更想知道的是: 為什麼這段代碼能正常運行,不出錯,也能更新UI ?


這是因為你的Thread執行的時候,ViewRootImpl還沒有對view tree的根節點DecorView執行performTraversals,view tree里的所有View都沒有被賦值mAttachInfo。

在onCreate完成時,Activity並沒有完成初始化view tree。view tree的初始化是從ViewRootImpl執行performTraversals開始,這個過程會對view tree進行從根節點DecorView開始的遍歷,對所有視圖完成初始化,初始化包括視圖的大小布局,以及AttachInfo,ViewParent等域的初始化。

執行ImageView.setImageResource,調用的過程是

ImageView.setImageResource
-&> View.invalidate
-&> View.invalidateInternal
-&> ViewGroup.invalidateChild
-&> ViewParent.invalidateChildInParent //這裡會不斷Loop去取上一個結點的mParent
-&> ViewRootImpl.invalidateChildInParent //DecorView的mParent是ViewRootImpl
-&> ViewRootImpl.checkThread //在這裡執行checkThread,如果非UI線程則拋出異常

但是在Thread執行setImageResource時,此時Activity還在初始化,ViewRoot沒有初始化整個view tree,ImageView的mAttachInfo是空的(mAttachInfo包含了Window的token等Binder)。而View.invalidateInternal調用ViewGroup.invalidateChild要判斷是否存在ViewParent和AttachInfo:

final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null ai != null l &< r t &< b) { //.... p.invalidateChild(this, damage); }

也就是說,此時因為不存在ViewParent,invalidate的過程中止而沒有完全執行,也即沒有發生checkThread。


我突然有一種醍醐灌頂的感覺。題主的問題,高票答案可以很好的解釋,但是問題可以稍微展開一下,而關鍵就是ViewRootImpl的checkThread方法。checkThead的源碼是這樣的:

void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}

可以看到,它檢查的並不是當前線程是否是UI線程,而是當前線程是否是操作線程。這個操作線程就是創建ViewRootImpl對象的線程:

public ViewRootImpl(Context context, Display display) {
...
mThread = Thread.currentThread();
...
}

所以,只要在創建Window/View/ViewRootView的線程中更新UI,就是合法的,就可以更新成功。

平時我們都在UI線程中創建Window/View/ViewRootView,所以只能在主線程中更新UI,但是如果我們從頭到尾都是在子線程中操作,那就是沒問題的。下面這段代碼,就可以完美運行:

new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
MyDialog dialog = new MyDialog(MainActivity.this);
dialog.show();
Looper.loop();
}
}).start();

如上,我們成功在子線程中創建並顯示了UI元素。

所以,Android要求我們在主線程中更新UI,只是一種建議,是以規範的形式解決多線程同步問題。其實子線程,完全有操作UI的能力。


大部分答主都講出原因了。我解釋下為什麼會要求只在 UI 線程修改 UI:

絕大部分 GUI 系統都是只允許「單個線程對某塊區域做繪製操作的」,例如在 Windows 和 Android 系統下都把這塊區域叫做 Window。如果允許多個線程對同一塊區域做繪製的話,很有可能導致某個線程還沒繪製完,另一個線程上一些依賴之前繪製結果的操作出問題,例如 Alpha 混合。除非加鎖,然而加鎖更慢還不如單線程。


在UI主線程修改UI,這屬於一個「建議」,而不是「標準」。因為在子線程定義UI的修改,你無法預料到UI會被如何修改。打個比方,你定義兩個耗時的子線程分別對ImageView進行定義,最後你無法預料最後ImageView會被定義成什麼。

另外我們可以看一下Android的特有的AsyncTask。它為了更新主UI專門設置了一個方法onProgressUpdate,而在實際負責運行代碼的過程doInBackground中,你只是使用publishProgress把值傳給onProgressUpdate。這就說明了,Android自己也是避免在子線程更新UI的。


16年7月更新

看了我14年的回答真是…

這個問題查一下ViewRootImpl的源碼的checkThread()就都明白了。注意ViewRootImpl是在activity的視圖完全初始化完畢之後才有的,參看View的mAttatchInfo對象,這個mAttatchInfo是在attatchedToWindow的時候賦予的。

排名第一的答案說的已經很好了。

//以下內容是14年的回答。

我也試了一段代碼,在新線程里對ProgressBar進行操作,(雖然setProgress這個方法加了synchronized),不會報錯。

但是Toast就不可以在非UI線程彈出。(報錯是沒有looper.prepare())

關於樓主問的問題,還等高手回答。。。

看到一篇博文寫的挺好

Android UI線程和非UI線程

———————————————我是分割線———————————————

試了一下蔣奇的回答

setProgress這個方法加了synchronized,在執行前會檢查。

我試了下textView.setText的方法,在新線程中Thread.sleep(100)之後再調用setText會報錯(超過100也會報錯 E/AndroidRuntime(28000): android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

),不sleep或者sleep很短(我試了10)是可以正常執行的。


推薦閱讀:

MIUI 是否會被其他各廠商同質化?
學會不做虧本生意的 Google,即將完成自己的最後一塊拼圖
Hola Launcher 和 APUS Launcher 比起來,哪個產品更好?
索尼手機做工精良,業界評價甚高,為什麼銷量就是上不去?
Android 如何簡單集成 Emoji 鍵盤

TAG:應用程序軟體 | Android |