關(guān)于這個問題在面試的時候可能會被問到凌节,其實在某些情況下是可以在子線程中更新UI的!
比如:在一個activity的xml文件中中隨便寫一個TextView文本控件蕉斜,然后在Activity的onCreate方法中開啟一個子線程并在該子線程的run方法中更新TextView文本控件,你會發(fā)現(xiàn)根本沒有任何問題怕膛。
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.threadcrashui_act);
textView = (TextView) findViewById(R.id.textView);
new Thread(new Runnable() {
@Override
public void run() {
// 更新TextView文本內(nèi)容
textView.setText("update TextView");
}
}).start();
}
5935799a621f34c621177ed25036e893.jpg
但是如果你讓子線程休眠2秒鐘如下面代碼:
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.threadcrashui_act);
textView = (TextView) findViewById(R.id.textView);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
//更新TextView文本內(nèi)容
textView.setText("update TextView");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
8fcda7657e437b3b8597ba5e430d3047.jpg
程序直接掛掉了熟嫩,好吧,我們先去看看log日志褐捻,如圖提示
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
程序不允許在非UI線程中更新UI線程
1b79fb4117a3fe4624dc48e1c777d84d.jpg
對于這個問題掸茅,可能大家都跟我一樣有點疑惑對吧?我當時也很疑惑柠逞,不明白為啥是這樣C潦ā!板壮!然后我去翻資料查找原因逗鸣,最終定位到setText這個方法
/**
* Sets the string value of the TextView. TextView <em>does not</em> accept
* HTML-like formatting, which you can do with text strings in XML resource files.
* To style your strings, attach android.text.style.* objects to a
* {@link android.text.SpannableString SpannableString}, or see the
* <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources">
* Available Resource Types</a> documentation for an example of setting
* formatted text in the XML resource file.
*
* @attr ref android.R.styleable#TextView_text
*/
@android.view.RemotableViewMethod
public final void setText(CharSequence text) {
setText(text, mBufferType);
}
緊接著看setText方法
/**
* Sets the text that this TextView is to display (see
* {@link #setText(CharSequence)}) and also sets whether it is stored
* in a styleable/spannable buffer and whether it is editable.
*
* @attr ref android.R.styleable#TextView_text
* @attr ref android.R.styleable#TextView_bufferType
*/
public void setText(CharSequence text, BufferType type) {
setText(text, type, true, 0);
if (mCharWrapper != null) {
mCharWrapper.mChars = null;
}
}
再進setText方法
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
//前邊的都省略掉 .......
if (mLayout != null) {
//這個方法的作用就是讓界面重新繪制下
checkForRelayout();
}
//后邊的也直接省略掉
然后我們看下checkForRelayout方法,在這里大家會看到一個invalidate的方法个束,因為我們知道所有的view更新操作都會調(diào)用view的invalidate方法慕购!那么問題就在這里了
/**
* Check whether entirely new text requires a new view layout
* or merely a new text layout.
*/
private void checkForRelayout() {
//注意看這里
invalidate();
} else {
//注意看這里
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);
}
好 那么我們就看下invalidate(true)方法看看到底是哪的問題
* This is where the invalidate() work actually happens. A full invalidate()
* causes the drawing cache to be invalidated, but this function can be
* called with invalidateCache set to false to skip that invalidation step
* for cases that do not need it (for example, a component that remains at
* the same dimensions with the same content)
.
*
* @param invalidateCache Whether the drawing cache for this view should be
* invalidated as well. This is usually true for a full
* invalidate, but may be set to false if the View's contents or
* dimensions have not changed.
*/
void invalidate(boolean invalidateCache)
{
//可能不同版本的api源碼不太一樣,這里直接看invalidateInternal方法茬底。
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,boolean fullInvalidate) {
//我們關(guān)心的是這里 ViewParent沪悲,這個ViewPrent是一個接口,而ViewGroup與ViewRootImpl實現(xiàn)了它阱表,有耐心的朋友可以在View這個類中去查找mPrent相關(guān)的一些信息殿如,如果細心的朋友在ViewGroup類中會找到ViewRootImpl這個類在它里邊的一些操作如setDragFocus等等贡珊。
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
//程序在這里檢查是不是在UI線程中做操作
p.invalidateChild(this, damage);
}
}
}
這個ViewRootImpl我們可能在Eclipse或者Android Studio中我們無法查看,大家可以使用Source Insight 去查看源碼:打開Source Insight 打開ViewRootImpl類涉馁,找到 invalidateChild這個方法
public void invalidateChild(View child, Rect dirty) {
//關(guān)鍵的地方就是這個方法
checkThread();
//后邊的全部省略
}
void checkThread() {
//在這里mThread表示的是主線程门岔,程序作了判斷,檢查當前線程是不是主線程烤送,如果不是就會拋出異常
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
看到上邊方法中拋出的異常是不是感覺很熟悉寒随,對,沒錯帮坚,就是我log中截出來的那句話F尥!那么我們現(xiàn)在懵逼了试和,為什么我們在不讓子線程休眠的情況下去更新TextView文本可以讯泣,而讓線程休眠兩秒后就出拋異常呢?根本原因就是ViewRootImpl到底是在哪里被初始化的阅悍!ViewRootImpl是在onResume中初始化的好渠,而我們開啟的子線程是在onCreat方法中,這個時候程序沒有去檢測當前線程是不是主線程节视,所以沒有拋異常H!下邊我們?nèi)タ碅ctivityThread源碼肴茄,去找出原因I纬!
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) {
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
//ViewPrent實現(xiàn)類
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
//問題的根源在這里寡痰,addView方法在這里對ViewRootImpl進行初始化,大家可以去看看ViewGroup的源碼棋凳,找里邊的addView方法拦坠,你會發(fā)現(xiàn),最后又回到View的invalidate(true)方法;
wm.addView(decor, l);
}
}
}