我們初學(xué)Android的時(shí)候就知道“子線程里面是不能直接更新UI的”气笙,那么真的是如此嗎?我們來(lái)看下面這段代碼
public class MainActivity extends AppCompatActivity {
TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = (TextView) findViewById(R.id.tv);
new Thread(new Runnable() {
@Override
public void run() {
tv.setText("我是子線程狡门,我更新UI了");
}
}).start();
}
}
布局文件很簡(jiǎn)單险掀,里面就只有一個(gè)TextView亡哄。這段代碼在子線程里面更新了UI象颖,理論上應(yīng)該是會(huì)報(bào)錯(cuò)的。讓我們來(lái)看一看實(shí)際的運(yùn)行結(jié)果:
居然沒(méi)有報(bào)錯(cuò)豺瘤,TextView也顯示出來(lái)為我們?cè)O(shè)置的值吆倦。驚不驚喜?意不意外坐求?
分析
子線程為啥不能更新UI?
子線程不能更新UI蚕泽,這句基本上沒(méi)錯(cuò)的,但也有例外情況桥嗤,我們上面這個(gè)例子就是须妻。不過(guò)我們還得先分析下在這絕大多數(shù)情況下仔蝌,子線程為啥不能更新UI,然后再來(lái)分析在這極少數(shù)例外的情況下荒吏,子線程為啥可以更新UI
1敛惊、設(shè)計(jì)層面上
Android系統(tǒng)為啥不允許子線程中更新UI呢?這是因?yàn)锳ndroid的UI控件不是線程安全的。如果多線程中并發(fā)訪問(wèn)可能會(huì)導(dǎo)致UI控件處于不可預(yù)期的狀態(tài)司倚。你可能會(huì)問(wèn)為啥不對(duì)UI控件加鎖呢?因?yàn)榧渔i會(huì)導(dǎo)致UI訪問(wèn)的邏輯變得復(fù)雜豆混,同時(shí)會(huì)降低UI訪問(wèn)的效率篓像,鎖機(jī)制會(huì)阻塞某些線程的執(zhí)行动知。鑒于以上問(wèn)題。最簡(jiǎn)單高效的辦法就是采用單線程來(lái)處理UI操作员辩,我們只需要用Handler來(lái)切換一下線程就可以了盒粮。
2、代碼層面上
下面這段代碼是ViewRootImpl的一個(gè)方法奠滑,其實(shí)我們?cè)谡{(diào)用UI控件
setText等更新UI的方法時(shí)丹皱,會(huì)調(diào)用到ViewRootImpl的這個(gè)方法,這個(gè)方法就是去檢查當(dāng)前線程是不是主線程(mThread是主線程)宋税,只有那么幾行代碼而已的摊崭,如果當(dāng)前線程不是主線程,就會(huì)拋出異常杰赛。這也就是子線程不能更新UI的代碼層面上的原因?
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
然而我們?cè)谧泳€程更新UI為什么不報(bào)錯(cuò)呢簸?
那么出現(xiàn)上面子線程更新UI居然不報(bào)錯(cuò)的原因是啥呢?不是說(shuō)子線程不能更新UI嗎?當(dāng)訪問(wèn)UI時(shí),ViewRootImpl會(huì)調(diào)用checkThread方法去檢查當(dāng)前訪問(wèn)UI的線程是哪個(gè)乏屯,如果不是UI線程則會(huì)拋出異常根时,這是沒(méi)問(wèn)題的。但是為什么一開(kāi)始在MainActivity的onCreate方法中創(chuàng)建一個(gè)子線程訪問(wèn)UI辰晕,程序還是正常能跑起來(lái)呢蛤迎?唯一的解釋就是執(zhí)行onCreate方法的那個(gè)時(shí)候ViewRootImpl還沒(méi)創(chuàng)建,無(wú)法去檢查當(dāng)前線程含友。有了這個(gè)想法替裆,那么我們就去驗(yàn)證下,這個(gè)想法是否正確窘问,怎么驗(yàn)證呢?當(dāng)然是看ViewRootImpl實(shí)在何處創(chuàng)建的辆童。
我們可以找到ActivityThread的handleResumeActivity方法
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
// 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
ActivityClientRecord r = performResumeActivity(token, clearHide);
if (r != null) {
final Activity a = r.activity;
if (localLOGV) Slog.v(
TAG, "Resume " + r + " started activity: " +
a.mStartedActivity + ", hideForNow: " + r.hideForNow
+ ", finished: " + a.mFinished);
final int forwardBit = isForward ?
WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
// If the window hasn't yet been added to the window manager,
// and this guy didn't finish itself or start another activity,
// then go ahead and add the window.
boolean willBeVisible = !a.mStartedActivity;
if (!willBeVisible) {
try {
willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible(
a.getActivityToken());
} catch (RemoteException e) {
}
}
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 (a.mVisibleFromClient) {
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;
}
// Get rid of anything left hanging around.
cleanUpPendingRemoveWindows(r);
// The window is now visible if it has been added, we are not
// simply finishing, and we are not starting another activity.
if (!r.activity.mFinished && willBeVisible
&& r.activity.mDecor != null && !r.hideForNow) {
if (r.newConfig != null) {
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Resuming activity "
+ r.activityInfo.name + " with newConfig " + r.newConfig);
performConfigurationChanged(r.activity, r.newConfig);
freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.newConfig));
r.newConfig = null;
}
if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward="
+ isForward);
WindowManager.LayoutParams l = r.window.getAttributes();
if ((l.softInputMode
& WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
!= forwardBit) {
l.softInputMode = (l.softInputMode
& (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
| forwardBit;
if (r.activity.mVisibleFromClient) {
ViewManager wm = a.getWindowManager();
View decor = r.window.getDecorView();
wm.updateViewLayout(decor, l);
}
}
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();//第一處
}
}
if (!r.onlyLocalRequest) {
r.nextIdle = mNewActivities;
mNewActivities = r;
if (localLOGV) Slog.v(
TAG, "Scheduling idle handler for " + r);
Looper.myQueue().addIdleHandler(new Idler());
}
r.onlyLocalRequest = false;
// Tell the activity manager we have resumed.
if (reallyResume) {
try {
ActivityManagerNative.getDefault().activityResumed(token);
} catch (RemoteException ex) {
}
}
} else {
// If an exception was thrown when trying to resume, then
// just end this activity.
try {
ActivityManagerNative.getDefault()
.finishActivity(token, Activity.RESULT_CANCELED, null, false);
} catch (RemoteException ex) {
}
}
}
其實(shí)這段代碼中會(huì)調(diào)Activity的onResume方法,從方法名上也可以看出來(lái)南缓。這段代碼很長(zhǎng)胸遇,我們只需要看到我標(biāo)注為第一處的那段代碼。也就是下面這句汉形。
r.activity.makeVisible();
我們跟進(jìn)去看看
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
ViewManager中添加DecorView纸镊,那現(xiàn)在應(yīng)該關(guān)注的就是ViewManager的addView方法了倍阐。而ViewManager是一個(gè)接口來(lái)的,我們應(yīng)該找到ViewManager的實(shí)現(xiàn)類才行逗威,而ViewManager的實(shí)現(xiàn)類是WindowManagerImpl峰搪。
找到了WindowManagerImpl的addView方法,如下:
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);//第三行
}
然后第三行又調(diào)用了WindowManagerGlobal的addView方法凯旭,咱們繼續(xù)跟進(jìn)去看看.
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
……省略代碼
ViewRootImpl root;//第1處
View panelParentView = null;
……省略代碼
root = new ViewRootImpl(view.getContext(), display);//第2處
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
……省略代碼
}
看到第1,2處的代碼木有概耻,ViewRootImpl在這里創(chuàng)建了。也就是說(shuō)ViewRootImpl實(shí)在Activity的onResume方法之后才調(diào)用的罐呼。onCreate方法時(shí)ViewRootImpl還沒(méi)有創(chuàng)建鞠柄,自然沒(méi)辦法檢查線程,也就不會(huì)報(bào)錯(cuò)嫉柴。
再次嘗試
既然知道了結(jié)論厌杜,那么我們?cè)僭囈幌拢@次我們?cè)诟耈I之前先sleep500毫秒计螺,讓它有時(shí)間執(zhí)行到onResume夯尽,創(chuàng)建ViewRootImpl。代碼如下:
public class MainActivity extends AppCompatActivity {
TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = (TextView) findViewById(R.id.tv);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(500);
tv.setText("我是子線程登馒,我更新UI了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
結(jié)果如下匙握,正如我們所想報(bào)錯(cuò)了
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7357)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1099)