Android 单线程模型是这样描述的:

Android UI 操作并不是线程安全的,并且这些操作必须在 UI 线程执行。

如果在其它线程访问 UI 线程,也就是非主线程,Android 提供了以下的方式:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable, long)
  • Handler

为什么在子线程中就不能操作 UI 呢?

当 App 第一次启动的时候,Android 会同时启动一个对应的主线程(Main Thread),这个主线程就是 UI 线程,也就是 ActivityThread。UI 线程主要负责处理与 UI 相关的事件,如用户的按键点击、用户触摸屏幕以及屏幕绘图等。

系统不会为每个 UI 组件单独创建一个线程,在同一个进程里的 UI 组件都会在 UI 线程里实例化,系统对每一个组件的调用都从 UI 线程分发出去。

onKeyDown()

那为什么选择一个主线程干这些活呢?换个说法,Android 为什么使用单线程模型,它有什么好处?

要明白这点,就得先看下单线程化的事件队列模型是怎么定义的:

采用一个专门的线程从队列中抽取事件,并把他们转发给应用程序定义的事件处理器。

这看起来就是 Android 的消息队列、Looper 和 Handler嘛

其实现代 GUI 框架就是使用了类似这样的模型:模型创建一个专门的线程,事件派发线程来处理 GUI 事件。单线程化也不单单存在 Android 中,Qt、XWindows 等都是单线程化。当然,也有人试图用多线程的 GUI,最终由于竞争条件和死锁导致的稳定性问题等,又回到单线程化的事件队列模型老路上来。单线程化的 GUI 框架通过限制来达到线程安全:所有 GUI 中的对象,包括可视组件和数据模型,都只能被事件线程访问。

这就解释了Android为什么使用单线程模型。

那 Android 的 UI 操作并不是线程安全的又是怎么回事?

invalidate 和 postInvalidate。前者在UI线程中使用,后者在非 UI 线程中使用。换句话说,Android 的 UI 操作不是线程安全可以表述为 invalidateinvalidate 在子线程中刷新界面,同时 UI 线程也在用 invalidate 刷新界面,这样会不会导致界面的刷新不能同步?既然刷新不同步,那么 invalidate 就不能在子线程中使用。这就是 invalidatepostInvalidate

看看源码是怎么实现的:

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,那会出现什么情况呢?

CalledFromWrongThreadException

android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

相信很多人遇到这个异常后,就会通过前面的四种方式中的其中一种解决:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable, long)
  • Handler

说到这儿还是没触发到根本,不禁会问:为什么会出现这个异常呢,这个异常在哪里抛出来的呢?

在 framework/base/core/java/android/view/ViewRootImpl.java 类中,有这么一个方法:

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

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()

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() 创建了 ViewRootWindowManagerImpl.java 中的 addView()

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

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

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。

延伸阅读

ViewRootActivityThread,看里面哪里 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(也就是在 onresume() 之前),并不会抛 CalledFromWrongThreadException 异常。但是一般情况下,我们不会在 onCreate()

这就是Android为我们设计的单线程模型,核心就是一句话:

Android UI操作并不是线程安全的,并且这些操作必须在 UI 线程执行。

但这一句话背后,却隐藏着我们平时看不见的代码实现,只有搞懂这些,我们才能知其然知其所以然。