老生常談的問(wèn)題了惭笑,大家都知道在子線(xiàn)程中更新ui會(huì)報(bào)CalledFromWrongThreadException,此異常在ViewRootImpl.checkThread()方法中拋出捺宗。換句話(huà)說(shuō)川蒙,在子線(xiàn)程中更新UI不觸發(fā)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還未創(chuàng)建一定不會(huì)觸發(fā)checkThread()昼牛,那么問(wèn)題來(lái)了ViewRootImpl何時(shí)創(chuàng)建?
Android Activity康聂、Window贰健、View 有說(shuō),在ActivityThread.handleResumeActivity()方法中先回調(diào)Activity.onResume()恬汁,然后調(diào)用Activity.makeVisible()經(jīng)過(guò)一系列調(diào)用到ViewRootImpl.performTraversals()開(kāi)始繪制流程伶椿。既然ViewRootImpl在onResume()之后才初始化氓侧,那么在onResume()和onResume()之前都可以在子線(xiàn)程中更新UI甘苍,可以驗(yàn)證下看彼。
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>
結(jié)果自然是正常運(yùn)行。嚴(yán)格來(lái)講并不算更新UI茁计,ViewRootImpl還未開(kāi)啟繪制流程星压,此處改變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>
不出所料扇住,看異常堆棧調(diào)用到了ViewRootImpl.requestLayout(),內(nèi)部調(diào)用了ViewRootImpl.checkThread()
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
下面來(lái)尋找源頭,從TextView.setText()開(kāi)始
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()方法中一堆判斷冯键,歸根結(jié)底TextView大小不改變不會(huì)觸發(fā)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()) {
//調(diào)用到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>
再運(yùn)行一下陈肛,點(diǎn)擊按鈕正常更新句旱。
ViewRootImpl.checkThread()中注釋說(shuō)明判斷的是更新UI的線(xiàn)程和創(chuàng)建UI的線(xiàn)程是否相同,并非一直以來(lái)說(shuō)的Android UI主線(xiàn)程匾南。那么在子線(xiàn)程中創(chuàng)建View自然可以在這個(gè)子線(xiàn)程中更新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()
}
}
子線(xiàn)程中初始化TextView并設(shè)置各種屬性,通過(guò)WindowManager添加進(jìn)根布局。ViewRootImpl中初始化了ViewRootHandler逻住,在子線(xiàn)程中記得手動(dòng)創(chuàng)建Looper瞎访。