Android - 子线程为什么不能更新UI

614 阅读2分钟

报错信息:

Only the original thread that created a view hierarchy can touch its views.

报错信息是在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负责DecorView的测量布局绘制的调用,调用DecorView的测量布局绘制后会遍历控件树的测量布局绘制,控件树里面的任何一个小View刷新,最终都会调用到ViewRootImpl的requestLayout方法,最终遍历整个控件树刷新

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

scheduleTraversals()函数里是对View进行绘制操作,而在绘制之前都会检查当前线程是否为主线程mThread,如果不是主线程,就抛出异常;这样做法就限制了开发者在子线程中更新UI的操作。

为什么最开始的在onCreate()里子线程对UI的操作没有报错呢,可以设想一下是因为ViewRootImp此时还没有创建,还未进行当前线程的判断。

ViewRootImp 是何时创建的?

从Activity启动过程中寻找,通过分析ActivityThread.java源码,并找到handleResumeActivity方法:

@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
        String reason) {
        
。。。
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
 。。。
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 (r.mPreserveWindow) {
        a.mWindowAdded = true;
        r.mPreserveWindow = false;
        ViewRootImpl impl = decor.getViewRootImpl();
        if (impl != null) {
            impl.notifyChildRebuilt();
        }
    }
    if (a.mVisibleFromClient) {
        if (!a.mWindowAdded) {
            a.mWindowAdded = true;
            wm.addView(decor, l);
        } else {
            a.onWindowAttributesChanged(l);
        }
    }

。。。
}

可以看到是在先调用performResumeActivity之后在赋值的

public final ActivityClientRecord performResumeActivity(IBinder token, boolean clearHide) { 
    if (r != null && !r.activity.mFinished) {
        r.activity.performResume();
        ... 
    }
}

最后发现会回调生命周期的onResume方法。

通过分析可得知,ViewRootImpl对象是在onResume方法回调之后才创建,那么就说明了为什么在生命周期的onCreate方法里,甚至是onResume方法里都可以实现子线程更新UI,因为此时还没有创建ViewRootImpl对象,并不会进行是否为主线程的判断

总结

必须要在主线程更新UI,实际是为了提高界面的效率和安全性,带来更好的流畅性;

你反推一下,假如允许多线程更新UI,但是访问UI是没有加锁的,一旦多线程抢占了资源,那么界面将会乱套更新了,体验效果就不言而喻了;

所以在Android中规定必须在主线程更新UI