赞
踩
协程是我们在 Android 上进行异步编程的推荐解决方案。值得关注的特点包括:
回顾下:Kotlin 提供了三个调度程序,以用于指定应在何处运行协程:
对于Dispatchers.IO 以下调用会报错:
- lifecycleScope.launch(Dispatchers.IO) {
- // 这里报错:Only the original thread that created a view hierarchy can touch its views.
- testView.text = "测试"
- }
- at android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
- at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8191)
- at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1420)
因为这在IO线程中来更新UI了。
### 那么下面来看代码示例-1:
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- ...
-
- lifecycleScope.launch(Dispatchers.IO) {
- testView.text = "测试"
- }
-
- ...
- }
示例-1 代码中也更新UI,而它不会报错:Only the original thread that created a view hierarchy can touch its views.
这就很奇怪了,这里也在IO线程中来更新了UI! 为什么呢?
#1. 开始追逐源码:根据报错提示,发现报错地方是:ViewRootImpl.checkThread(),我们可以理解为 示例-1 没有执行该方法:
- // ViewRootImpl.java
- void checkThread() {
- if (mThread != Thread.currentThread()) {
- throw new CalledFromWrongThreadException(
- "Only the original thread that created a view hierarchy can touch its views.");
- }
- }
继续根据报错提示,会发现是这个方法: ViewRootImpl.requestLayout()
- /**
- * The top of a view hierarchy, implementing the needed protocol between View
- * and the WindowManager. This is for the most part an internal implementation
- * detail of {@link WindowManagerGlobal}.
- *
- * {@hide}
- */
- @SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})
- public final class ViewRootImpl implements ViewParent,
- View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
-
- ...
-
- @Override
- public void requestLayout() {
- if (!mHandlingLayoutInLayoutRequest) {
- checkThread();
- mLayoutRequested = true;
- scheduleTraversals();
- }
- }
-
- ...
-
- }

里面有个参数:mHandlingLayoutInLayoutRequest,如果
mHandlingLayoutInLayoutRequest = true
就不会调用 checkThread(),我们离真相更近一步。当为True时的条件是:
- // Set this flag to indicate that any further requests are happening during
- // the second pass, which may result in posting those requests to the next
- // frame instead
- mHandlingLayoutInLayoutRequest = true;
注释的意思是:设置此标志以指示在第二次传递期间正在发生任何进一步的请求,这可能导致将这些请求发布到下一帧,很难懂!!!
#2. 摸不着头脑,那我们换个思路来,从 testView.text = "测试" 这行代码来查看 TextView.java
- // TextView.java
- @android.view.RemotableViewMethod
- public final void setText(CharSequence text) {
- setText(text, mBufferType);
- }
-
- public void setText(CharSequence text, BufferType type) {
- setText(text, type, true, 0);
-
- ...
- }
- @UnsupportedAppUsage
- private void setText(CharSequence text, BufferType type,
- boolean notifyBefore, int oldlen) {
- ...
-
- if (mLayout != null) {
- checkForRelayout();
- }
-
- ...
- }
这里我们看下 checkForRelayout()
- // TextView.java
- @UnsupportedAppUsage
- private void checkForRelayout() {
- // If we have a fixed width, we can just swap in a new text layout
- // if the text height stays the same or if the view height is fixed.
-
- if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
- || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
- && (mHint == null || mHintLayout != null)
- && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
-
- ...
-
- // We lose: the height has changed and we have a dynamic height.
- // Request a new view layout using our new text layout.
- requestLayout();
- invalidate();
- } else {
- // Dynamic width, so we have no choice but to request a new
- // view layout with a new text layout.
- nullLayouts();
- requestLayout();
- invalidate();
- }
- }

可以看到始终会执行方法 requestLayout() ,但是这里是View.java类中,进一步查看:
- // View.java
- @CallSuper
- public void requestLayout() {
-
- ...
-
- if (mParent != null && !mParent.isLayoutRequested()) {
- mParent.requestLayout();
- }
-
- ...
- }
现在可以看到 mParent.requestLayout() ,这里的 mParent 是 ViewParent 的对象,而ViewRootImpl类是ViewParent的子类,上面的代码首先判断mParent是否为空,也就是ViewRootImpl对象,可以想象这个时候是因为 ViewRootImp 还没有创建,所以不会执行线程检查;而ViewRootImpl对象是在onResume()方法之后才创建的。
有兴趣的可以去查看下去寻找ViewRootImp什么时候创建,从Activity启动时查找源代码,通过分析可以查看ActivityThread.java源代码。
因此可以解答了前面的问题:在示例-1中启动IO线程来更新UI,是可以正常更新UI的。
而且我们在 onStart()和onResume()方法中启动子线程来更新UI都不会报错。
后语:要更新UI一定要在主线程中实现
谷歌提出:“一定要在主线程中
UI
”,其实是为了提高界面的效率和安全性,带来更好的流畅度;退一步,如果允许多线程更新UI
,但是访问UI
没有被锁定,一旦多线程抢占资源,那么界面就会无序更新,体验效果不言而喻;所以在Android
必须在主线程中更新UI
。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。