很多初學(xué)者肯定有這樣一個經(jīng)驗肩刃,在Activity的一個子線程中更新UI精绎,發(fā)現(xiàn)會報錯速缨。很多人知道這個錯誤,但卻不知道是什么原因引起的代乃。今天我們來分析一下是什么原因引起的旬牲。我今天以textview更新ui為例。
首先看代碼
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.generalwei.mytest.ThreadActivity">
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="hello"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:id="@+id/btn_change"
android:text="更換UI"/>
</RelativeLayout>
public class ThreadActivity extends AppCompatActivity {
private android.widget.TextView tv;
private android.widget.Button btnchange;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_thread);
this.btnchange = (Button) findViewById(R.id.btn_change);
this.tv = (TextView) findViewById(R.id.tv);
btnchange.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
tv.setText(new Date().getTime()+"");
}
}).start();
}
});
}
}
運行app搁吓,點擊btnchange按鈕原茅,發(fā)現(xiàn)app崩潰。查看日志,發(fā)現(xiàn)這樣一個錯誤:
我們來查看一下tv.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) {
...
if (mLayout != null) {
checkForRelayout();
}
...
}
這里我們還是沒有看見什么原因引起的擂橘,繼續(xù)追查checkForRelayout()方法。
private void checkForRelayout() {
...
invalidate();
...
}
查看invalidate()方法,我們會看見這樣一個注釋:
/**
* Invalidate the whole view. If the view is visible,
* {@link #onDraw(android.graphics.Canvas)} will be called at some point in
* the future.
* <p>
* This must be called from a UI thread. To call from a non-UI thread, call
* {@link #postInvalidate()}.
*/
public void invalidate() {
invalidate(true);
}
意思是說如果view是可見的摩骨,這個方法會刷新view通贞。但是必須發(fā)生在ui線程上∧瘴澹看到這邊我們發(fā)現(xiàn)我們的追蹤是正確昌罩。那么接著看,為什么一定要在ui線程上更新ui唤冈。
void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
...
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
...
}
ViewParent 是一個接口,ViewRootImpl是它的實現(xiàn)類,那么我們繼續(xù)追查,代碼如下:
@Override
public void invalidateChild(View child, Rect dirty) {
invalidateChildInParent(null, dirty);
}
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
...
}
你會發(fā)現(xiàn)一個檢查線程的方法银伟,那么查看方法checkThread()你虹。
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
查看代碼后發(fā)現(xiàn)有一個mThread線程,它是會是UI線程嗎彤避,我們就查看Thread的來源傅物。
public ViewRootImpl(Context context, Display display) {
...
mThread = Thread.currentThread();
...
發(fā)現(xiàn)mThread是在ViewRootImpl創(chuàng)建的時候賦值,這個的線程一定是UI線程琉预。所以當(dāng)前線程不是UI線程的時候會拋異常董饰。
為什么在onResume之前非UI線程也能更新UI
發(fā)現(xiàn)mThread是在ViewRootImpl創(chuàng)建的時候賦值,這個的線程一定是UI線程圆米。所以當(dāng)前線程不是UI線程的時候會拋異常卒暂。
但是有時候在也能在非UI線程中更新,后來我們發(fā)現(xiàn)在onResume之前用非UI線程更新能UI娄帖,而onResume之后就不行了也祠。這是因為onResume之前還沒有創(chuàng)建ViewRootImpl這個類,ActivityThread類中有一個handleResumeActivity方法近速,這個方法是用來回調(diào)Activity的onResume方法诈嘿,具體的看如下代碼:
final void handleResumeActivity(IBinder token,boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
ActivityClientRecord r = mActivities.get(token);
if (!checkAndUpdateLifecycleSeq(seq, r, "resumeActivity")) {
return;
}
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
mSomeActivitiesChanged = true;
// TODO Push resumeArgs into the activity for consideration
r = performResumeActivity(token, clearHide, reason);
if (r != null) {
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 && !a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, 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;
}
...
// Tell the activity manager we have resumed.
if (reallyResume) {
try {
ActivityManagerNative.getDefault().activityResumed(token);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
} else {
// If an exception was thrown when trying to resume, then
// just end this activity.
try {
ActivityManagerNative.getDefault()
.finishActivity(token, Activity.RESULT_CANCELED, null,
Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
}
我們可以看見這樣一個方法performResumeActivity()堪旧,它的源碼如下:
public final ActivityClientRecord performResumeActivity(IBinder token,boolean clearHide, String reason) {
...
if (r != null && !r.activity.mFinished) {
...
r.activity.performResume();
...
}
}
這里可以看見它調(diào)用了activity.performResume(),那么再繼續(xù)查看下面的源碼:
final void performResume() {
performRestart();
...
mInstrumentation.callActivityOnResume(this);
...
onPostResume();
...
}
performRestart()方法主要是為了執(zhí)行回調(diào)onRestart方法,具體內(nèi)容就不做分析了奖亚。mInstrumentation.callActivityOnResume()方法則是為了回調(diào)Activity的OnResume()方法淳梦。onPostResume()方法這是為了激活Window。
在handleResumeActivity()方法中我們可以看見一個WindowManager類昔字,這個類是用來控制窗口顯示的爆袍,而它的addView是用來添加視圖。WindowManagerImpl是WindowManager的實現(xiàn)類李滴,WindowManagerImpl的addView方法代碼如下:
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
mGlobal是WindowManagerGlobal的對象螃宙,繼續(xù)看mGlobal.addView的代碼:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// Start watching for system property changes.
if (mSystemPropertyUpdater == null) {
mSystemPropertyUpdater = new Runnable() {
@Override public void run() {
synchronized (mLock) {
for (int i = mRoots.size() - 1; i >= 0; --i) {
mRoots.get(i).loadSystemProperties();
}
}
}
};
SystemProperties.addChangeCallback(mSystemPropertyUpdater);
}
int index = findViewLocked(view, false);
if (index >= 0) {
if (mDyingViews.contains(view)) {
// Don't wait for MSG_DIE to make it's way through root's queue.
mRoots.get(index).doDie();
} else {
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
}
// The previous removeView() had not completed executing. Now it has.
}
// If this is a panel window, then find the window it is being
// attached to for future reference.
if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
final int count = mViews.size();
for (int i = 0; i < count; i++) {
if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
panelParentView = mViews.get(i);
}
}
}
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
...
}
你可以看做ViewRootImpl的初始化時在這里進行的,這就是為什么在onResume之前可以更新UI了所坯。
為什么要這么設(shè)計呢谆扎?
因為所有的UI控件都是非線程安全的,如果在非UI線程更新UI會造成UI混亂芹助。所以一般我們會在Handler中更新UI堂湖。
如有寫的不當(dāng)之處,請多指教状土。