上篇文章:Android子线程真的不能刷新UI吗?(一)复现异常,复现了子线程修改UI的异常。这篇文章,详细跟踪setText方法,是怎么导致抛出异常的。
会抛异常
24698-24800/com.zj.androidthreaddemo E/AndroidRuntime: FATAL EXCEPTION: Thread-3
Process: com.zj.androidthreaddemo, PID: 24698
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8632)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1380)
at android.view.View.requestLayout(View.java:23377)
at android.view.View.requestLayout(View.java:23377)
at android.view.View.requestLayout(View.java:23377)
at android.view.View.requestLayout(View.java:23377)
at android.view.View.requestLayout(View.java:23377)
at android.view.View.requestLayout(View.java:23377)
at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3239)
at android.view.View.requestLayout(View.java:23377)
at android.widget.TextView.checkForRelayout(TextView.java:9221)
at android.widget.TextView.setText(TextView.java:5935)
at android.widget.TextView.setText(TextView.java:5776)
at android.widget.TextView.setText(TextView.java:5733)
at com.zj.androidthreaddemo.MainActivity$1.run(MainActivity.java:23)
at java.lang.Thread.run(Thread.java:784)
TextView.setText()源码:
public final void setText(CharSequence text) {
//调用重载
setText(text, mBufferType);
}
public void setText(CharSequence text, BufferType type) {
//再次调用重载
setText(text, type, true, 0);
if (mCharWrapper != null) {
mCharWrapper.mChars = null;
}
}
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
if (mLayout != null) {
//在这里调用了checkForRelayout
checkForRelayout();
}
...
}
android 6.0 /frameworks/base/core/java/android/widget/TextView.java
private void checkForRelayout() {
if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT ||
(mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) &&
(mHint == null || mHintLayout != null) &&
(mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
......
} else {
nullLayouts();
//在这里调用了requestLayout()
requestLayout();
invalidate();
}
}
调用View.requestLayout()
Android子线程与更新UI问题的深入讲解
Android 6.0 /frameworks/base/core/java/android/view/View.java
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
// AttachInfo#mViewRequestingLayout用来追踪最开始发起requestLayout的View。
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
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();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
View.requestLayout会调用 mParent.requestLayout();
,也就是执行父类的requestLayout方法。
有重写,但是还是调用父类ViewGroup的requestLayout,最终调用的还是View.requestLayout会。
public void requestLayout() {
this.markHierarchyDirty();
super.requestLayout();
}
是DecorView。
Android6.0上,DecorView是PhoneWindow的内部类,源码路径:/frameworks/base/core/java/com.android.internal.policy.PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
......
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
}
......
}
Android DecorView 一窥全貌(上)
没有。DecorView继承自FrameLayout,FrameLayout继承自ViewGroup, ViewGroup继承自View,所以DecorView还是调用的View的requestLayout方法。核心代码仍然是:mParent.requestLayout();
ViewRootImpl
核心代码如下。ViewRootImpl的setView方法,通过view.assignParent(this);
,把自身传递给DecorView,作为DecorView的mParent。 这个view就是DecorView
//ViewRootImpl构造方法
public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
boolean useSfChoreographer) {
...
//持有主线程,即创建ViewRootImpl的线程
mThread = Thread.currentThread();
//初始化AttachInfo,内部传递持有了ViewRootImpl,后续的代码里,它将被传递给View,所以可以通过View获取到mAttachInfo,进而获取到ViewRootImpl。
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,context);
}
ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
if (mView == null) {
//持有DecorView
mView = view;
...
//AttachInfo持有DecorView
mAttachInfo.mRootView = view;
//调用requestLayout
requestLayout();
...
//通过WindowSession来完成window最终的添加,该过程mWindowSession类型是IWindowSession,它是一个Binder对象,真正的实现类是Session,所以这其实是一次IPC的过程,远程的调用了Session的addToDisPlay方法。
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId,
mInsetsController.getRequestedVisibility(), inputChannel, mTempInsets,
mTempControls);
...
//将自己传递给View的mParent变量,如此调用View相关的invalidate方法,就直接调用到了ViewRootImpl
view.assignParent(this);
...
}
}
//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");
}
}
所以requestLayout()方法,最终调用了ViewRootImpl.requestLayout()方法。
DecorView的mParent具体是怎么被赋值的,过程很长,参考:关于View中mParent的来龙去脉
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
调用了checkThread()方法
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
checkThread方法就是上面异常的抛出位置。