1. 程式人生 > >多執行緒學習之--真的不能在子執行緒裡更新UI嗎?

多執行緒學習之--真的不能在子執行緒裡更新UI嗎?

在我們學習多執行緒的路上,都會聽到這樣一句話:

不能在子執行緒裡更新UI,UI更新必須在UI執行緒中

why?為什麼不能在子執行緒中更新UI?如果在子執行緒中更新UI會怎樣?
為了模擬在子執行緒中更新UI的場景,簡單地寫了幾行程式碼:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (Button) findViewById(R.id.text);
        final
Thread thread = new Thread(new Runnable() { @Override public void run() { ((TextView)findViewById(R.id.sv_view)).setText("子執行緒"); } }); saButton.setOnClickListener(new View.OnClickListener() { @Override public void
onClick(View v) { thread.start(); } }); }

執行,“理所當然”地崩潰了。列印錯誤日誌如下:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
                                                                     at android.view
.ViewRootImpl.checkThread(ViewRootImpl.java:6357) at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:874)

崩潰的原因是:“Only the original thread that created a view hierarchy can touch its views.”意思是隻有建立這個View佈局層次的原始執行緒才可以改變這個View,看起來好像也並沒有解釋為什麼子執行緒中不能更新UI。
而我們能看到產生異常崩潰的程式碼在ViewRootImpl這個類的checkThread方法,所以我們找到這個類:
ViewRootImpl.java#checkThread

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
            "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

異常丟擲的條件是mThread != Thread.currentThread()那麼這個mThread在哪裡初始化的呢?接著看。

public ViewRootImpl(Context context, Display display) {
        mContext = context;
        mWindowSession = WindowManagerGlobal.getWindowSession();
        mDisplay = display;
        mBasePackageName = context.getBasePackageName();

        mDisplayAdjustments = display.getDisplayAdjustments();

        mThread = Thread.currentThread();//此處初始化
        ...
        }

在ViewRootImpl的構造方法裡可以看到mThread指向當前執行緒的引用,意思是隻要在子執行緒中建立ViewRootImpl的例項我們就可以避免拋異常了嗎?於是樓主嘗試在子執行緒中建立ViewRootImpl的例項可是發現並不能找到ViewRootImpl這個類。換個角度,如果不能在子執行緒更新UI,那主執行緒重新整理UI是不是也要例項化這個類呢?而我們啟動Activity繪製UI的方法在onResume方法裡,所以我們找到Activity的執行緒ActivityThread類。
ActivityThread.java#handleResumeActivity

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {
            ...
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;
            }
            ...
}

wm.addView(decor, l);是他進行的View的載入,我們去看看他的實現方法,在WindowManager的實現類WindowManagerImpl裡:

@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }

發現他是呼叫WindowManagerGlobal的方法實現的,最後我們找到了最終實現addView的方法:
WindowManagerGlobal.java#addView

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            // Start watching for system property changes.
            ...
            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;
        }
    }

果然在這裡,View的載入最後就是在這裡實現的,而ViewRootImpl的例項化也在這裡。所以如果我們在子執行緒中呼叫WindowManager的addView方法,是不是就可以成功更新UI呢?所以我修改了程式碼:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (Button) findViewById(R.id.text);
        final Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                TextView tx = new TextView(MainActivity.this);
                tx.setText("子執行緒");
                tx.setBackgroundColor(Color.WHITE);
                ViewManager viewManager = MainActivity.this.getWindowManager();
                WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
                        200, 200, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
                        WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE);
                viewManager.addView(view,layoutParams);
            }
        });
        ...
        }

執行,程式崩潰了,來看看錯誤日誌:

 java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()

錯誤原因是沒有啟動Looper。原來是因為在ViewRootImpl類裡新建了ViewRootHandler的例項mHandler,而mHandler要啟動Looper才能處理相關資訊。所以我們在程式碼里加入兩行:

...
public void run() {
                Looper.prepare();
                TextView tx = new TextView(MainActivity.this);
                tx.setText("子執行緒");
                ...
                windowManager.addView(tx, params);
                Looper.loop();
            }
            ...

再次執行,成功了!

這裡寫圖片描述

所以其實是可以在子執行緒中更新UI的,只要例項化ViewRootImpl。而為什麼Android設計只能在UI執行緒中更新UI呢?大概是因為如果子執行緒更新UI可能導致執行緒之間搶奪資源和死鎖等執行緒安全問題而不允許在子執行緒中更新UI。