
還是先來(lái)一張圖片,慶祝一下19-20賽季的湖人總冠軍吧??!
Android中的子線程能否操作UI
現(xiàn)在進(jìn)入正題,想想從開(kāi)始工作到現(xiàn)在,被無(wú)數(shù)次的告誡子線程不能更新UI,UI操作必須在主線程中完成。然而作為辣雞代碼搬運(yùn)工的我,怎么能輕易就聽(tīng)你們的呢。
public class MainActivity extends AppCompatActivity {
private TextView mTvTest;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTvTest = findViewById(R.id.tv_test);
new Thread(new Runnable() {
@Override
public void run() {
mTvTest.setText("子線程修改UI成功");
}
}).start();
}
}
一番輸出,run起來(lái),完全沒(méi)有問(wèn)題啊,Activity上的TextView上面的的文字不也就更新了啊。
但是,在我們的實(shí)際開(kāi)發(fā)中,當(dāng)我們開(kāi)啟一個(gè)新的線程耗時(shí)操作之后,直接更新UI,就會(huì)出現(xiàn)問(wèn)題。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = findViewById(R.id.tv);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
tv.setText("子線程修改UI成功");
}
}).start();
}
瞬間就崩潰了
Process: cn.qiaowa.testapplication, PID: 21102
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:9219)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1600)
拋出了一個(gè)Only the original thread that created a view hierarchy can touch its views.的異常,告訴我們只能在主線程中操作UI。
從這兩段代碼,我們可以清楚的得出一個(gè)結(jié)論Android中子線程是可以操作UI的,但是為什么第二段代碼中,我們子線程中操作UI的時(shí)候,有會(huì)拋出異常Only the original thread that created a view hierarchy can touch its views.我們接下來(lái)繼續(xù)研究一下。
異常為何產(chǎn)生
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
根據(jù)Log日志,我們可以知道這個(gè)異常是在 android.view.ViewRootImpl這個(gè)類(lèi)中拋出來(lái)的。定位一下源碼
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
繼續(xù)追蹤一下ViewRootImpl#checkThread()這個(gè)方法的調(diào)用地方
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
看到這里requestLayout () 方法,就是View里面請(qǐng)求重新測(cè)量布局的方法。
再看看ViewPootImpl的類(lèi)繼承關(guān)系
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
}
ViewPootImpl實(shí)現(xiàn)了一個(gè)ViewParent接口,而requestLayout ()方法就是繼承于這個(gè)地方。
public interface ViewParent {
// 去除了其他代碼
/**
* Called when something has changed which has invalidated the layout of a
* child of this view parent. This will schedule a layout pass of the view
* tree.
*/
public void requestLayout();
}
到這里我們已經(jīng)研究出這個(gè)異常的大致流程了,但是這個(gè)異常是從那個(gè)地方出發(fā)的呢?換句話說(shuō)就requestLayout()這個(gè)方法是怎么調(diào)用的呢?我們探究。
requestLayout()這個(gè)方法是View里面請(qǐng)求重新測(cè)量布局的。那么我們就回到TextView.setText(...)中看看。
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
//代碼省略
if (mLayout != null) {
//關(guān)鍵代碼所在
checkForRelayout();
}
sendOnTextChanged(text, 0, oldlen, textLength);
onTextChanged(text, 0, oldlen, textLength);
notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
if (needEditableForNotification) {
sendAfterTextChanged((Editable) text);
} else {
notifyAutoFillManagerAfterTextChangedIfNeeded();
}
// SelectionModifierCursorController depends on textCanBeSelected, which depends on text
if (mEditor != null) mEditor.prepareCursorControllers();
}
再看看checkForRelayout ()
/**
* Check whether entirely new text requires a new view layout
* or merely a new text layout.
*/
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();
}
}
這里不管是走if還是else,都會(huì)走TextView#requestLayout()方法,那我們?cè)倏纯催@個(gè)TextView#requestLayout()
public void requestLayout() {
//代碼省略
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
}
最終它會(huì)調(diào)用mParent.requestLayout();這個(gè)mParent是一個(gè)ViewParent接口類(lèi),而在開(kāi)始我們分析ViewRootImpl時(shí)候我們就知道了它就實(shí)現(xiàn)了ViewParent接口,所以通過(guò)TextView.setText(...)最終就會(huì)調(diào)用到ViewRootImpl #RequestLayout方法。
再回到setText()的源碼中的
if (mLayout != null) {
checkForRelayout();
}
如果mLayout不為空的時(shí)候才會(huì)進(jìn)行線程的檢測(cè),這個(gè)時(shí)候如果再子線程操作UI的時(shí)候必然會(huì)拋出異常。想一下,如果這個(gè)mLayout位空的話,不就可以繞開(kāi)線程的校驗(yàn)了么(第一個(gè)事例子線程更新UI的事例就是這種情況),就不會(huì)拋出異常了。
走到這里我們可以得到一個(gè)更加準(zhǔn)確的結(jié)論:Android中子線程是可以更新UI的,當(dāng)mLayout還沒(méi)有初始化完成的時(shí)候,子線程更新UI不會(huì)拋出異常。而在mLayout初始化完成之后,更新UI就會(huì)進(jìn)行線程校驗(yàn),如果是在當(dāng)前線程是子線程就會(huì)拋出異常,告訴我們只能在主線程中更新UI
由此可見(jiàn),子線程操作UI的時(shí)候,是否拋出異常的關(guān)鍵就是這個(gè)mLayout的初始化情況了,那我們繼續(xù)研究一下mLayout的初始化情況。
mLayout 的初始化
從開(kāi)始的兩個(gè)事例中發(fā)現(xiàn)唯一不同的是,第二段代碼讓子線程sleep了3秒后才去更新UI的,結(jié)果就拋出了異常,也就是mLayout已經(jīng)初始化完成了??梢灾肋@個(gè)肯定是個(gè)生命周期有一定的關(guān)系。那么先看看ActivityThread這個(gè)類(lèi)。
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
//代碼省略
// TODO Push resumeArgs into the activity for consideration
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
//代碼省略
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
} else {
// The activity will get a callback for this {@link LayoutParams} change
// earlier. However, at that time the decor will not be set (this is set
// in this method), so no action will be taken. This call ensures the
// callback occurs with the decor set.
a.onWindowAttributesChanged(l);
}
}
// If the window has already been added, but during resume
// we started another activity, then don't yet make the
// window visible.
} else if (!willBeVisible) {
if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
//代碼省略
}
關(guān)鍵代碼wm.addView(decor, l),這個(gè)就是將 decorView 添加到 window中。我們?cè)诳匆幌?WindowManager的實(shí)現(xiàn)類(lèi) WindowManagerImpl
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
發(fā)現(xiàn)此時(shí),addView操作交給了mGlobal這個(gè)代理類(lèi)來(lái)處理,而這個(gè)代理類(lèi)正是WindowManagerGlobal,那我們繼續(xù)看看
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
ViewRootImpl root;
...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
初始化一個(gè)ViewRootImpl類(lèi),然后調(diào)用setView方法,
view.assignParent(this);
在setView放里面調(diào)用了assignParent方法,將ViewRootImpl傳入到View當(dāng)中。
由此,可以看出在activity 的onResume生命周期中進(jìn)行了 mLayout的初始化,從初始化完成之后,操作UI就要進(jìn)行線程的檢測(cè)。