一.问题
子线程更新UI(TextView.setText("子线程更新TextView")) 报错信息
com.example.rxjava20 E/AndroidRuntime: FATAL EXCEPTION: Thread-7
Process: com.example.rxjava20, PID: 584
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:9196)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1577)
at android.view.View.requestLayout(View.java:24885)
at android.view.View.requestLayout(View.java:24885)
at android.view.View.requestLayout(View.java:24885)
at android.view.View.requestLayout(View.java:24885)
at android.view.View.requestLayout(View.java:24885)
at android.view.View.requestLayout(View.java:24885)
at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3233)
at android.view.View.requestLayout(View.java:24885)
at android.widget.TextView.checkForRelayout(TextView.java:10102)
at android.widget.TextView.setText(TextView.java:6377)
at android.widget.TextView.setText(TextView.java:6180)
at android.widget.TextView.setText(TextView.java:6137)
at com.example.rxjava20.HandlerActivity$1.run(HandlerActivity.java:27)
at java.lang.Thread.run(Thread.java:764)
由报错信息,我们可以清楚的看到报错的日志是 ViewRootImpl类的checkThread方法。下面结合源码看一下。
二.源码分析
由于上面我们看到的报错日志,提示是ViewRootImpl类的checkThread方法抛出的错,那么我们就从这个方法开始我们的源码之旅。
ViewRootImpl类的checkThread方法源码
public final class ViewRootImpl implements ViewParent,View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
...
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
...
}
源码可知,此方法中就是校验当前线程是否是主线程,不是主线程报错。就是刚刚上面的错误日志。
那么这个方法在哪里调用的呢?点击该方法发现ViewRootImpl类中大概有13个方法调用此方法。
比如
requestFitSystemWindows()方法
@Override
public void requestFitSystemWindows() {
checkThread();
mApplyInsetsRequested = true;
scheduleTraversals();
}
比如
requestLayout()方法
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
比如
requestChildFocus()方法
@Override
public void requestChildFocus(View child, View focused) {
if (DEBUG_INPUT_RESIZE) {
Log.v(mTag, "Request child focus: focus now " + focused);
}
checkThread();
scheduleTraversals();
}
等等...
由于,调用checkThread方法在ViewRootImpl类中,调用方法众多。一时不清楚是哪个方法调用而引起的闪退。那么我们可以正着来。也就是说我们从TextView.setText()方法开始。
setText()方法部分源码
private void setText(CharSequence text, BufferType type,boolean notifyBefore, int oldlen) {
...
if (mLayout != null) {
checkForRelayout();
}
...
}
如果mLayout不为空,执行checkForRelayout()方法。那么mLayout什么时候赋值的呢?
//TextView类的全局变量
private Layout mLayout;
//TextView类的中有多处给mLayout变量赋值的地方 比如
startMarquee()方法:TextView跑马灯效果开始
stopMarquee()方法:TextView跑马灯效果结束
makeNewLayout()方法:而该方法在TextView类中又有好多类调用 比如
setGravity()方法:设置TextView属性
onMeasure()方法:TextView的测量方法
小结1
也就是说如果mLayout为空。则在子线程设置TextView.setText()就不会报错。
checkForRelayout()方法源码
private void checkForRelayout() {
// If we have a fixed width, we can just swap in a new text layout
// if the text height stays the same or if the view height is fixed.
if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
|| (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
&& (mHint == null || mHintLayout != null)
&& (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
// Static width, so try making a new text layout.
int oldht = mLayout.getHeight();
int want = mLayout.getWidth();
int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
/*
* No need to bring the text into view, since the size is not
* changing (unless we do the requestLayout(), in which case it
* will happen at measure).
*/
makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),false);
if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
// In a fixed-height view, so use our new text layout.
if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
&& mLayoutParams.height != LayoutParams.MATCH_PARENT) {
autoSizeText();
invalidate();
return;
}
// Dynamic height, but height has stayed the same,
// so use our new text layout.
if (mLayout.getHeight() == oldht
&& (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
autoSizeText();
invalidate();
return;
}
}
// We lose: the height has changed and we have a dynamic height.
// Request a new view layout using our new text layout.
requestLayout();
invalidate();
} else {
// Dynamic width, so we have no choice but to request a new
// view layout with a new text layout.
nullLayouts();
requestLayout();
invalidate();
}
}
也就是说,此方法两个分支都会走到View类的requestLayout()方法。但是第一个分支会有Return的情况。如果return就不会执行requestLayout方法。
最外层条件
(mLayoutParams.width != LayoutParams.WRAP_CONTENT
|| (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
&& (mHint == null || mHintLayout != null)
&& (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)
(宽度不是是wrap_content) 或者((mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth ) &&(mHint == null || mHintLayout != null)&&
mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)
其实这三个条件同时满足时就可以证明当前的View宽度是固定的并且宽度值是大于0的。
我们再看里面的条件
必须满足:mEllipsize != TextUtils.TruncateAt.MARQUEE 即当前TextView不是跑马灯的形式。满足这个条件的前提下有两个条件return。
<1> mLayoutParams.height != LayoutParams.WRAP_CONTENT&& mLayoutParams.height != LayoutParams.MATCH_PARENT 即TextView的高度值是固定的值。
<2> mLayout.getHeight() == oldht&& (mHintLayout == null || mHintLayout.getHeight() == oldht) 即当前TextView的高度等于修改UI之前的高度并且HintLayout等于空或者是HintLayout的高度也等于修改UI之前的高度。也就是说即便高度是不固定的,但是只要修改前后高度一致,那么一样return。
小结2
只要View的宽度和高度在修改前后保持不变。就不会执行requestLayout方法。则在子线程设置TextView.setText()就不会报错。
View类的requestLayout()方法源码
public void requestLayout() {
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
}
mParent是什么呢?
protected ViewParent mParent;
public interface ViewParent {
}
ViewParent是一个接口,ViewRootImpl类就是它的实现类
ViewRootImpl类 requestLayout方法源码
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
这个方法中,会执行checkThread()方法来校验线程。
三.代码验证
上述源码分析有两个添加可以让TextView在子线程更新UI。
1.setText时mLayout为空
代码
public class HandlerActivity extends AppCompatActivity {
private TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
mTextView = findViewById(R.id.activity_handler_textview);
new Thread(new Runnable() {
@Override
public void run() {
Layout mLayout = mTextView.getLayout();
Log.d("HandlerActivity", "mLayout----:" + mLayout);
mTextView.setText("子线程更新TextView");
Log.d("HandlerActivity", "当前线程----:" + Thread.currentThread().getName());
}
}).start();
}
}
结果
D/HandlerActivity: mLayout----:null
D/HandlerActivity: 当前线程----:Thread-7
原因
也就是说,在onCreate方法中,开启一个线程执行TextView.setText()方法是不会报错的。因为此时mLaout为空。按照上述源码分析此时不会执行checkForRelayout();方法。更没有机会执行View类的requestLayout()方法。也就没有机会走到ViewRootImpl类的requestLayout()方法。也就没有机会走到ViewRootImpl类的checkThread()方法。所以可以子线程更新UI。
2.上述源码中几个return的情况
改进一下代码使mLayout不为空
代码
public class HandlerActivity extends AppCompatActivity {
private TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
mTextView = findViewById(R.id.activity_handler_textview);
}
@Override
protected void onResume() {
super.onResume();
new Thread(new Runnable() {
@Override
public void run() {
SystemClock.sleep(5000);
Layout mLayout = mTextView.getLayout();
Log.d("HandlerActivity", "mLayout----:" + mLayout);
mTextView.setText("子线程更新TextView");
Log.d("HandlerActivity", "当前线程----:" + Thread.currentThread().getName());
}
}).start();
}
}
<TextView
android:id="@+id/activity_handler_textview"
android:layout_width="30dp"
android:layout_height="60dp"
android:text="发送"
android:gravity="center"
android:background="@color/colorPrimary"
android:textColor="#FFFFFF"
tools:ignore="MissingConstraints">
</TextView>
结果
D/HandlerActivity: mLayout----:android.text.BoringLayout@236cfe3
D/HandlerActivity: 当前线程----:Thread-7
此时mLayout不为空。但是由于TextView的宽高固定所以按照上述源码分析在TextView类的checkForRelayout()方法中直接return了。不会执行View类的requestLayout()方法。也就没有机会走到ViewRootImpl类的requestLayout()方法。也就没有机会走到ViewRootImpl类的checkThread()方法。所以可以子线程更新UI。
那么修改一下TextView的宽高呢
即只修改 TextView的高度改成
android:layout_height="wrap_content"
结果
D/HandlerActivity: mLayout----:android.text.BoringLayout@13fdde0
E/AndroidRuntime: FATAL EXCEPTION: Thread-7
Process: com.example.rxjava20, PID: 18258
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:9196)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1577)
at android.view.View.requestLayout(View.java:24885)
at android.view.View.requestLayout(View.java:24885)
at android.view.View.requestLayout(View.java:24885)
at android.view.View.requestLayout(View.java:24885)
at android.view.View.requestLayout(View.java:24885)
at android.view.View.requestLayout(View.java:24885)
at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3233)
at android.view.View.requestLayout(View.java:24885)
at android.widget.TextView.checkForRelayout(TextView.java:10090)
at android.widget.TextView.setText(TextView.java:6377)
at android.widget.TextView.setText(TextView.java:6180)
at android.widget.TextView.setText(TextView.java:6137)
at com.example.rxjava20.HandlerActivity$1.run(HandlerActivity.java:31)
at java.lang.Thread.run(Thread.java:764)
即,将高度改成wrap_content时,条件不满足执行了View类的requestLayout()方法。也会走到ViewRootImpl类的requestLayout()方法。也就会走到ViewRootImpl类的checkThread()方法。所以不可以子线程更新UI。
四.总结
<1> 子线程不能更新UI的原因是最终走到ViewRootImpl类的checkThread()方法。该方法中会校验线程是否是主线程。
<2> 但是在子线程中更新UI时,可能最终走不到ViewRootImpl类的checkThread()方法。也就可以在子线程更新UI了。比如源码分析的mLayout为空。或者TextView的宽高都是具体的固定值,修改前后宽高都不会变。
<3> 虽然某些情况下,可以在子线程更新UI。但是正常的情况下不应该在子线程更新UI。这里我只是在源码的层面上讲解一下原理。