如何做到在子线程更新 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?的主要内容,如果未能解决你的问题,请参考以下文章