子线程也能更新UI?

在写一个小项目的时候遇到一个很奇怪的问题,在子线程里面有两个更新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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值