如何做到在子线程更新 UI?

Posted 一叶飘舟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何做到在子线程更新 UI?相关的知识,希望对你有一定的参考价值。

一般来讲,子线程是不能更新 UI 的,如果在子线程更新 UI,会报错。

但在某种情况下直接开启线程更新 UI 是不会报错的。

比如,在 onCreate 方法中,直接开启子线程更新 UI,这样是不会报错的。

override fun onCreate(savedInstanceState: Bundle?) 
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    textView = findViewById(R.id.tv)
    thread 
        textView.text = "哈哈哈哈"
    

如果在子线程中假如延时,比如加一行Thread.sleep(2000)就会报错。

这是为什么呢?

有人会说,因为睡眠了 2 s,因此 UI 的线程检查机制就已经建立了,所以在子线程更新就会报错。

更新 UI 的线程检测是什么时候开始的

子线程更新的错误定位是 ViewRootImpl 中的 checkThread 方法和 requestLayout 方法。

// ViewRootImpl 下 checkThread 的源码
void checkThread() 
    if (mThread != Thread.currentThread()) 
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    


//ViewRootImpl 下 requestLayout 的源码
@Override
public void requestLayout() 
    if (!mHandlingLayoutInLayoutRequest) 
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    

从源码中可以看出,checkThread 就是进行线程检测的方法,而调用是在 requestLayout 方法中。

要想知道 requestLayout 是何时调用的,就要知道 ViewRootImpl 是如何创建的?

因为在 onCreate 中创建子线程访问 UI,是不报错的,这也说明在 onCreate中,ViewRootImpl 还未创建。

ViewRootImpl 是何时创建的。

在 ActivityThread 的 handleResumeActivity 中调用了 performResumeActivity 进行 onResume 的回调。

@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,String reason) 
    // 代码省略...

    // performResumeActivity 最终会调用 Activity 的 onResume方法
    // 调用链如下: 会调用 r.activity.performResume。
    // performResumeActivity -> r.activity.performResume -> Instrumentation.callActivityOnResume(this) -> activity.onResume();
    final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);

    // 代码省略...

    if (r.window == null && !a.mFinished && willBeVisible) 
        r.activity.mVisibleFromServer = true;
        mNumVisibleActivities++;
        if (r.activity.mVisibleFromClient) 
            // 注意这句,让 activity 显示,并且会最终创建 ViewRootImpl
            r.activity.makeVisible();
        
    

进一步跟进 activity.makeVisible()

void makeVisible() 
    if (!mWindowAdded) 
        ViewManager wm = getWindowManager();
        // 往 WindowManager 中添加 DecorView
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    
    mDecor.setVisibility(View.VISIBLE);

WindowManager 是一个接口,它的实现类是 WindowManagerImpl

// WindowManagerImpl 的 addView 方法
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) 
    applyDefaultToken(params);
    // 最终调用了 WindowManagerGlobal 的 addView 
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);


// WindowManagerGlobal 的 addView
public void addView(View view, ViewGroup.LayoutParams params,
                    Display display, Window parentWindow) 
    // 省略部分代码

    // ViewRootImpl 对象的声明
    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) 
        // 省略部分代码

        // ViewRootImpl 对象的创建
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        try 
            // 调用 ViewRootImpl 的 setView 方法
            root.setView(view, wparams, panelParentView);
         catch (RuntimeException e) 
            // BadTokenException or InvalidDisplayException, clean up.
            if (index >= 0) 
                removeViewLocked(index, true);
            
            throw e;
        
    

由此可以看出,ViewRootImpl 是在 activity 的 onResume 方法调用后才由 WindowManagerGlobal 的 addView 方法创建。

那 requestLayout 是如何调用的呢?

在上面 WindowManagerGlobal 的 addView 方法中,创建完 ViewRootImpl 后,会调用它的 setView 的方法,在 setView 方法内部会调用 requestLayout

此时就会去检测 UI 更新时调用的线程了。

// ViewRootImpl 的 setView
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) 
    synchronized (this) 
        if (mView == null) 
            mView = view;

            // 省略无关代码...
            // requestLayout 的调用
            requestLayout();

          // 省略无关代码...
        


// requestLayout 方法
@Override
public void requestLayout() 
    if (!mHandlingLayoutInLayoutRequest) 
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    

而在 SheduleTranversals 方法中,会调用 TraversalRunnable 的 run方法,最终会在 performTraversals 方法中,调用 performMeasure performLayout performDraw 去开始 View 的绘制流程。

void scheduleTraversals() 
    if (!mTraversalScheduled) 
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // TraversalRunnable 的 run 方法中,会开启 UI 的measure、layout、draw
        mChoreographer.postCallback(
            Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
       // 省略无关代码...
    


final class TraversalRunnable implements Runnable 
    @Override
    public void run() 
        doTraversal();
    

void doTraversal() 
    if (mTraversalScheduled) 
        // 省略部分代码
        performTraversals();
    


private void performTraversals() 
    // Ask host how big it wants to be
    // 省略部分代码
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    performLayout(lp, mWidth, mHeight);
    performDraw();

子线程更新 UI 实战

既然知道了子线程更新 UI 的检测是在 checkThread 方法中,那么有没有什么方法可以绕过呢?能否做到子线程更新 UI 呢?

答案是可以的。

我以一个简单的 demo 实验一下,下面先看效果。

代码如下:

// MainActivity
public class MainActivity extends AppCompatActivity 
    private View containerView;
    private ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener;
    private TextView mTv2;
    private TextView mTv1;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        containerView = findViewById(R.id.container_layout);
        mTv1 = findViewById(R.id.text);
        mTv2 = findViewById(R.id.text2);

        // 开启线程,启动 GlobalLayoutListener
        Executors.newSingleThreadExecutor().execute(() -> initGlobalLayoutListener());
    

    private void initGlobalLayoutListener() 
        globalLayoutListener = () -> 
            Log.e("caihua", "onGlobalLayout : " + Thread.currentThread().getName());
            ViewGroup.LayoutParams layoutParams = containerView.getLayoutParams();
            containerView.setLayoutParams(layoutParams);
        ;
        this.getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener);
    


    public void updateUiInMain(View view) 
        mTv1.setText("主线程更新 UI");
    

    public void updateUiInThread(View view) 
        new Thread()
            @Override
            public void run() 
                SystemClock.sleep(2000);
                mTv2.setText("子线程更新 UI :" + Thread.currentThread().getName());
            
        .start();
    


原理:通过 ViewTreeObserver.OnGlobalLayoutListener 设置全局的布局监听,然后在 onGlobalLayout 方法中,调用 view 的 setLayoutParams 方法,setLayoutParams 方法内部会调用 requestLayout,这样就可以绕过线程检测。

为什么能绕过呢?

因为 setLayoutParams 中调用的 requestLayout 方法并不是 ViewRootImpl 中 requestLayout.

而 View 的 requestLayout 并不调用 checkThread 方法去检测线程。

源码如下↓

// view.setLayoutParams 源码
public void setLayoutParams(ViewGroup.LayoutParams params) 
    if (params == null) 
        throw new NullPointerException("Layout parameters cannot be null");
    
    mLayoutParams = params;
    resolveLayoutParams();
    if (mParent instanceof ViewGroup) 
        ((ViewGroup) mParent).onSetLayoutParams(this, params);
    
    // 调用 requestLayout 方法。
    requestLayout();

// View 的 requestLayout 方法
public void requestLayout() 
    if (mMeasureCache != null) mMeasureCache.clear();

    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) 
        ViewRootImpl viewRoot = getViewRootImpl();
        if (viewRoot != null && viewRoot.isInLayout()) 
            if (!viewRoot.requestLayoutDuringLayout(this)) 
                return;
            
        
        mAttachInfo.mViewRequestingLayout = this;
    

    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;

    if (mParent != null && !mParent.isLayoutRequested()) 
        mParent.requestLayout();
    
    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) 
        mAttachInfo.mViewRequestingLayout = null;
    

以上是关于如何做到在子线程更新 UI?的主要内容,如果未能解决你的问题,请参考以下文章

Android编程:如何在子线程中更新TextView控件

Android 在子线程中更新UI的几种方法示例

ios开发在子线程更新ui会怎样

多线程学习之--真的不能在子线程里更新UI吗?

在子线程中更新UI,只能使用Handler

AndroidUI进阶-为什么不能在子线程更新UI