1. 程式人生 > >Android 中子執行緒真的不能更新UI嗎?

Android 中子執行緒真的不能更新UI嗎?

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

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

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

Java
123456789101112131415 <?xml version="1.0"encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid:id="@+id/main_tv"android:layout_width
="wrap_content"android:layout_height="wrap_content"android:textSize="18sp"android:layout_centerInParent="true"/></RelativeLayout>

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

MainActivity程式碼如下所示:

Java
12345678910111213141516171819202122 publicclassMainActivityextendsAppCompatActivity{privateTextView main_tv;@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);main_tv=(TextView)findViewById(R.id.main_tv);newThread(newRunnable(){@Overridepublicvoidrun(){main_tv.setText("子執行緒中訪問");}}).start();}}

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

點選執行。你會發現即使在子執行緒中訪問UI,程式一樣能跑起來。結果如下所示:

這裡寫圖片描述

咦,那為嘛以前在子執行緒中更新UI會報錯呢?難道真的可以在子執行緒中訪問UI?

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

Java
123456789101112131415161718192021222324252627 publicclassMainActivityextendsAppCompatActivity{privateTextView main_tv;@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);main_tv=(TextView)findViewById(R.id.main_tv);newThread(newRunnable(){@Overridepublicvoidrun(){try{Thread.sleep(200);}catch(InterruptedExceptione){e.printStackTrace();}main_tv.setText("子執行緒中訪問");}}).start();}}

讓子執行緒睡眠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的。那我們分析一下異常資訊:

首先,從以下異常資訊可以知道

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

這個異常是從android.view.ViewRootImpl的checkThread方法丟擲的。

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

Java
123456 voidcheckThread(){if(mThread!=Thread.currentThread()){thrownewCalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views.");}}

只有那麼幾行程式碼而已的,而mThread是主執行緒,在應用程式啟動的時候,就已經被初始化了。

由此我們可以得出結論:
在訪問UI的時候,ViewRootImpl會去檢查當前是哪個執行緒訪問的UI,如果不是主執行緒,那就會丟擲如下異常:

Java
1 Only the original thread that createdaview hierarchy can touch its views

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

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

那現在就看看requestLayout方法,

Java
12345678 @OverridepublicvoidrequestLayout(){if(!mHandlingLayoutInLayoutRequest){checkThread();mLayoutRequested=true;scheduleTraversals();}}

這裡也是呼叫了checkThread()方法來檢查當前執行緒,咦?除了檢查執行緒好像沒有什麼資訊。那再點進scheduleTraversals()方法看看

Java
12345678910111213 voidscheduleTraversals(){if(!mTraversalScheduled){mTraversalScheduled=true;mTraversalBarrier=mHandler.getLooper().getQueue().postSyncBarrier();mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL,mTraversalRunnable,null);if(!mUnbufferedInputDispatch){scheduleConsumeBatchedInput();}notifyRendererOfFramePending();pokeDrawLockIfNeeded();}}

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

Java
123456 finalclassTraversalRunnableimplementsRunnable{@Overridepublicvoidrun(){doTraversal();}}

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

Java
1234567891011121314151617 voiddoTraversal(){if(mTraversalScheduled){mTraversalScheduled=false;mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);if(mProfile){Debug.startMethodTracing("ViewAncestor");}performTraversals();if(mProfile){Debug.stopMethodTracing();mProfile=false;}}}

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

分析到了這裡,其實異常資訊對我們幫助也不大了,它只告訴了我們子執行緒中訪問UI在哪裡丟擲異常。
而我們會思考:當訪問UI時,ViewRootImpl會呼叫checkThread方法去檢查當前訪問UI的執行緒是哪個,如果不是UI執行緒則會丟擲異常,這是沒問題的。但是為什麼一開始在MainActivity的onCreate方法中建立一個子執行緒訪問UI,程式還是正常能跑起來呢??
唯一的解釋就是執行onCreate方法的那個時候ViewRootImpl還沒建立,無法去檢查當前執行緒。

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

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

Java
123456789101112131415161718192021222324 finalvoidhandleResumeActivity(IBinder token,booleanclearHide,booleanisForward,booleanreallyResume){// If we are getting ready to gc after going to the background, well// we are back active so skip it.unscheduleGcIdler();mSomeActivitiesChanged=true;// TODO Push resumeArgs into the activity for considerationActivityClientRecordr=performResumeActivity(token,clearHide);if(r!=null){finalActivitya=r.activity;//程式碼省略r.activity.mVisibleFromServer=true;mNumVisibleActivities++;if(r