Android只在UI主线程修改UI,是个谎言吗? 为什么这段代码能完美运行?

我今天尝试了下这个代码,竟然没报错。 日志也显示这是两个线程。 那么其实在非UI线程也可以更新UI是吗? (我其实是预期crash的, 没想到连个日志…
关注者
221
被浏览
62,695

23 个回答

这是因为你的Thread执行的时候,ViewRootImpl还没有对view tree的根节点DecorView执行performTraversals,view tree里的所有View都没有被赋值mAttachInfo。

在onCreate完成时,Activity并没有完成初始化view tree。view tree的初始化是从ViewRootImpl执行performTraversals开始,这个过程会对view tree进行从根节点DecorView开始的遍历,对所有视图完成初始化,初始化包括视图的大小布局,以及AttachInfo,ViewParent等域的初始化。

执行ImageView.setImageResource,调用的过程是

ImageView.setImageResource 
-> View.invalidate 
-> View.invalidateInternal 
-> ViewGroup.invalidateChild
-> ViewParent.invalidateChildInParent //这里会不断Loop去取上一个结点的mParent
-> ViewRootImpl.invalidateChildInParent //DecorView的mParent是ViewRootImpl
-> ViewRootImpl.checkThread //在这里执行checkThread,如果非UI线程则抛出异常

但是在Thread执行setImageResource时,此时Activity还在初始化,ViewRoot没有初始化整个view tree,ImageView的mAttachInfo是空的(mAttachInfo包含了Window的token等Binder)。而View.invalidateInternal调用ViewGroup.invalidateChild要判断是否存在ViewParent和AttachInfo:

final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
    //....
    p.invalidateChild(this, damage);
}

也就是说,此时因为不存在ViewParent,invalidate的过程中止而没有完全执行,也即没有发生checkThread。

我突然有一种醍醐灌顶的感觉。题主的问题,高票答案可以很好的解释,但是问题可以稍微展开一下,而关键就是ViewRootImpl的checkThread方法。checkThead的源码是这样的:

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

可以看到,它检查的并不是当前线程是否是UI线程,而是当前线程是否是操作线程。这个操作线程就是创建ViewRootImpl对象的线程:

public ViewRootImpl(Context context, Display display) {
   ...
   mThread = Thread.currentThread();
   ...
}

所以,只要在创建Window/View/ViewRootView的线程中更新UI,就是合法的,就可以更新成功。

平时我们都在UI线程中创建Window/View/ViewRootView,所以只能在主线程中更新UI,但是如果我们从头到尾都是在子线程中操作,那就是没问题的。下面这段代码,就可以完美运行:

new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                MyDialog dialog = new MyDialog(MainActivity.this);
                dialog.show();
                Looper.loop();
            }
        }).start();

如上,我们成功在子线程中创建并显示了UI元素。

所以,Android要求我们在主线程中更新UI,只是一种建议,是以规范的形式解决多线程同步问题。其实子线程,完全有操作UI的能力。