子线程更新UI问题

一.问题

 

子线程更新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。这里我只是在源码的层面上讲解一下原理。

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值