Android子執行緒真的不能更新UI麼

LeoLiang發表於2016-01-08

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執行緒執行。但這一句話背後,卻隱藏著我們平時看不見的程式碼實現,只有搞懂這些,我們才能知其然知其所以然。

相關文章