如何做到在子线程更新 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控件

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

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

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

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

为啥loop之后就可以子线程更新ui