Android有條鐵則:子線程不能修改UI 至于為什么 闻镶,就是修改了會(huì)報(bào)錯(cuò)唄
@Override
protected voidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
newThread(newRunnable() {
@Override
public voidrun() {
try{
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
((TextView) findViewById(R.id.tv_hello)).setText("longlong");
findViewById(R.id.tv_hello).setOnClickListener(MainActivity.this);
}
}).start();
}
具體錯(cuò)誤如下
3-23 14:24:59.871 24916-24991/com.example.exile.exiledemo E/AndroidRuntime: FATAL EXCEPTION: Thread-2857
Process: com.example.exile.exiledemo, PID: 24916
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8358)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1327)
at android.view.View.requestLayout(View.java:20170)
···
at java.lang.Thread.run(Thread.java:818)
但是令人稱奇的是:如果我把sleep去掉 竟然是可以更新的并不會(huì)報(bào)錯(cuò)甚脉,這就勾起了我的好奇心,我想進(jìn)一步深入源碼的認(rèn)識(shí)下一到底為什么這時(shí)候可以更新铆农,為甚延時(shí)之后又不能更新了牺氨,
為什么修改UI會(huì)報(bào)錯(cuò):(在哪里檢查的報(bào)錯(cuò))
為什么沒有延時(shí)的時(shí)候在子線程又可以修改了:(什么時(shí)候開始執(zhí)行了檢查方法)
為什么延時(shí)后修改又報(bào)錯(cuò)了:(在什么時(shí)候生效了)
Android這樣不允許在子線程修改UI有什么考慮,和好處:
1:
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8358)
報(bào)錯(cuò)在這里:
這個(gè)異常是從android.view.ViewRootImpl的checkThread方法拋出的墩剖。
這里順便鋪墊一個(gè)知識(shí)點(diǎn):
ViewRootImpl猴凹,ViewRoot和View
ViewRoot對(duì)應(yīng)ViewViewRootImpl類,它是連接WindowManager和DecorView的紐帶岭皂,view的三大流程(measure,layout,draw)均是通過ViewRoot來完成的郊霎,ActivityThread中,當(dāng)Activity對(duì)象被創(chuàng)建完畢后爷绘,會(huì)將DecorView添加到Window中书劝,同時(shí)創(chuàng)建ViewRootImpl對(duì)象并和DecorView建立聯(lián)系。DecorView作為頂級(jí)的View一般情況下它內(nèi)部會(huì)包含一個(gè)豎向的LinearLayout揉阎,這個(gè)LinearLayout里面有上下兩個(gè)部分(具體情況和Android版本及主題有關(guān))庄撮,上面是標(biāo)題欄背捌,下面是內(nèi)容欄毙籽。
仔細(xì)看這個(gè)方法:
void checkThread() {
if(mThread!= Thread.currentThread()) {
throw newCalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
這句異常寫在這里 但是值得注意的是 這里只是把當(dāng)前的線程和ViewRootImpl創(chuàng)建的線程進(jìn)行了對(duì)比,意思很明顯ViewRootImpl創(chuàng)建在哪個(gè)線程中之后的更新UI操作就要在哪個(gè)線程毡庆。只是通常情況下坑赡,它是在UI線程中被創(chuàng)建烙如。
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1327)
@Override
public voidrequestLayout() {
if(!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested=true;
scheduleTraversals();
}
}
at android.view.View.requestLayout(View.java:20170)
是因?yàn)関iew調(diào)用了requestLayout的方法導(dǎo)致了
@CallSuper
public voidrequestLayout() {
if(mMeasureCache!=null)mMeasureCache.clear();
if(mAttachInfo!=null&&mAttachInfo.mViewRequestingLayout==null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
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()) {
mParent.requestLayout();
}
if(mAttachInfo!=null&&mAttachInfo.mViewRequestingLayout==this) {
mAttachInfo.mViewRequestingLayout=null;
}
}
這里是子view調(diào)用了parentView的requestLayout方法
重點(diǎn)來了 既然知道 viewrootimpl是viewroot的實(shí)現(xiàn)類,這就說明viewrootimpl其實(shí)是最頂層view的實(shí)現(xiàn)類(但是ViewRootImpl并不是View而是DecorView的Parent引用) 并且 viewrootimpl實(shí)現(xiàn)了ViewParent接口
/**
* The top of a view hierarchy(view的頂層), implementing the needed protocol between View
* and the WindowManager. This is for the most part an internal implementation
* detail of {@linkWindowManagerGlobal}.
*
* {@hide}
*/
@SuppressWarnings({"EmptyCatchBlock","PointlessBooleanExpression"})
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks,ThreadedRenderer.HardwareDrawCallbacks {
這就說明最終會(huì)被調(diào)用的其實(shí)就是
ViewRootImpl的requestLayout方法毅否,當(dāng)然啊錯(cuò)誤日志也給出了這樣的關(guān)系亚铁,
現(xiàn)在讓我們仔細(xì)看ViewRootImpl的requestLayout方法
@Override
public voidrequestLayout() {
if(!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested=true;
scheduleTraversals();
}
}
每次requestlayout的時(shí)候 都會(huì)調(diào)用checkThread方法,來檢查線程
先看下requestLayout都干了什么吧螟加,1 檢查線程徘溢,2 scheduleTraversals();
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
注意到postCallback方法的的第二個(gè)參數(shù)傳入了很像是一個(gè)后臺(tái)任務(wù)捆探。那再點(diǎn)進(jìn)去
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
找到了然爆,那么繼續(xù)跟進(jìn)doTraversal()方法。
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
可以看到里面調(diào)用了一個(gè)performTraversals()方法黍图,View的繪制過程就是從這個(gè)performTraversals方法開始的曾雕。PerformTraversals方法的代碼有點(diǎn)長(zhǎng)就不貼出來了,如果繼續(xù)跟進(jìn)去就是學(xué)習(xí)View的繪制了助被。而我們現(xiàn)在知道了剖张,每一次訪問了UI,Android都會(huì)重新繪制View揩环。這個(gè)是很好理解的搔弄。
分析到了這里,其實(shí)異常信息對(duì)我們幫助也不大了丰滑,它只告訴了我們子線程中訪問UI在哪里拋出異常肯污。
而我們會(huì)思考:當(dāng)訪問UI時(shí),ViewRoot會(huì)調(diào)用checkThread方法去檢查當(dāng)前訪問UI的線程是哪個(gè)吨枉,如果不是UI線程則會(huì)拋出異常蹦渣,這是沒問題的。但是為什么一開始在MainActivity的onCreate方法中創(chuàng)建一個(gè)子線程訪問UI貌亭,程序還是正常能跑起來呢柬唯??
唯一的解釋就是執(zhí)行onCreate方法的那個(gè)時(shí)候ViewRootImpl還沒創(chuàng)建圃庭,無法去檢查當(dāng)前線程锄奢。
那么就可以這樣深入進(jìn)去。尋找ViewRootImpl是在哪里剧腻,是什么時(shí)候創(chuàng)建的拘央。好,繼續(xù)前進(jìn)
這里省略Avtivity的創(chuàng)建過程(在每個(gè)生命周期都做了什么书在,生命周期怎么被調(diào)用的灰伟,Activity是怎么被創(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;
//代碼省略
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}
//代碼省略
}
可以看到內(nèi)部調(diào)用了performResumeActivity方法栏账,這個(gè)方法看名字肯定是回調(diào)onResume方法的入口的帖族,那么我們還是跟進(jìn)去瞧瞧。
public final ActivityClientRecord performResumeActivity(IBinder token,
boolean clearHide) {
ActivityClientRecord r = mActivities.get(token);
if (localLOGV) Slog.v(TAG, "Performing resume of " + r
+ " finished=" + r.activity.mFinished);
if (r != null && !r.activity.mFinished) {
//代碼省略
r.activity.performResume();
//代碼省略
return r;
}
可以看到r.activity.performResume()這行代碼挡爵,跟進(jìn) performResume方法竖般,如下:
final void performResume() {
performRestart();
mFragments.execPendingActions();
mLastNonConfigurationInstances = null;
mCalled = false;
// mResumed is set by the instrumentation
mInstrumentation.callActivityOnResume(this);
//代碼省略
}
Instrumentation調(diào)用了callActivityOnResume方法,callActivityOnResume源碼如下:
public void callActivityOnResume(Activity activity) {
activity.mResumed = true;
activity.onResume();
if (mActivityMonitors != null) {
synchronized (mSync) {
final int N = mActivityMonitors.size();
for (int i=0; i
final ActivityMonitor am = mActivityMonitors.get(i);
am.match(activity, activity, activity.getIntent());
}
}
}
}
找到了茶鹃,activity.onResume()涣雕。這也證實(shí)了,performResumeActivity方法確實(shí)是回調(diào)onResume方法的入口闭翩。
那么現(xiàn)在我們看回來handleResumeActivity方法胞谭,執(zhí)行完performResumeActivity方法回調(diào)了onResume方法后,
會(huì)來到這一塊代碼:
r.activity.mVisibleFromServer =true;
mNumVisibleActivities++;
if(r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
activity調(diào)用了makeVisible方法男杈,這應(yīng)該是讓什么顯示的吧丈屹,跟進(jìn)去探探。
voidmakeVisible() {
if(!mWindowAdded) {
ViewManager wm=getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded=true;
}
mDecor.setVisibility(View.VISIBLE);
}
往WindowManager中添加DecorView伶棒,那現(xiàn)在應(yīng)該關(guān)注的就是WindowManager的addView方法了旺垒。而WindowManager是一個(gè)接口來的,我們應(yīng)該找到WindowManager的實(shí)現(xiàn)類才行肤无,而WindowManager的實(shí)現(xiàn)類是WindowManagerImpl先蒋。這個(gè)和ViewRoot是一樣,就是名字多了個(gè)impl宛渐。
找到了WindowManagerImpl的addView方法竞漾,如下:
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
里面調(diào)用了WindowManagerGlobal的addView方法,那現(xiàn)在就鎖定
WindowManagerGlobal的addView方法:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
//代碼省略
ViewRootImpl root;
View panelParentView = null;
//代碼省略
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.
synchronized (mLock) {
final int index = findViewLocked(view, false);
if (index >= 0) {
removeViewLocked(index, true);
}
}
throw e;
}
}
終于擊破窥翩,ViewRootImpl是在WindowManagerGlobal的addView方法中創(chuàng)建的业岁。
回顧前面的分析,總結(jié)一下:
ViewRootImpl的創(chuàng)建在onResume方法回調(diào)之后寇蚊,而我們一開篇是在onCreate方法中創(chuàng)建了子線程并訪問UI笔时,在那個(gè)時(shí)刻,ViewRootImpl是沒有創(chuàng)建的仗岸,無法檢測(cè)當(dāng)前線程是否是UI線程允耿,所以程序沒有崩潰一樣能跑起來,而之后修改了程序扒怖,讓線程休眠了200毫秒后较锡,程序就崩了。很明顯200毫秒后ViewRootImpl已經(jīng)創(chuàng)建了盗痒,可以執(zhí)行checkThread方法檢查當(dāng)前線程蚂蕴。
4策略和考慮
首先UI線程(mainThread)并不是線程安全的,這樣如果子線程修改UI容易數(shù)據(jù)錯(cuò)亂,如果做到線程安全的話掂墓,這樣做是很低效的。
其次谷歌推薦如果子線程需要修改UI可以使用handler看成,這樣的隊(duì)列設(shè)也是考慮到并發(fā)君编,效率的體現(xiàn)。
為什么 android 會(huì)設(shè)計(jì)成只有創(chuàng)建 ViewRootImpl 的原始線程才能更改 ui 呢川慌?這就要說到 Android 的單線程模型了,因?yàn)槿绻С侄嗑€程修改 View 的話梦重,由此產(chǎn)生的線程同步和線程安全問題將是非常繁瑣的兑燥,所以 Android 直接就定死了,View 的操作必須在創(chuàng)建它的 UI 線程琴拧,從而簡(jiǎn)化了系統(tǒng)設(shè)計(jì)降瞳。
有沒有可以在其他非原始線程更新 ui 的情況呢?有蚓胸,SurfaceView 就可以在其他線程更新挣饥,具體的大家可以去網(wǎng)上了解一下相關(guān)資料。