Android只在UI主线程修改UI,是个谎言吗? 为什么这段代码能完美运行?
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的能力。