今天有同事问我,在Android中子线程是不是不可以直接操作View组件
我毫不犹豫的回答“是”
接着他让我看一段代码 大致如下
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(R.layout.activity_scroll_textview);
tv = findViewById(R.id.tv);
new Thread() {
@Override
public void run() {
super.run();
tv.setText("xsxxxx");
}
}.start();
同事说 这段代码跑起来不会崩溃,我好奇的自己写了一段试了一下,果然没崩溃
于是仔细研究了一下子线程更新UI的崩溃原理
在常见情况下,如果我们在子线程中更新UI就会抛出如下异常
android.view.ViewRootImpl$CalledFromWrongThreadException Only the original thread that created a view hierarchy can touch its views.
ViewRootImpl.java 7979
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7979)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1253)
at android.view.View.requestLayout(View.java:23305)
at android.view.View.requestLayout(View.java:23305)
at android.view.View.requestLayout(View.java:23305)
at android.view.View.requestLayout(View.java:23305)
at android.view.View.requestLayout(View.java:23305)
at android.widget.ScrollView.requestLayout(ScrollView.java:1603)
at android.view.View.requestLayout(View.java:23305)
at android.view.View.requestLayout(View.java:23305)
at android.widget.TextView.checkForRelayout(TextView.java:8957)
at android.widget.TextView.setText(TextView.java:5754)
at android.widget.TextView.setText(TextView.java:5588)
at android.widget.TextView.setText(TextView.java:5545)
at com.base.collections.extensions.sample.ui.TestChainActivity$1.run(TestChainActivity.java:31)
可以看出这是View 主动抛出了一个自定义异常
我们在源码里面全局查找这段异常
发现 其实是在 ViewRootImpl 类的checkThread()方法中 检测了当前操作方法的Thread是否与 ViewRootImpl创建时所在的线程是同一个线程
ViewRootImpl在这里是由框架创建的,因此这里进行的就是主线程的检测
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
checkThread调用的地方如图
看这个崩溃堆栈 ,可以发现 在setText时,触发了checkForRelayout 方法
最终调用ViewRootImpl的 requestLayout方法,在requestLayout方法中 使用 checkThread()最终抛出了异常
我们看一下setText源码
源码很长 这里精简了一下
private void setText(CharSequence text, BufferType type,boolean notifyBefore, int oldlen) {
if (text == null) {
text = "";
}
mBufferType = type;
mText = text;
...............................
if (mLayout != null) {
checkForRelayout();
}
sendOnTextChanged(text, 0, oldlen, textLength);
onTextChanged(text, 0, oldlen, textLength);
...............................
if (needEditableForNotification) {
sendAfterTextChanged((Editable) text);
}
}
checkForRelayoutde 判断条件 mLayout 是否为NULL
mLayout 是在 onMeasure 方法中获取的
所以我们得到了第一个可能不崩溃的原因,TextView 还没有被加入到ViewTree中去
再看checkForRelayout方法
private void checkForRelayout() {
if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT ||
(mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) &&
(mHint == null || mHintLayout != null) &&
(mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
int oldht = mLayout.getHeight();
int want = mLayout.getWidth();
int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
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) {
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)) {
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();
}
}
大概的解释下,如果TextView 不是固定的宽高,那么根据输入的字符大小数量,TextView 需要动态的计算宽高 否则只要直接绘制就可以了
这时候我们得到了第二个可能不崩溃的原因
如果一个TextView 设置的是固定宽高 这个时候在设置Text的时候 就不需要requestLayout,所以就不会触发checkThread()方法
继续往下看
虽然没有调用requestLayout,但是还是调用了 invalidate();方法,在文章的开头,我们观察checkThread()方法时,提到ViewRootImpl调用checkThread的场景方法,其中有一个是 invalidateChildInParent 这样的话按照我的理解,即使没有在requestLayout的时候崩溃,那么总该在invalidate的时候蹦了吧。真的是这样吗?继续来看源码
from View.class
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) {
。。。。。。。。。。。。。
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
。。。。。。。。。。。。。
}
}
最终走到了ViewParent的invalidateChild 方法中
from ViewGroup.class
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;
}
.................................................................
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
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);
}
}
从这段代码里面我们可以看到
其实在invalidateChild的过程中是分两种情况的,开启硬件加速和不开启硬件加速
先看不开启硬件加速的场景,主要就是走了 parent.invalidateChildInParent ,这是一个do while 循环
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
.............
return mParent;
}
return null;
}
invalidateChildInParent 方法会将自己的mParent 返回 并进入下一次循环,这样的最终就会走到ViewRootImpl的invalidateChildInParent ,然后就是checkThread 再然后就是抛出异常
于是我们将demo中的硬件加速关闭,来测试这种场景,果然抛出了异常,证明在非硬件加速情况下这种流程是对的
至于开启了硬件加速之后不抛出异常这种场景,由于对硬件加速原理了解甚微,这里就不多做研究了
总结:
在子线程中给TextView setText 不会抛出异常的两个场景
1:TextView 还没来得及加入到ViewTree中
2:TextView已经被加入了ViewTree,但是被设置了固定宽高,且开启了硬件加速
子线程操作View 确实不一定导致Crash,那是因为刚好满足一定的条件并没有触发checkThread机制,但这并不代表我们在开发过程中可以这么写,谨记