许多人都是人云亦云,自己却从来没有实践过,然而纸上得来终觉浅,绝知此事要躬行。我们先来简单的看下这个代码:
public class MainActivity extends AppCompatActivity {
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.hello);
new Thread(new Runnable() {
@Override
public void run() {
textView.setText("Just do it !");
}
}).start();
}
}
大家觉得这个会崩溃掉吗? 马上动手打开AndroidStudio新建一个项目,打开手机模拟器,跑一遍就可以发现运行时没有任何问题的,也成功更新了UI,为啥它在子线程也能成功的更新UI呢,我们看一下textView.setText()的源码(按住Ctrl+鼠标左键点进去),可以发现:
// 第一层setText
public final void setText(CharSequence text) {
setText(text, mBufferType);
}
// 第二层setText
public void setText(CharSequence text, BufferType type) {
setText(text, type, true, 0);
if (mCharWrapper != null) {
mCharWrapper.mChars = null;
}
}
// 第三层setText
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
mTextSetFromXmlOrResourceId = false;
if (text == null) {
text = "";
}
...此处省略部分代码
if (mLayout != null) {
checkForRelayout();
}
... 此处省略部分代码
}
可以看到第三层的setText调用了一个checkForRelayout()方法,那么再点进去看一下:
private void checkForRelayout() {
... 此处省略
requestLayout();
invalidate();
... 此处省略
}
// 贴一下 requestLayout()的实现
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout(); // 调用这这个mParent的requestLayout
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
可以看到checkForRelayout调用的requestLayout,里面又调用了mParent的requestLayout, 那么这个mParent又是什么呢?通过定义可以发现它是一个ViewParent接口,那么是哪里赋值实例化的呢?
* @see #getParent()
*/
protected ViewParent mParent; // mParent的定义,其中ViewParent是一个接口
// 查找View.java源码可以发现,是通过assignParent赋值的
void assignParent(ViewParent parent) {
if (mParent == null) {
mParent = parent;
} else if (parent == null) {
mParent = null;
} else {
throw new RuntimeException("view " + this + " being added, but"
+ " it already has a parent");
}
}
// ViewRootImpl中的setView函数中调用了这个assignParent函数
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
... 此处省略
view.assignParent(this);
... 此处省略
}
所以View.java中的mParent.requestLayout() 调用的是ViewRootImpl.java中的requestLayout函数,如下:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
// 贴一下chekcThread这个函数的代码
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
可以看到,最终是调用了checkThread这个函数,如果当前线程不是主线程就会抛出一个异常,那为啥我最开头举的例子没有报错呢? 回到第三层的setText中会有一个 if 判断条件 , if(mParent != nul )才会执行requestLayout ,上面也分析了mParent的具体实现是ViewRootImpl, 而ViewRootImpl具体实例化是通过WindowManagerGlobal.addView()添加进去的,之前还有PhoneWindow以及DecorView的加载, 所以我们的setView是在ViewRootImpl还没有初始化完成的时候就已经调用了,所以不会调用到checkThread, 自然也不会报错。
假如我们在textView.setText() 的前面加一个延时函数,例如延时200ms ,Thread.sleep(200), 等待ViewRootImpl加载完毕,这个时候你就会发现,再次启动就会崩掉了。(没崩掉可能启动的太慢了,加长一些延时时间)