一般来讲,子线程是不能更新 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 显示,并且会最终创建 ViewRootImplr.activity.makeVisible();}}
}

进一步跟进 activity.makeVisible()

void makeVisible() {if (!mWindowAdded) {ViewManager wm = getWindowManager();// 往 WindowManager 中添加 DecorViewwm.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、drawmChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);// 省略无关代码...}
}final class TraversalRunnable implements Runnable {@Overridepublic 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;@Overrideprotected 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);// 开启线程,启动 GlobalLayoutListenerExecutors.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(){@Overridepublic 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?相关推荐

  1. Android子线程更新UI的方法总结

    消息机制,对于Android开发者来说,应该是非常熟悉.对于处理有着大量交互的场景,采用消息机制,是再好不过了.有些特殊的场景,比如我们都知道,在Android开发中,子线程不能更新UI,而主线程又不 ...

  2. android 关于关于子线程更新UI的一些事

    我们在看一些书或者博客时总是会看到一句话"android更新UI操作都是在Main主线程中,子线程中不能进行UI更新操作"那么,在子线程中真的不能进行UI的更新操作吗? //源码环 ...

  3. AndroidStudio子线程更新UI的几种方式

    在安卓开发中,大部分情况下是不能在子线程直接更新UI的,只能在UI线程更新UI,其根本原因在于加入在一个Activity中有多个线程去更新UI,且没有加锁机制,可能会产生界面混乱的情况,但是如果都加锁 ...

  4. Android中Activity、Window、ViewRootImpl与子线程更新UI

    三者层级关系 1.Window Window是一个抽象类,唯一的实现类是PhoneWindow Window分为三种类型应用Window.子Window.系统Window.子Window无法独立存在必 ...

  5. Android为什么不能在子线程更新UI

    Android为什么不能在子线程更新UI Android为什么不能在子线程更新UI? 如果不做这个校验,是不是我也可以正常在子线程更新UI 但是google为什么要这样去设计呢 ViewRootImp ...

  6. 面试官问我:Andriod为什么不能在子线程更新UI?

    记得看文章三部曲,点赞,评论,转发. 微信搜索[程序员小安]关注还在移动开发领域苟活的大龄程序员,"面试系列"文章将在公众号同步发布. 1.前言 看完<你为什么在现在的公司不 ...

  7. C#利用Invoke和委托实现子线程更新UI(方式1)

    UI布局如下 委托定义如下: public delegate void SetMessageDelegate(string message); From1的代码如下: public partial c ...

  8. pyqt5 子线程更新ui

    from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import * import time''' 信号 ...

  9. 子线程更新UI,牵扯activity的启动过程

    http://m.blog.csdn.net/article/details?id=43449123 点击打开链接

  10. 为什么我的子线程更新了 UI 没报错?借此,纠正一些Android 程序员的一个知识误区...

    开门见山: 这个误区是:子线程不能更新 UI ,其应该分类讨论,而不是绝对的. 半小时前,我的 XRecyclerView 群里面,一位群友私聊我,问题是: 为什么我的子线程更新了 UI 没报错? 我 ...

最新文章

  1. JAVA(小技巧--List)
  2. 【网络流】网络流小总结
  3. 反转数字(qduoj)
  4. c语言3种链接属性: 外部(external), 内部(internal),无设置(none)
  5. 《写给大家看的设计书》封面设计基本要求
  6. Tabulator PDF下载中文字体设置
  7. 回声消除技术原理与解决办法
  8. 第三章 心剑,有妹紫灵
  9. js获取浏览器类型及版本
  10. Manjaro的安装与配置
  11. 对话腾讯天琴赵伟峰:当音乐与科技结合,会碰撞出怎样的火花?
  12. ios16隐藏的六个实用功能,你知道几个?
  13. 华为系统更新彻底卸载_华为手机系统更新好吗 华为手机系统更新方法
  14. 1051 复数乘法(JAVA)
  15. 随机森林算法(RandomForest)实现MNIST手写体数字识别
  16. 干细胞技术是不是骗局
  17. 熵权法(客观赋权法)
  18. 中国联通今年5G投资350亿元!附2019业绩PPT全文
  19. jzoj6377. 【NOIP2019模拟2019.10.05】幽曲[埋骨于弘川]
  20. 光明旅者的冲刺挑战成就

热门文章

  1. shortcutwebsite
  2. 后台任务列表 App 界面模糊处理
  3. 学习“基于深度学习的故障诊断”开源
  4. potato电脑版连接不上_potato chat正式版PC端安装教程
  5. 数据结构课程设计 # 论文查重分析系统 (C/C++版和python版)
  6. Foxmail添加163邮箱账号的方法
  7. Black Hat 2017:不容错过的七大主题演讲
  8. win10激活--以专业版(professional)为例
  9. 华为手机非华为电脑NFC一碰传使用
  10. 快速原型工具,帮你从0开始画原型图!