在写一个小项目的时候遇到一个很奇怪的问题,在子线程里面有两个更新UI操作,但是前面一个不报错,后面一个报错了。
我确实没有乱说,代码(完整代码,后面log对应行数都是准的)如下:
package com.demo.text_demo;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import com.demo.R;
import java.io.PrintWriter;
import java.io.StringWriter;
public class TextDemoActivity extends AppCompatActivity implements View.OnClickListener {
private TextView tv_refresh_text;
private TextView tv_refresh_text_too;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_text_demo);
tv_refresh_text = findViewById(R.id.tv_refresh_text);
tv_refresh_text_too= findViewById(R.id.tv_refresh_text_too);
tv_refresh_text.setOnClickListener(this);
}
@Override
public void onClick(View v) {
logUtils("onClick: " + Thread.currentThread().getName());
Thread newThread = new Thread(new Runnable() {
@Override
public void run() {
logUtils("newThread: " + Thread.currentThread().getName());
try {
tv_refresh_text.setText("我更新了自己");
tv_refresh_text_too.setText("我也要更新自己123456789123456789我也要更新自己123456789123456789");
} catch (Exception e) {
logException(e);
}
}
});
newThread.start();
}
private void logUtils(String msg) {
Log.e("textDemo", msg);
}
private void logException(Exception e) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
logUtils(sw.toString());
}
}
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".text_demo.TextDemoActivity">
<TextView
android:id="@+id/tv_refresh_text"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_30"
android:text="文本1"
android:gravity="center"
android:textColor="@color/black"
android:textSize="@dimen/sp_16" />
<TextView
android:id="@+id/tv_refresh_text_too"
android:layout_width="match_parent"
android:layout_marginTop="10dp"
android:layout_height="wrap_content"
android:text="文本2"
android:textColor="@color/black"
android:textSize="@dimen/sp_16" />
</LinearLayout>
页面很简单,两行文本,点击上面的TextView 会在一个子线程中对两个TextView进行setText()操作。
点击以后报错如下
2021-01-25 15:20:26.024 E/textDemo: onClick: main
2021-01-25 15:20:26.025 E/textDemo: newThread: Thread-3
2021-01-25 15:20:26.028 E/textDemo: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8913)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1557)
at android.view.View.requestLayout(View.java:24694)
at android.view.View.requestLayout(View.java:24694)
at android.view.View.requestLayout(View.java:24694)
at android.view.View.requestLayout(View.java:24694)
at android.view.View.requestLayout(View.java:24694)
at android.view.View.requestLayout(View.java:24694)
at android.view.View.requestLayout(View.java:24694)
at android.widget.TextView.checkForRelayout(TextView.java:9750)
at android.widget.TextView.setText(TextView.java:6314)
at android.widget.TextView.setText(TextView.java:6142)
at android.widget.TextView.setText(TextView.java:6094)
at com.sz.edit_sn.text_demo.TextDemoActivity$1.run(TextDemoActivity.java:42)
at java.lang.Thread.run(Thread.java:929)
报错的是后面的 tv_refresh_text_too.setText("我也要更新自己123456789123456789我也要更新自己123456789123456789"); 前面的 tv_refresh_text.setText("我更新了自己")没有报错,手机界面中UI都更新成功了。
@Override
public void onClick(View v) {
logUtils("onClick: " + Thread.currentThread().getName());
Thread newThread = new Thread(new Runnable() {
@Override
public void run() {
logUtils("newThread: " + Thread.currentThread().getName());
try {
tv_refresh_text.setText("我更新了自己"); ← 这一行没报错并更新UI成功
tv_refresh_text_too.setText("我也要更新自己123456789123456789我也要更新自己123456789123456789"); ← 这一行报错
} catch (Exception e) {
logException(e);
}
}
});
newThread.start();
}
确实是子线程两个更新UI操作,前一个没报错,后一个报错了。。。
从日志我们也可以看出更新TextView操作是在newThread: Thread-3中进行的
子线程不是不能进行UI更新操作么??????
我们先来看 tv_refresh_text.setText() 为啥没有报错
Read the fucking source code ! (PS: 可以直接跳到最后看总结)
tv_refresh_text.setText("我更新了自己");
👇
👇 调用
👇
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
public final void setText(CharSequence text) {
setText(text, mBufferType);
}
👇
public void setText(CharSequence text, BufferType type) {
setText(text, type, true, 0);
}
👇 ✍最终调用这个方法
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
mTextSetFromXmlOrResourceId = false;
if (text == null) {
text = "";
}
⚠️ Spanned 我们没有设置,感兴趣可以看看SpannableString
if (text instanceof Spanned && ... ) {
...
...
⚠️ 如果您我们设置了Spanner这里会改变 mEllipsize 的值
但是我们并没有。
❗ 注意 mEllipsize 这个变量后面会用到很重要 ❗
setEllipsize(TextUtils.TruncateAt.MARQUEE);
}
...省略代码...
if (mLayout != null) {
checkForRelayout();✍ 我们接着去看checkForRelayout()
}
sendOnTextChanged(text, 0, oldlen, textLength);
onTextChanged(text, 0, oldlen, textLength);
......
}
👇
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);
✍还记得上面的 mEllipsize 么,我们没有设置
所以mEllipsize != TextUtils.TruncateAt.MARQUEE为true
if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
// In a fixed-height view, so use our new text layout.
✍这里的说高度不是WRAP_CONTENT也不是MATCH_PARENT
就是一个写死的固定高度比如写死60dp
tv_refresh_text.setText()就是进入这个判断条件。
if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
&& mLayoutParams.height != LayoutParams.MATCH_PARENT) {
autoSizeText();✍ autoSizeTextType Android8.0新特性
invalidate();
return;
}
// Dynamic height, but height has stayed the same,
// so use our new text layout.
✍不满足上一个判断条件,但是新的TextView的高度和原来的高度没有改变
就走这个判断条件
if (mLayout.getHeight() == oldht
&& (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
autoSizeText();✍ autoSizeTextType Android8.0新特性
invalidate();
return;
}
✍ 上面两个判断条件都会调用 invalidate();
}
// We lose: the height has changed and we have a dynamic height.
// Request a new view layout using our new text layout.
requestLayout();✍正常刷新UI就走这里的流程,tv_refresh_text_too 就是走这里,
先不看,后面再分析
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 # invalidate() 方法 :
public class View implements ... {
public void invalidate() {
invalidate(true);
} 👇
👇
public void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
} 👇
👇
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
if (mGhostView != null) {
✍蒙层布局,我们没有,不用管
mGhostView.invalidate(true);
return;
}
if (skipInvalidate()) {
✍不可见或者透明直接返回
return;
}
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
if (fullInvalidate) {
mLastIsOpaque = isOpaque();
mPrivateFlags &= ~PFLAG_DRAWN;
}
mPrivateFlags |= PFLAG_DIRTY;
if (invalidateCache) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
// Propagate the damage rectangle to the parent view.
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;✍ p = mParent
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
✍即mParent.invalidateChild,这里TextView的父布局为Linearlayout,
Linearlayout继承于ViewGroup,所以我们到ViewGroup里去看invalidateChild方法
p.invalidateChild(this, damage);
}
// Damage the entire projection receiver, if necessary.
if (mBackground != null && mBackground.isProjected()) {
final View receiver = getProjectionReceiver();
if (receiver != null) {
receiver.damageInParent();
}
}
}
}
}
ViewGroup # invalidateChild:
@UiThread
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
@Deprecated
@Override
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
✍是否启用硬件加速
if (attachInfo != null && attachInfo.mHardwareAccelerated) {
// HW accelerated fast path
onDescendantInvalidated(child, child);
return;
}
✍现在这个this是LinearLayout继承于ViewGroup
ViewParent parent = this;
if (attachInfo != null) {
...省略代码...
do {
View view = null;
if (parent instanceof View) {✍ parent肯定属于View
view = (View) parent;
}
......
✍ parent = this即调用下面的invalidateChildInParent
parent = parent.invalidateChildInParent(location, dirty);
if (view != null) {
// Account for transform on current parent
Matrix m = view.getMatrix();
if (!m.isIdentity()) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
m.mapRect(boundingRect);
dirty.set((int) Math.floor(boundingRect.left),
(int) Math.floor(boundingRect.top),
(int) Math.ceil(boundingRect.right),
(int) Math.ceil(boundingRect.bottom));
}
}
} while (parent != null);
}
}
@Deprecated
@Override
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
// either DRAWN, or DRAWING_CACHE_VALID
if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE))
!= FLAG_OPTIMIZE_INVALIDATE) {
dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
location[CHILD_TOP_INDEX] - mScrollY);
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
}
final int left = mLeft;
final int top = mTop;
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
dirty.setEmpty();
}
}
location[CHILD_LEFT_INDEX] = left;
location[CHILD_TOP_INDEX] = top;
} else {
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
dirty.set(0, 0, mRight - mLeft, mBottom - mTop);
} else {
// in case the dirty rect extends outside the bounds of this container
dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
}
location[CHILD_LEFT_INDEX] = mLeft;
location[CHILD_TOP_INDEX] = mTop;
mPrivateFlags &= ~PFLAG_DRAWN;
}
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
if (mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;
}
✍注意这里返回的是 mParent ,这个是在View中的变量,这样parent = mParent
就走到了再上一层,最终会调用parent = ViewRootImpl
return mParent;
}
return null;
}
}
最终调用 ViewRootImpl 的 invalidateChildInParent( , ) 方法
public final class ViewRootImpl implements ViewParent,...{
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread(); ✍"检查当前线程是否为主线程"
if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
if (dirty == null) {
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}
if (mCurScrollY != 0 || mTranslator != null) {
mTempRect.set(dirty);
dirty = mTempRect;
if (mCurScrollY != 0) {
dirty.offset(0, -mCurScrollY);
}
if (mTranslator != null) {
mTranslator.translateRectInAppWindowToScreen(dirty);
}
if (mAttachInfo.mScalingRequired) {
dirty.inset(-1, -1);
}
}
invalidateRectOnScreen(dirty);
return null;
}
void checkThread() {
✍"Only the original thread that created a view hierarchy can touch its views."
"一个熟悉的异常错误"
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
}
这不是要检查是否主线程会报错的么?!!!
我们回到 ViewGroup#invalidateChild 方法
@UiThread
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
@Deprecated
@Override
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
✍是否启用硬件加速
if (attachInfo != null && attachInfo.mHardwareAccelerated) {
// HW accelerated fast path
onDescendantInvalidated(child, child);
return;
}
.....后面代码省略.....
}
}
是的走了硬件加速,就return了,不走后面了。
接着看 ViewGroup#onDescendantInvalidated
@Override
@CallSuper
public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
/*
* HW-only, Rect-ignoring damage codepath
*
* We don't deal with rectangles here, since RenderThread native code computes damage for
* everything drawn by HWUI (and SW layer / drawing cache doesn't keep track of damage area)
*/
.....省略代码......
if (mParent != null) {
✍调用父布局的onDescendantInvalidated,就是这个方法本身,直到ViewRootImpl
mParent.onDescendantInvalidated(this, target);
}
}
接着看 ViewRootImpl#onDescendantInvalidated 方法
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
@Override
public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {
if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
mIsAnimating = true;
}
invalidate();
}
void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
scheduleTraversals();
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
✍就是执行 mTraversalRunnable,即doTraversal()方法,后面就是熟悉的绘制加载UI了
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
}
到这里我们看到没有checkThread(),所以就不会报非主线程更新UI错误,而是直接更新UI了,所以tv_refresh_text.setText("我更新了自己"); 这个更新操作就没有报错。
感觉怎么有点不对劲呢,开了硬件加速,就不检查线程,就不会报错,那我只要开启了硬件加速,所有UI都可以在子线程中更新了?
不是这样的,同样的情况下tv_refresh_result.setText("我也更新了自己") 不就报错了么。
再回到前面的
tv_refresh_text.setText("我更新了自己");
👇
👇 调用
👇
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
public final void setText(CharSequence text) {
setText(text, mBufferType);
}
👇
public void setText(CharSequence text, BufferType type) {
setText(text, type, true, 0);
}
👇 ✍最终调用这个方法
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
m
...省略代码...
if (mLayout != null) {
checkForRelayout();✍ 我们接着去看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)) {
if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
// In a fixed-height view, so use our new text layout.
✍条件1 height不为WRAP_CONTENT也不为MATCH_PARENT
if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
&& mLayoutParams.height != LayoutParams.MATCH_PARENT) {
...
invalidate();
return;
}
✍条件2 height没有发生改变和之前相同,
if (mLayout.getHeight() == oldht
&& (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
...
invalidate();
return;
}
}
requestLayout();✍v_refresh_text_too 就是走这里
invalidate();
} else {
// Dynamic width, so we have no choice but to request a new
// view layout with a new text layout.
nullLayouts();
requestLayout();
invalidate();
}
}
}
只有满足条件1或者条件2的情况下,子线程更新UI不会报错。
否则就像 tv_refresh_text_too.setText()就会调用其父类View的 requestLayout()
View:
@CallSuper
public void requestLayout() {
......
if (mParent != null && !mParent.isLayoutRequested()) {
✍ViewGroup没有重载这个方法,所以还是View自己的这个方法
直到最终调用ViewRootImp的requestLayout()方法
mParent.requestLayout();
}
......
}
ViewRootImp:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();✍先检查线程
mLayoutRequested = true;
scheduleTraversals();✍ 这个熟悉的方法
}
}
所以同样的硬件加速,tv_refresh_text_too.setText()就报错了。
另外1️⃣:上面条件1比较好理解,条件2到底是啥意思呢?
就比如 tv_refresh_text_too.setText("我也要更新自己123456789123456789我也要更新自己123456789123456789"); 如果改成tv_refresh_text_too.setText("123");
你会发现在运行demo 他就不会报错,因为它setText("123")不会改变它之前的高度。
而 tv_refresh_text_too.setText("我也要更新自己123456789123456789我也要更新自己123456789123456789");一行放不下了会变成两行展示,这时候就不满足条件2高度不变的限制,所以会报错!
另外2️⃣:如果你关闭了硬件加速,在Mainfest文件Activity的声明中加上 android:hardwareAccelerated="false",你再运行就会发现tv_refresh_text.setText("我更新了自己");也会报错。
总结:
写了一大篇的废话其实就是
1 开启了硬件加速
2 在宽度固定,高度写死多少dp(条件1) 或者 新的UI内容不会改变之前UI的高度(条件2)
这种情况下,子线程更新UI不会报错!
================================================================
子线程可以更新UI的其它骚操作!
操作1:先requestLayout 再在子线程设置UI就不会走checkThread
val textView = findViewById<TextView>(R.id.tv_update)
textView.requestLayout()
thread {
textView.text = "haha"
}
SurfaceView 也可以子线程更新UI