【转】Android子线程真的不能更新UI么
Android單線程模型是這樣描述的:
Android UI操作并不是線程安全的,并且這些操作必須在UI線程執(zhí)行如果在其它線程訪問UI線程,Android提供了以下的方式:
Activity.runOnUiThread(Runnable) View.post(Runnable) View.postDelayed(Runnable, long) Handler為什么呢?在子線程中就不能操作UI么?
當(dāng)一個(gè)程序第一次啟動(dòng)的時(shí)候,Android會(huì)同時(shí)啟動(dòng)一個(gè)對(duì)應(yīng)的主線程,這個(gè)主線程就是UI線程,也就是ActivityThread。UI線程主要負(fù)責(zé)處理與UI相關(guān)的事件,如用戶的按鍵點(diǎn)擊、用戶觸摸屏幕以及屏幕繪圖等。系統(tǒng)不會(huì)為每個(gè)組件單獨(dú)創(chuàng)建一個(gè)線程,在同一個(gè)進(jìn)程里的UI組件都會(huì)在UI線程里實(shí)例化,系統(tǒng)對(duì)每一個(gè)組件的調(diào)用都從UI線程分發(fā)出去。所以,響應(yīng)系統(tǒng)回調(diào)的方法永遠(yuǎn)都是在UI線程里運(yùn)行,如響應(yīng)用戶動(dòng)作的onKeyDown()的回調(diào)。
那為什么選擇一個(gè)主線程干這些活呢?換個(gè)說法,Android為什么使用單線程模型,它有什么好處?
先讓我們看下單線程化的事件隊(duì)列模型是怎么定義的:
采用一個(gè)專門的線程從隊(duì)列中抽取事件,并把他們轉(zhuǎn)發(fā)給應(yīng)用程序定義的事件處理器這看起來就是Android的消息隊(duì)列、Looper和Handler嘛。類似知識(shí)請(qǐng)參考:深入理解Message, MessageQueue, Handler和Looper
其實(shí)現(xiàn)代GUI框架就是使用了類似這樣的模型:模型創(chuàng)建一個(gè)專門的線程,事件派發(fā)線程來處理GUI事件。單線程化也不單單存在Android中,Qt、XWindows等都是單線程化。當(dāng)然,也有人試圖用多線程的GUI,最終由于競(jìng)爭(zhēng)條件和死鎖導(dǎo)致的穩(wěn)定性問題等,又回到單線程化的事件隊(duì)列模型老路上來。單線程化的GUI框架通過限制來達(dá)到線程安全:所有GUI中的對(duì)象,包括可視組件和數(shù)據(jù)模型,都只能被事件線程訪問。
這就解釋了Android為什么使用單線程模型。
那Android的UI操作并不是線程安全的又是怎么回事?
Android實(shí)現(xiàn)View更新有兩組方法,分別是invalidate和postInvalidate。前者在UI線程中使用,后者在非UI線程中使用。換句話說,Android的UI操作不是線程安全可以表述為invalidate在子線程中調(diào)用會(huì)導(dǎo)致線程不安全。作一個(gè)假設(shè),現(xiàn)在我用invalidate在子線程中刷新界面,同時(shí)UI線程也在用invalidate刷新界面,這樣會(huì)不會(huì)導(dǎo)致界面的刷新不能同步?既然刷新不同步,那么invalidate就不能在子線程中使用。這就是invalidate不能在子線程中使用的原因。
postInvalidate可以在子線程中使用,它是怎么做到的?
看看源碼是怎么實(shí)現(xiàn)的:
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 windowif (mAttachInfo != null) {Message msg = Message.obtain();msg.what = AttachInfo.INVALIDATE_MSG;msg.obj = this;mAttachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds);} }說到底還是通過Handler的sendMessageDelayed啊,還是逃不過消息隊(duì)列,最終還是交給UI線程處理。所以View的更新只能由UI線程處理。
如果我非要在子線程中更新UI,那會(huì)出現(xiàn)什么情況呢?
android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.拋了一個(gè)CalledFromWrongThreadException異常。
相信很多人遇到這個(gè)異常后,就會(huì)通過前面的四種方式中的其中一種解決:
Activity.runOnUiThread(Runnable) View.post(Runnable) View.postDelayed(Runnable, long) Handler說到底還沒觸發(fā)到根本,為什么會(huì)出現(xiàn)這個(gè)異常呢?這個(gè)異常在哪里拋出來的呢?
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的構(gòu)造函數(shù),mThread就是在這初始化的:
public ViewRootImpl(Context context, Display display) {mContext = context;mWindowSession = WindowManagerGlobal.getWindowSession();mDisplay = display;mBasePackageName = context.getBasePackageName();mDisplayAdjustments = display.getDisplayAdjustments();mThread = Thread.currentThread();...... }再研究一下這個(gè)CalledFromWrongThreadException異常的堆棧,會(huì)發(fā)現(xiàn)最后到了invalidateChild和invalidateChildInParent方法中:
@Override public void invalidateChild(View child, Rect dirty) {invalidateChildInParent(null, dirty); }@Override public ViewParent invalidateChildInParent(int[] location, Rect dirty) {checkThread();...... }最終通過checkThread形成了這個(gè)異常。說到底,非UI線程是可以刷新UI的呀,前提是它要擁有自己的ViewRoot。如果想直接創(chuàng)建ViewRoot實(shí)例,你會(huì)發(fā)現(xiàn)找不到這個(gè)類。那怎么做呢?通過WindowManager。
class NonUiThread extends Thread{@Overridepublic 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創(chuàng)建了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是一個(gè)WindowManagerGlobal實(shí)例,代碼在 frameworks/base/core/java/android/view/WindowManagerGlobal.java中,具體實(shí)現(xiàn)如下:
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 thingstry {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本身是在什么時(shí)候創(chuàng)建ViewRoot的呢?
既然是單線程模型,就要先找到這個(gè)UI線程實(shí)現(xiàn)類ActivityThread,看里面哪里addView了。沒錯(cuò),是在onResume里面,對(duì)應(yīng)ActivityThread就是handleResumeActivity這個(gè)方法:
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 considerationActivityClientRecord 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,并不會(huì)拋CalledFromWrongThreadException異常。但是一般情況下,我們不會(huì)在onCreate中做這樣的事情。
這就是Android為我們?cè)O(shè)計(jì)的單線程模型,核心就是一句話:Android UI操作并不是線程安全的,并且這些操作必須在UI線程執(zhí)行。但這一句話背后,卻隱藏著我們平時(shí)看不見的代碼實(shí)現(xiàn),只有搞懂這些,我們才能知其然知其所以然。
轉(zhuǎn)載于:https://www.cnblogs.com/hacjy/p/6894515.html
總結(jié)
以上是生活随笔為你收集整理的【转】Android子线程真的不能更新UI么的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 天籁为什么不保值?
- 下一篇: 道奇发动机护板有声音什么原因?