老生常谈的问题了,大家都知道在子线程中更新ui会报CalledFromWrongThreadException。
实际上此异常在ViewRootImpl的checkThread()方法中抛出。换句话说,在子线程中更新ui,不触发checkThread()方法是可以正常更新的。
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还未创建一定不会触发checkThread(),那么问题来了,ViewRootImpl何时创建?
Android Activity、Window、View 之前有说,在ActivityThread.handleResumeActivity()方法中先回调Activity. onResume(),然后调用Activity.makeVisible()经过一系列调用到WindowManagerGlobal.addView(),ViewRootImpl在此方法中初始化,然后调用到ViewRootImpl.performTraversals()开始绘制流程。既然ViewRootImpl在onResume()之后才初始化,那么在onResume()和onResume()之前都是可以直接更新ui的,可以验证下。
TextActivity
package com.chenxuan.jetpack
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_text.*
class TextActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_text)
Thread(Runnable {
tv.text = "call on Thread"
}).start()
}
}
activity_text.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="content"
android:textColor="@android:color/holo_orange_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
结果自然是正常运行。严格来讲并不算更新ui,ViewRootImpl还未初始化、开启绘制流程,此处改变text只是改变了TextView的变量值。
- ViewRootImpl初始化后仍可更新ui,有限定条件。
TextActivity
package com.chenxuan.jetpack
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_text.*
class TextActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_text)
button.setOnClickListener {
Thread(Runnable {
tv.text = "call on Thread"
}).start()
}
}
}
activity_text.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="content"
android:textColor="@android:color/holo_orange_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="update"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv" />
</androidx.constraintlayout.widget.ConstraintLayout>

不出所料。看看异常信息,触发了ViewRootImpl.requestLayout,内部调用了ViewRootImpl.checkThread()
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
下面来寻找源头,从TextView.setText()方法开始
public final void setText(CharSequence text) {
setText(text, mBufferType);
}
public void setText(CharSequence text, BufferType type) {
setText(text, type, true, 0);
if (mCharWrapper != null) {
mCharWrapper.mChars = null;
}
}
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
...
//重点
checkForRelayout();
}
TextView.checkForRelayout()
private void checkForRelayout() {
if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
|| (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
&& (mHint == null || mHintLayout != null)
&& (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
...
if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
&& mLayoutParams.height != LayoutParams.MATCH_PARENT) {
autoSizeText();
invalidate();
return;
}
if (mLayout.getHeight() == oldht
&& (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
autoSizeText();
invalidate();
return;
}
}
requestLayout();
invalidate();
} else {
nullLayouts();
requestLayout();
invalidate();
}
}
checkForRelayout()方法中一堆判断,归根结底,若TextView大小不改变便不会触发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()) {
//调用到ViewRootImpl.requestLayout()
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
固定TextView大小
activity_text.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tv"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:gravity="center"
android:text="content"
android:textColor="@android:color/holo_orange_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="update"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv" />
</androidx.constraintlayout.widget.ConstraintLayout>
再运行一下,点击按钮正常更新。
- ViewRootImpl.checkThread()中注释说明判断的是更新ui的线程和创建ui的线程是否相同,并非一直以来说的Android ui主线程。那么在子线程中创建ui,自然可以在这个子线程中进行任何操作。
package com.chenxuan.jetpack
import android.content.Context
import android.os.Bundle
import android.os.Looper
import android.view.Gravity
import android.view.WindowManager
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class TextActivity : AppCompatActivity() {
lateinit var asyncText: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val wm :WindowManager= getSystemService(Context.WINDOW_SERVICE) as WindowManager
Thread(Runnable {
Looper.prepare()
asyncText = TextView(this)
asyncText.setBackgroundResource(android.R.color.darker_gray)
asyncText.setTextColor(resources.getColor(android.R.color.holo_orange_light))
asyncText.text = "asyncText"
val param = WindowManager.LayoutParams()
param.gravity = Gravity.CENTER
param.width = WindowManager.LayoutParams.WRAP_CONTENT
param.height = WindowManager.LayoutParams.WRAP_CONTENT
wm.addView(asyncText,param)
Looper.loop()
}).start()
}
}
在子线程中初始化TextView,并设置各种属性,通过WindowManager添加进根布局。
ViewRootImpl中初始化了handler->ViewRootHandler,在子线程中记得手动创建Looper。
网友评论