Android单线程模型是这样描述的:

Android UI操作并不是线程安全的,并且这些操作必须在UI线程执行

  如果在其它线程访问UI线程,Android提供了以下的方式:

Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)
Handler

  为什么呢?在子线程中就不能操作UI么?

  当一个程序第一次启动的时候,Android会同时启动一个对应的主线程,这个主线程就是UI线程,也就是ActivityThread。UI线程主要负责处理与UI相关的事件,如用户的按键点击、用户触摸屏幕以及屏幕绘图等。系统不会为每个组件单独创建一个线程,在同一个进程里的UI组件都会在UI线程里实例化,系统对每一个组件的调用都从UI线程分发出去。所以,响应系统回调的方法永远都是在UI线程里运行,如响应用户动作的onKeyDown()的回调。

  那为什么选择一个主线程干这些活呢?换个说法,Android为什么使用单线程模型,它有什么好处?

  先让我们看下单线程化的事件队列模型是怎么定义的:

采用一个专门的线程从队列中抽取事件,并把他们转发给应用程序定义的事件处理器

  这看起来就是Android的消息队列、Looper和Handler嘛。类似知识请参考:深入理解Message, MessageQueue, Handler和Looper

  其实现代GUI框架就是使用了类似这样的模型:模型创建一个专门的线程,事件派发线程来处理GUI事件。单线程化也不单单存在Android中,Qt、XWindows等都是单线程化。当然,也有人试图用多线程的GUI,最终由于竞争条件和死锁导致的稳定性问题等,又回到单线程化的事件队列模型老路上来。单线程化的GUI框架通过限制来达到线程安全:所有GUI中的对象,包括可视组件和数据模型,都只能被事件线程访问。

  这就解释了Android为什么使用单线程模型。

  那Android的UI操作并不是线程安全的又是怎么回事?

  Android实现View更新有两组方法,分别是invalidate和postInvalidate。前者在UI线程中使用,后者在非UI线程中使用。换句话说,Android的UI操作不是线程安全可以表述为invalidate在子线程中调用会导致线程不安全。作一个假设,现在我用invalidate在子线程中刷新界面,同时UI线程也在用invalidate刷新界面,这样会不会导致界面的刷新不能同步?既然刷新不同步,那么invalidate就不能在子线程中使用。这就是invalidate不能在子线程中使用的原因。

  postInvalidate可以在子线程中使用,它是怎么做到的?

  看看源码是怎么实现的:

public void postInvalidate() {postInvalidateDelayed(0);
}public void postInvalidateDelayed(long delayMilliseconds) {// We try only with the AttachInfo because there's no point in invalidating// if we are not attached to our windowif (mAttachInfo != null) {Message msg = Message.obtain();msg.what = AttachInfo.INVALIDATE_MSG;msg.obj = this;mAttachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds);}
}

  说到底还是通过Handler的sendMessageDelayed啊,还是逃不过消息队列,最终还是交给UI线程处理。所以View的更新只能由UI线程处理。

  如果我非要在子线程中更新UI,那会出现什么情况呢?

android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

  抛了一个CalledFromWrongThreadException异常。

  相信很多人遇到这个异常后,就会通过前面的四种方式中的其中一种解决:

Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)
Handler

  说到底还没触发到根本,为什么会出现这个异常呢?这个异常在哪里抛出来的呢?

void checkThread() {if (mThread != Thread.currentThread()) {throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views.");}
}

  该代码出自 framework/base/core/java/android/view/ViewRootImpl.java

  再看下ViewRootImpl的构造函数,mThread就是在这初始化的:

public ViewRootImpl(Context context, Display display) {mContext = context;mWindowSession = WindowManagerGlobal.getWindowSession();mDisplay = display;mBasePackageName = context.getBasePackageName();mDisplayAdjustments = display.getDisplayAdjustments();mThread = Thread.currentThread();......
}

  再研究一下这个CalledFromWrongThreadException异常的堆栈,会发现最后到了invalidateChild和invalidateChildInParent方法中:

@Override
public void invalidateChild(View child, Rect dirty) {invalidateChildInParent(null, dirty);
}@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {checkThread();......
}

  最终通过checkThread形成了这个异常。说到底,非UI线程是可以刷新UI的呀,前提是它要拥有自己的ViewRoot。如果想直接创建ViewRoot实例,你会发现找不到这个类。那怎么做呢?通过WindowManager。

class NonUiThread extends Thread{@Overridepublic void run() {Looper.prepare();TextView tx = new TextView(MainActivity.this);tx.setText("non-UiThread update textview");WindowManager windowManager = MainActivity.this.getWindowManager();WindowManager.LayoutParams params = new WindowManager.LayoutParams(
             200, 200, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,WindowManager.LayoutParams.TYPE_TOAST,PixelFormat.OPAQUE);windowManager.addView(tx, params); Looper.loop();}}

  就是通过windowManager.addView创建了ViewRoot,WindowManagerImpl.java中的addView方法:

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {applyDefaultToken(params);mGlobal.addView(view, params, mDisplay, mParentWindow);
}

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

  mGlobal是一个WindowManagerGlobal实例,代码在 frameworks/base/core/java/android/view/WindowManagerGlobal.java中,具体实现如下:

public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {if (view == null) {throw new IllegalArgumentException("view must not be null");}if (display == null) {throw new IllegalArgumentException("display must not be null");}if (!(params instanceof WindowManager.LayoutParams)) {throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");}final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;if (parentWindow != null) {parentWindow.adjustLayoutParamsForSubWindow(wparams);} else {// If there's no parent, then hardware acceleration for this view is// set from the application's hardware acceleration setting.final Context context = view.getContext();if (context != null&& (context.getApplicationInfo().flags& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;}}ViewRootImpl root;View panelParentView = null;synchronized (mLock) {// Start watching for system property changes.if (mSystemPropertyUpdater == null) {mSystemPropertyUpdater = new Runnable() {@Override public void run() {synchronized (mLock) {for (int i = mRoots.size() - 1; i >= 0; --i) {mRoots.get(i).loadSystemProperties();}}}};SystemProperties.addChangeCallback(mSystemPropertyUpdater);}int index = findViewLocked(view, false);if (index >= 0) {if (mDyingViews.contains(view)) {// Don't wait for MSG_DIE to make it's way through root's queue.
                    mRoots.get(index).doDie();} else {throw new IllegalStateException("View " + view+ " has already been added to the window manager.");}// The previous removeView() had not completed executing. Now it has.
            }// If this is a panel window, then find the window it is being// attached to for future reference.if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {final int count = mViews.size();for (int i = 0; i < count; i++) {if (mRoots.get(i).mWindow.asBinder() == wparams.token) {panelParentView = mViews.get(i);}}}root = new ViewRootImpl(view.getContext(), display);view.setLayoutParams(wparams);mViews.add(view);mRoots.add(root);mParams.add(wparams);}// do this last because it fires off messages to start doing thingstry {root.setView(view, wparams, panelParentView);} catch (RuntimeException e) {// BadTokenException or InvalidDisplayException, clean up.synchronized (mLock) {final int index = findViewLocked(view, false);if (index >= 0) {removeViewLocked(index, true);}}throw e;}}

  所以,非UI线程能更新UI,只要它有自己的ViewRoot。

  延伸一下:Android Activity本身是在什么时候创建ViewRoot的呢?

  既然是单线程模型,就要先找到这个UI线程实现类ActivityThread,看里面哪里addView了。没错,是在onResume里面,对应ActivityThread就是handleResumeActivity这个方法:

final void handleResumeActivity(IBinder token,boolean clearHide, boolean isForward, boolean reallyResume) {// If we are getting ready to gc after going to the background, well// we are back active so skip it.
        unscheduleGcIdler();mSomeActivitiesChanged = true;// TODO Push resumeArgs into the activity for considerationActivityClientRecord r = performResumeActivity(token, clearHide);......if (r.window == null && !a.mFinished && willBeVisible) {r.window = r.activity.getWindow();View decor = r.window.getDecorView();decor.setVisibility(View.INVISIBLE);ViewManager wm = a.getWindowManager();WindowManager.LayoutParams l = r.window.getAttributes();a.mDecor = decor;l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;l.softInputMode |= forwardBit;if (a.mVisibleFromClient) {a.mWindowAdded = true;wm.addView(decor, l);}// If the window has already been added, but during resume// we started another activity, then don't yet make the// window visible.} else if (!willBeVisible) {if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");r.hideForNow = true;}......
}

  所以,如果在onCreate中通过子线程直接更新UI,并不会抛CalledFromWrongThreadException异常。但是一般情况下,我们不会在onCreate中做这样的事情。

  这就是Android为我们设计的单线程模型,核心就是一句话:Android UI操作并不是线程安全的,并且这些操作必须在UI线程执行。但这一句话背后,却隐藏着我们平时看不见的代码实现,只有搞懂这些,我们才能知其然知其所以然。

参考:Android子线程在没有ViewRoot的情况下能刷新UI吗?

转载于:https://www.cnblogs.com/lao-liang/p/5108745.html

Android子线程真的不能更新UI么相关推荐

  1. 【转】Android子线程真的不能更新UI么

    Android单线程模型是这样描述的: Android UI操作并不是线程安全的,并且这些操作必须在UI线程执行 如果在其它线程访问UI线程,Android提供了以下的方式: Activity.run ...

  2. 面试官:子线程 真的不能更新UI ?

    我的公众号:胡飞洋 我们从一个异常说起: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thr ...

  3. Android 为什么不能在子线程中直接更新UI

    1.现象 在子线程中直接更新UI就会crash,报错如下: android.view.ViewRootImpl$CalledFromWrongThreadException:Only the orig ...

  4. 安卓在子线程中实现更新UI界面的三种方法 Handler+Message、runOnUiThread、控件.post()

    1.说明 安卓中UI线程为主线程,更新UI界面必须在主线程中进行,在子线程中实现更新UI界面的三种方法:Handler.RunOnUiThread.控件.post() 2.1 Handler (1)定 ...

  5. android线程改变布局,震惊!Android子线程也能修改UI?(第二篇)

    某天早晨,群里有个小伙伴这样问了一个问题: XXX:为什么我的控件可以在子线程里面更新 我(不假思索):你是不是在onCreate里面开了一个子线程,然后更新了UI XXX:好像是这样.. 我:你试试 ...

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

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

  7. 为什么Android必须在主线程更新UI?

    为什么Android必须在主线程更新UI? 站在各位大牛的肩膀上,谢谢! 正常情况下,Android需要在UI线程更新UI,然鹅,在特殊情况下,子线程也能更新UI不在讨论之列,可参考Android中子 ...

  8. 为什么 Android 必须在主线程更新 UI ?

    点击蓝字 关注我们 为什么Android必须在主线程更新UI? 站在各位大牛的肩膀上,谢谢! 正常情况下,Android需要在UI线程更新UI,然鹅,在特殊情况下,子线程也能更新UI不在讨论之列,这篇 ...

  9. Android 子线程更新主线程UI视图

    消息机制,对于Android开发者来说,应该是非常熟悉.对于处理有着大量交互的场景,采用消息机制,是再好不过了.在Android开发中,子线程不能更新主线程UI,而主线程又不能进行耗时操作(例:网络请 ...

最新文章

  1. linux磁盘管理相关命令,Linux | 磁盘管理命令
  2. vlc打开h264参数配置
  3. nginx 官方手册 php,nginx + php 的配置
  4. 0523 CSS知识点
  5. uart口图片_uart 加强了的串口调试助手,可以自动记录传输数据,并且显示图片,示波器等功能 Com Port 编程 267万源代码下载- www.pudn.com...
  6. mac mysql prefpane_【MySQL数据库开发之一】Mac下配置安装数据库-MySQL
  7. prim算法_历时两月,终拿字节跳动offer,算法面试题分享「带答案」
  8. JS:关于JS字面量及其容易忽略的12个小问题
  9. 亿能bms上位机_BMS上位机 - 源码下载|Windows编程|通讯编程|源代码 - 源码中国
  10. 浅谈游戏《Hollow Knight空洞骑士》
  11. 程序员必备75道逻辑思维题(附答案)之二
  12. IT面试技巧经典问答
  13. 嘘——2021还没对象?你的虚拟女友已上线。
  14. 使用screw一键生成数据库文档
  15. 关键字synchronized与volatile详解
  16. Python+selenium实现医院自动挂号
  17. 只谈处理器 且看Apple A4到A5的进化
  18. 有哪些高质量的英文有声书 audiobook?
  19. QVariant方法功能(QT5.12)
  20. 图片和字符串相互转换

热门文章

  1. 当root.sh与ORA-15031相遇
  2. 如何安装部署秋色园QBlog站点
  3. jetbrains从入门到卸载 (前言) 为什么要jetbrains
  4. Android TextView内容过长加省略号,点击显示全部内容
  5. linux shell 中21含义
  6. ORACLE 效率测试小工具 Runstats
  7. 不让自己的应用程序在桌面的图标列表里启动显示的方法
  8. Android ListView滑动后背景变黑
  9. java线程池(ThreadPool)
  10. Android高效加载大图、多图解决方案_LruCache,有效避免程序OOM