协程是我们在 Android 上进行异步编程的推荐解决方案。值得关注的特点包括:
轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行 *** 作。内存泄漏更少:使用结构化并发机制在一个作用域内执行多项 *** 作。内置取消支持:取消 *** 作会自动在运行中的整个协程层次结构内传播。Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。
回顾下:Kotlin 提供了三个调度程序,以用于指定应在何处运行协程:
Dispatchers.Main - 使用此调度程序可在 Android 主线程上运行协程。此调度程序只能用于与界面交互和执行快速工作。示例包括调用 suspend 函数,运行 Android 界面框架 *** 作,以及更新LiveData对象。Dispatchers.IO - 此调度程序经过了专门优化,适合在主线程之外执行磁盘或网络 I/O。示例包括使用Room组件、从文件中读取数据或向文件中写入数据,以及运行任何网络 *** 作。Dispatchers.Default - 此调度程序经过了专门优化,适合在主线程之外执行占用大量 CPU 资源的工作。用例示例包括对列表排序和解析 JSON。对于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
。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)