Android中為什么在子線程不能更新UI

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)容欄毙籽。

Paste_Image.png

仔細(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)資料。

參考資料:http://blog.csdn.net/xyh269/article/details/52728861s

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末沛膳,一起剝皮案震驚了整個(gè)濱河市扔枫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌锹安,老刑警劉巖短荐,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異叹哭,居然都是意外死亡忍宋,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門风罩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來讶踪,“玉大人,你說我怎么就攤上這事泊交∪榧ィ” “怎么了?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵廓俭,是天一觀的道長(zhǎng)云石。 經(jīng)常有香客問我,道長(zhǎng)研乒,這世上最難降的妖魔是什么汹忠? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上宽菜,老公的妹妹穿的比我還像新娘谣膳。我一直安慰自己铅乡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布花履。 她就那樣靜靜地躺著,像睡著了一般挚赊。 火紅的嫁衣襯著肌膚如雪诡壁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天妹卿,我揣著相機(jī)與錄音蔑鹦,去河邊找鬼纽帖。 笑死,一個(gè)胖子當(dāng)著我的面吹牛举反,可吹牛的內(nèi)容都是我干的懊直。 我是一名探鬼主播火鼻,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼魁索!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起尝偎,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤鹏控,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后当辐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡耍群,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年蹈垢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片曹抬。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡沐祷,死狀恐怖攒岛,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情灾锯,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布吵聪,位于F島的核電站兼雄,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏赦肋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一囱井、第九天 我趴在偏房一處隱蔽的房頂上張望趣避。 院中可真熱鬧,春花似錦程帕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽顺呕。三九已至括饶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間图焰,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工僵闯, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留藤滥,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓向图,卻偏偏與公主長(zhǎng)得像标沪,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子金句,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容