為什麼我們可以在非UI執行緒中更新UI
尊重原創轉載請註明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵權必究!
炮兵鎮樓
看到這樣的標題……估計N多人會說我是逗比…………因為很多盆友在學習Android(特別是從4.0之後開始入門的)的時候都會常看見或聽到別人說我們更新UI呢要在UI執行緒(或者說主執行緒)中去更新UI,不要在子執行緒中更新UI,而Android官方呢也建議我們不要在非UI執行緒直接更新UI,為什麼呢?藉助Android官方的一句話來說就是:
“The Android UI toolkit is not thread-safe and the view must always be manipulated on the UI thread.”
因此,很多童鞋會有這麼一個慣性思維:在非UI執行緒中不能更新UI!既然Android不建議我們這麼做,那其必定會對我們在code時做一些限制,比如當我們嘗試執行如下程式碼時:
/** * 主介面 * * @author Aige {@link http://blog.csdn.net/aigestudio} * @since 2014/11/17 */public class MainActivity extends Activity { private TextView tvText; @Override public void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvText = (TextView) findViewById(R.id.main_tv); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } tvText.setText("OtherThread" ); } }).start(); }}
為了把情況說明,這裡我也將xml佈局檔案程式碼貼出來:<!-- http://blog.csdn.net/aigestudio --><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:background="#ffffff" android:layout_height="match_parent" > <TextView android:id="@+id/main_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" /></LinearLayout>
當我們執行上述程式碼後,你便會在Logcat中得到如下error提示:android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
這句話非常簡單,而且……我相信每個做Android開發到一定時間的盆友都碰到過,Android通過檢查我們當前的執行緒是否為UI執行緒從而丟擲一個自定義的AndroidRuntimeException來提醒我們“Only the original thread that created a view hierarchy can touch its views”並強制終止程式執行,具體的實現在ViewRootImpl類的checkThread方法中:
@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks { // 省去海量程式碼………………………… void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } } // 省去巨量程式碼……………………}
這就是Android在4.0後對我們做出的一個限制。寫這篇Blog的具體原因來自凱子哥的一篇博文:來來來,同學,咱們討論一下“只能在UI主執行緒更新View”這件小事,鑑於凱子哥如此好學,我想想呢也許很多盆友也有類似疑問:究竟TM到底能不能在非UI執行緒中更新UI呢?同時也為了引出我對在非UI執行緒更新UI方法的一些總結,我決定在3/4之前先擼一篇Blog掃清障礙先。首先,我先回答幾個問題包括凱子哥的:- 究竟TM到底能不能在非UI執行緒中更新UI呢?答案:能、當然可以
- View的執行和Activity的生命週期有什麼必然聯絡嗎?答案:沒有、或者隱晦地說沒有必然聯絡
- 除了Handler外是否還有更簡便的方式在非UI執行緒更新UI呢?答案:有、而且還不少
OK,這裡我們再來看一下上面的一段程式碼,線上程中我呼叫了Thread.sleep(200);來讓我們的匿名執行緒暫停了200ms,如果……假如……我們去掉它的話……………………會發生什麼?來試試:
/** * 主介面 * * @author Aige {@link http://blog.csdn.net/aigestudio} * @since 2014/11/17 */public class MainActivity extends Activity { private TextView tvText; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvText = (TextView) findViewById(R.id.main_tv); new Thread(new Runnable() { @Override public void run() { tvText.setText("OtherThread"); } }).start(); }}
這時你會發現我們的程式碼TM地正確執行了!而且我們的TextView正確顯示出了“OtherThread”文字!看到螢幕上的這11個英文字母我相信大家又把剛放出來又吸進去的屁再一次地放了出來…………這就是凱子哥Blog中提到的問題,我們成功地在非UI執行緒中更新了UI。其實這裡最最根本的原因是我們並沒有checkThread我們的當前執行緒,而我在文章最開始的程式碼中通過Thread.sleep(200)暫停了一小段時間,這裡為什麼回暫停執行緒一段時間?在這段時間的背後Android背地裡揹著我們都幹了什麼?數百頭母驢為何半夜慘叫?小賣部安全套為何屢遭黑手?女生宿舍內褲為何頻頻失竊?連環強姦母豬案,究竟是何人所為?老尼姑的門夜夜被敲,究竟是人是鬼?數百隻小母狗意外身亡的背後又隱藏著什麼?這一切的背後, 是人性的扭曲還是道德的淪喪?是性的爆發還是飢渴的無奈?抱歉……磕個藥,上面我們講到,我們能正確以上述程式碼的方式在非UI執行緒中更新UI而不報錯,那麼原因也許只有一個,那就是沒有執行checkThread方法去檢查我們的當前執行緒……但是,細看呼叫checkThread方法的呼叫方法們你就會發現,TM全是跟View建立生成相關:也就是說一旦我們嘗試去對我們的控制元件進行生成,這些方法其中一個必然會被呼叫,這時候很多朋友就會蛋疼了…………但是,請不要被checkThread方法的思維所束縛,這時候你該擴大你的思維範疇,既然checkThread方法屬於ViewRootImpl的成員方法,那麼會不會是此時我們的ViewRootImpl根本就沒被建立呢?懷著這個出發點,我們再度審視ActivtyThread排程Activity生命週期的各個環節,首先看看performLaunchActivity方法中的處理:
public final class ActivityThread { // 省去海量程式碼………………………… private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { ActivityInfo aInfo = r.activityInfo; // 省去對packageInfo的邏輯處理 // 省去對ComponentName的邏輯處理 Activity activity = null; try { java.lang.ClassLoader cl = r.packageInfo.getClassLoader(); // 通過Instrumentation物件生成Activity類的例項 activity = mInstrumentation.newActivity( cl, component.getClassName(), r.intent); // 省去三行程式碼………… } catch (Exception e) { // 省去對異常的捕獲處理 } try { Application app = r.packageInfo.makeApplication(false, mInstrumentation); // 省去多行無關程式碼 if (activity != null) { Context appContext = createBaseContextForActivity(r, activity); CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager()); Configuration config = new Configuration(mCompatConfiguration); // 省去多行無關程式碼 if (customIntent != null) { activity.mIntent = customIntent; } r.lastNonConfigurationInstances = null; activity.mStartedActivity = false; int theme = r.activityInfo.getThemeResource(); if (theme != 0) { activity.setTheme(theme); } /* * 呼叫callActivityOnCreate方法處理Create邏輯 */ activity.mCalled = false; mInstrumentation.callActivityOnCreate(activity, r.state); if (!activity.mCalled) { // 省去多行無關程式碼 } r.activity = activity; r.stopped = true; /* * 呼叫performStart方法處理Start邏輯 */ if (!r.activity.mFinished) { activity.performStart(); r.stopped = false; } // 省去多行無關程式碼 } // 省去兩行無關程式碼 } catch (SuperNotCalledException e) { // 省去對異常的捕獲處理 } catch (Exception e) { // 省去對異常的捕獲處理 } return activity; } // 省去巨量程式碼……………………}
performLaunchActivity方法中目測木有我我們想要的資訊,其建立了Activity並排程了Create和Start的邏輯處理,那我們看看callActivityOnCreate方法呢:public class Instrumentation { // 省去海量程式碼………………………… public void callActivityOnCreate(Activity activity, Bundle icicle) { // 省去某些邏輯…… activity.performCreate(icicle); // 省去某些邏輯…… } // 省去巨量程式碼……………………}
callActivityOnCreate中除了對MQ的一些排程外最重要的還是通過Activity的例項呼叫了performCreate方法:public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2, Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener, ComponentCallbacks2 { // 省去海量程式碼………………………… final void performCreate(Bundle icicle) { onCreate(icicle); mVisibleFromClient = !mWindow.getWindowStyle().getBoolean( com.android.internal.R.styleable.Window_windowNoDisplay, false); mFragments.dispatchActivityCreated(); } // 省去巨量程式碼……………………}
performCreate方法邏輯就更乾脆了,最主要的還是呼叫了我們Activity的onCreate方法,我們沒在這裡找到我們想要的東西,那再來看performStart:public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2, Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener, ComponentCallbacks2 { // 省去海量程式碼………………………… final void performStart() { mFragments.noteStateNotSaved(); mCalled = false; mFragments.execPendingActions(); mInstrumentation.callActivityOnStart(this); if (!mCalled) { throw new SuperNotCalledException( "Activity " + mComponent.toShortString() + " did not call through to super.onStart()"); } mFragments.dispatchStart(); if (mAllLoaderManagers != null) { final int N = mAllLoaderManagers.size(); LoaderManagerImpl loaders[] = new LoaderManagerImpl[N]; for (int i=N-1; i>=0; i--) { loaders[i] = mAllLoaderManagers.valueAt(i); } for (int i=0; i<N; i++) { LoaderManagerImpl lm = loaders[i]; lm.finishRetain(); lm.doReportStart(); } } } // 省去巨量程式碼……………………}
performStart相對於performCreate有更多的邏輯處理,但依然木有我們想要的結果,其最終還是同過Instrumentation物件呼叫callActivityOnStart:public class Instrumentation { // 省去海量程式碼………………………… public void callActivityOnStart(Activity activity) { activity.onStart(); } // 省去巨量程式碼……………………}
callActivityOnStart僅僅是呼叫了Activity的onStart方法,同樣……onStart方法中也沒有我們想要的結果~~~~我們抱著即將從埃菲爾鐵塔頂端做自由落體的心態繼續看onResume方法的排程,其在ActivityThread中通過handleResumeActivity排程:public final class ActivityThread { // 省去海量程式碼………………………… final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { unscheduleGcIdler(); ActivityClientRecord r = performResumeActivity(token, clearHide); if (r != null) { final Activity a = r.activity; // 省去無關程式碼………… final int forwardBit = isForward ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0; boolean willBeVisible = !a.mStartedActivity; if (!willBeVisible) { try { willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible( a.getActivityToken()); } catch (RemoteException e) { } } if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (a.mVisibleFromClient) { a.mWindowAdded = true; wm.addView(decor, l); } } else if (!willBeVisible) { // 省去無關程式碼………… r.hideForNow = true; } cleanUpPendingRemoveWindows(r); if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) { if (r.newConfig != null) { // 省去無關程式碼………… performConfigurationChanged(r.activity, r.newConfig); freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.newConfig)); r.newConfig = null; } // 省去無關程式碼………… WindowManager.LayoutParams l = r.window.getAttributes(); if ((l.softInputMode & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != forwardBit) { l.softInputMode = (l.softInputMode & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)) | forwardBit; if (r.activity.mVisibleFromClient) { ViewManager wm = a.getWindowManager(); View decor = r.window.getDecorView(); wm.updateViewLayout(decor, l); } } r.activity.mVisibleFromServer = true; mNumVisibleActivities++; if (r.activity.mVisibleFromClient) { r.activity.makeVisible(); } } if (!r.onlyLocalRequest) { r.nextIdle = mNewActivities; mNewActivities = r; // 省去無關程式碼………… Looper.myQueue().addIdleHandler(new Idler()); } r.onlyLocalRequest = false; // 省去與ActivityManager的通訊處理 } else { // 省略異常發生時對Activity的處理邏輯 } } // 省去巨量程式碼……………………}
handleResumeActivity方法邏輯相對要複雜一些,除了一啪啦對當前顯示Window的邏輯判斷以及沒建立的初始化等等工作外其在最終會呼叫Activity的makeVisible方法:public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2, Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener, ComponentCallbacks2 { // 省去海量程式碼………………………… void makeVisible() { if (!mWindowAdded) { ViewManager wm = getWindowManager(); wm.addView(mDecor, getWindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(View.VISIBLE); } // 省去巨量程式碼……………………}
在makeVisible方法中邏輯相當簡單,獲取一個視窗管理器物件並將我們曾在自定義控制元件其實很簡單7/12中提到過的根檢視DecorView新增到其中,addView的具體實現在WindowManagerGlobal中:public final class WindowManagerGlobal { public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { // 省去很多程式碼 ViewRootImpl root; // 省去一行程式碼 synchronized (mLock) { // 省去無關程式碼 root = new ViewRootImpl(view.getContext(), display); // 省去一行程式碼 // 省去一行程式碼 mRoots.add(root); // 省去一行程式碼 } // 省去部分程式碼 }}
在addView生成了一個ViewRootImpl物件並將其儲存在了mRoots陣列中,每當我們addView一次,就會生成一個ViewRootImpl物件,其實看到這裡我們還可以擴充套件一下問題一個APP是否可以擁有多個根檢視呢?答案是肯定的,因為只要我呼叫了addView方法,我們傳入的View引數就可以被認為是一個根檢視,但是!在framework的預設實現中有且僅有一個根檢視,那就是我們上面makeVisible方法中addView進去的DecorView,所以為什麼我們可以說一個APP雖然可以有多個Activity,但是每個Activity只會有一個Window一個DecorView一個ViewRootImpl,看到這裡很多童鞋依然會問,也就是說在onResume方法被執行後我們的ViewRootImpl才會被生成對吧,但是為什麼下面的程式碼依然可以正確執行呢:/** * 主介面 * * @author Aige {@link http://blog.csdn.net/aigestudio} * @since 2014/11/17 */public class MainActivity extends Activity { private TextView tvText; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvText = (TextView) findViewById(R.id.main_tv); } @Override protected void onResume() { super.onResume(); new Thread(new Runnable() { @Override public void run() { tvText.setText("OtherThread"); } }).start(); }}
沒錯,可以執行!首先我們這裡的是個執行緒,其次這裡要涉及framework對UI事件處理的方式,我們在Android翻頁效果原理實現之引入折線中曾說過Android對UI事件的處理需要依賴於Message Queue,當一個Msg被壓入MQ到處理這個過程並非立即的,它需要一段事件,我們線上程中通過Thread.sleep(200)在等,在等什麼呢?在等ViewRootImpl的例項物件被建立,有關於GUI中Message Queue的處理如有機會我會濃縮在《深入理解 Android GUI 框架》系列中,這裡就暫且先不說了,那麼又有同學會問了!納尼,既然ViewRootImpl還未被建立那麼為什麼會能繪製出文本?!!!如果你有這個疑問,我只能說你觀察細緻問得好,但是,這個問題我不打算解答,留給各位,上面我其實就在教大家如何去尋找原因了,漁已授之於你所以就不再多說了~~~~既然我們找到了原因所在,那麼我們該如何擺脫“The Android UI toolkit is not thread-safe and the view must always be manipulated on the UI thread.”這個噩夢呢?