Android子執行緒真的不能更新UI麼
Android單執行緒模型是這樣描述的:
Android UI操作並不是執行緒安全的,並且這些操作必須在UI執行緒執行
如果在其它執行緒訪問UI執行緒,Android提供了以下的方式:
Activity.runOnUiThread(Runnable) View.post(Runnable) View.postDelayed(Runnable, long) Handler
為什麼呢?在子執行緒中就不能操作UI麼?
當一個程式第一次啟動的時候,Android會同時啟動一個對應的主執行緒,這個主執行緒就是UI執行緒,也就是ActivityThread。UI執行緒主要負責處理與UI相關的事件,如使用者的按鍵點選、使用者觸控螢幕以及螢幕繪圖等。系統不會為每個元件單獨建立一個執行緒,在同一個程式裡的UI元件都會在UI執行緒裡例項化,系統對每一個元件的呼叫都從UI執行緒分發出去。所以,響應系統回撥的方法永遠都是在UI執行緒裡執行,如響應使用者動作的onKeyDown()的回撥。
那為什麼選擇一個主執行緒幹這些活呢?換個說法,Android為什麼使用單執行緒模型,它有什麼好處?
先讓我們看下單執行緒化的事件佇列模型是怎麼定義的:
採用一個專門的執行緒從佇列中抽取事件,並把他們轉發給應用程式定義的事件處理器
這看起來就是Android的訊息佇列、Looper和Handler嘛。類似知識請參考: 深入理解Message, MessageQueue, Handler和Looper
其實現代GUI框架就是使用了類似這樣的模型:模型建立一個專門的執行緒,事件派發執行緒來處理GUI事件。單執行緒化也不單單存在Android中,Qt、XWindows等都是單執行緒化。當然,也有人試圖用多執行緒的GUI,最終由於競爭條件和死鎖導致的穩定性問題等,又回到單執行緒化的事件佇列模型老路上來。單執行緒化的GUI框架通過限制來達到現場安全:所有GUI中的物件,包括可視元件和資料模型,都只能被事件執行緒訪問。
這就解釋了Android為什麼使用單執行緒模型。
那Android的UI操作並不是執行緒安全的又是怎麼回事?
Android實現View更新有兩組方法,分別是invalidate和postInvalidate。前者在UI執行緒中使用,後者在非UI執行緒中使用。換句話說,Android的UI操作不是執行緒安全可以表述為invalidate在子執行緒中呼叫會導致執行緒不安全。作一個假設,現在我用invalidate在子執行緒中重新整理介面,同時UI執行緒也在用invalidate重新整理介面,這樣會不會導致介面的重新整理不能同步?既然重新整理不同步,那麼invalidate就不能在子執行緒中使用。這就是invalidate不能在子執行緒中使用的原因。
postInvalidate可以在子執行緒中使用,它是怎麼做到的?
看看原始碼是怎麼實現的:
public void postInvalidate() { postInvalidateDelayed(0); } public void postInvalidateDelayed(long delayMilliseconds) { // We try only with the AttachInfo because there's no point in invalidating // if we are not attached to our window if (mAttachInfo != null) { Message msg = Message.obtain(); msg.what = AttachInfo.INVALIDATE_MSG; msg.obj = this; mAttachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds); } }
說到底還是通過Handler的sendMessageDelayed啊,還是逃不過訊息佇列,最終還是交給UI執行緒處理。所以View的更新只能由UI執行緒處理。
如果我非要在子執行緒中更新UI,那會出現什麼情況呢?
android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
拋了一個CalledFromWrongThreadException異常。
相信很多人遇到這個異常後,就會通過前面的四種方式中的其中一種解決:
Activity.runOnUiThread(Runnable) View.post(Runnable) View.postDelayed(Runnable, long) Handler
說到底還沒觸發到根本,為什麼會出現這個異常呢?這個異常在哪裡丟擲來的呢?
void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } }
該程式碼出自 framework/base/core/java/android/view/ViewRootImpl.java
再看下ViewRootImpl的建構函式,mThread就是在這初始化的:
public ViewRootImpl(Context context, Display display) { mContext = context; mWindowSession = WindowManagerGlobal.getWindowSession(); mDisplay = display; mBasePackageName = context.getBasePackageName(); mDisplayAdjustments = display.getDisplayAdjustments(); mThread = Thread.currentThread(); ...... }
再研究一下這個CalledFromWrongThreadException異常的堆疊,會發現最後到了invalidateChild和invalidateChildInParent方法中:
@Override public void invalidateChild(View child, Rect dirty) { invalidateChildInParent(null, dirty); } @Override public ViewParent invalidateChildInParent(int[] location, Rect dirty) { checkThread(); ...... }
最終通過checkThread形成了這個異常。說到底,非UI執行緒是可以重新整理UI的呀,前提是它要擁有自己的ViewRoot。如果想直接建立ViewRoot例項,你會發現找不到這個類。那怎麼做呢?通過WindowManager。
class NonUiThread extends Thread{ @Override public void run() { Looper.prepare(); TextView tx = new TextView(MainActivity.this); tx.setText("non-UiThread update textview"); WindowManager windowManager = MainActivity.this.getWindowManager(); WindowManager.LayoutParams params = new WindowManager.LayoutParams( 200, 200, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW, WindowManager.LayoutParams.TYPE_TOAST,PixelFormat.OPAQUE); windowManager.addView(tx, params); Looper.loop(); } }
就是通過windowManager.addView建立了ViewRoot,WindowManagerImpl.java中的addView方法:
@Override public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { applyDefaultToken(params); mGlobal.addView(view, params, mDisplay, mParentWindow); }
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
mGlobal是一個WindowManagerGlobal例項,程式碼在 frameworks/base/core/java/android/view/WindowManagerGlobal.java中,具體實現如下:
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { if (view == null) { throw new IllegalArgumentException("view must not be null"); } if (display == null) { throw new IllegalArgumentException("display must not be null"); } if (!(params instanceof WindowManager.LayoutParams)) { throw new IllegalArgumentException("Params must be WindowManager.LayoutParams"); } final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params; if (parentWindow != null) { parentWindow.adjustLayoutParamsForSubWindow(wparams); } else { // If there's no parent, then hardware acceleration for this view is // set from the application's hardware acceleration setting. final Context context = view.getContext(); if (context != null && (context.getApplicationInfo().flags & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) { wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; } } ViewRootImpl root; View panelParentView = null; synchronized (mLock) { // Start watching for system property changes. if (mSystemPropertyUpdater == null) { mSystemPropertyUpdater = new Runnable() { @Override public void run() { synchronized (mLock) { for (int i = mRoots.size() - 1; i >= 0; --i) { mRoots.get(i).loadSystemProperties(); } } } }; SystemProperties.addChangeCallback(mSystemPropertyUpdater); } int index = findViewLocked(view, false); if (index >= 0) { if (mDyingViews.contains(view)) { // Don't wait for MSG_DIE to make it's way through root's queue. mRoots.get(index).doDie(); } else { throw new IllegalStateException("View " + view + " has already been added to the window manager."); } // The previous removeView() had not completed executing. Now it has. } // If this is a panel window, then find the window it is being // attached to for future reference. if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW && wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) { final int count = mViews.size(); for (int i = 0; i < count; i++) { if (mRoots.get(i).mWindow.asBinder() == wparams.token) { panelParentView = mViews.get(i); } } } root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); mRoots.add(root); mParams.add(wparams); } // do this last because it fires off messages to start doing things try { root.setView(view, wparams, panelParentView); } catch (RuntimeException e) { // BadTokenException or InvalidDisplayException, clean up. synchronized (mLock) { final int index = findViewLocked(view, false); if (index >= 0) { removeViewLocked(index, true); } } throw e; } }
所以,非UI執行緒能更新UI,只要它有自己的ViewRoot。
延伸一下:Android Activity本身是在什麼時候建立ViewRoot的呢?
既然是單執行緒模型,就要先找到這個UI執行緒實現類ActivityThread,看裡面哪裡addView了。沒錯,是在onResume裡面,對應ActivityThread就是handleResumeActivity這個方法:
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { // 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 consideration ActivityClientRecord r = performResumeActivity(token, clearHide); ...... 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); } // If the window has already been added, but during resume // we started another activity, then don't yet make the // window visible. } else if (!willBeVisible) { if (localLOGV) Slog.v( TAG, "Launch " + r + " mStartedActivity set"); r.hideForNow = true; } ...... }
所以,如果在onCreate中通過子執行緒直接更新UI,並不會拋CalledFromWrongThreadException異常。但是一般情況下,我們不會在onCreate中做這樣的事情。
這就是Android為我們設計的單執行緒模型,核心就是一句話:Android UI操作並不是執行緒安全的,並且這些操作必須在UI執行緒執行。但這一句話背後,卻隱藏著我們平時看不見的程式碼實現,只有搞懂這些,我們才能知其然知其所以然。
相關文章
- Android 子執行緒 UI 操作真的不可以?Android執行緒UI
- 如何在子執行緒中更新UI執行緒UI
- 子執行緒與UI執行緒的通訊(委託)執行緒UI
- 子執行緒 UI 問題捉蟲執行緒UI
- 為何要在主執行緒上更新UI執行緒UI
- iOS 在主執行緒操作UI不能保證安全iOS執行緒UI
- Android多執行緒之執行緒池Android執行緒
- Android程式框架:執行緒與執行緒池Android框架執行緒
- Android 進階 ———— Handler系列之建立子執行緒HandlerAndroid執行緒
- Android執行緒池Android執行緒
- QT 主執行緒子執行緒互相傳值QT執行緒
- 程式設計之路第17天:不能在ui執行緒執行阻塞操作,請使用setTimeout代替????程式設計UI執行緒
- android程式和執行緒Android執行緒
- 執行緒的生命週期,真的沒那麼簡單執行緒
- pyqt5 子執行緒如何操作主執行緒GUIQT執行緒GUI
- 模擬主執行緒等待子執行緒的過程執行緒
- 走進Java Android 的執行緒世界(二)執行緒池JavaAndroid執行緒
- java多執行緒程式設計:你真的瞭解執行緒中斷嗎?Java執行緒程式設計
- Android中的執行緒池Android執行緒
- Android執行緒間通訊Android執行緒
- 多執行緒C++更新MYSQL執行緒C++MySql
- Android進階:六、在子執行緒中直接使用 Toast 及其原理Android執行緒AST
- Swift多執行緒:使用Thread進行多執行緒間通訊,協調子執行緒任務Swift執行緒thread
- Android技術分享|【Android踩坑】懷疑人生,主執行緒修改UI也會崩潰?Android執行緒UI
- Android中的執行緒通訊Android執行緒
- Android JNI 中的執行緒操作Android執行緒
- Android 基礎知識——執行緒Android執行緒
- Android 多執行緒-----AsyncTask詳解Android執行緒
- Android入門教程 | 多執行緒Android執行緒
- Android執行緒池使用介紹Android執行緒
- 子執行緒使用父執行緒RequestScope作用域Bean問題的探究執行緒Bean
- (MFC)子執行緒的資料如何傳遞給主執行緒中?執行緒
- performSelector:withObject:afterDelay: 在子執行緒中呼叫不執行performSelectorObject執行緒
- Java中執行緒池,你真的會用嗎?Java執行緒
- 什麼是Python執行緒?Python執行緒如何建立?Python執行緒
- iOS拾遺——為什麼必須在主執行緒操作UIiOS執行緒UI
- 多執行緒------執行緒與程式/執行緒排程/建立執行緒執行緒
- 2019Android多執行緒總結Android執行緒
- 瞭解 Android 的程式和執行緒Android執行緒