老生常谈的问题了,大家都知道在子线程中更新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()经过一系列调用到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主线程。那么在子线程中创建View自然可以在这个子线程中更新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中初始化了ViewRootHandler,在子线程中记得手动创建Looper。