從源碼的角度來(lái)分析子線程真的不能更新UI嗎?

我們初學(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é)果:

子線程更新UI.png

居然沒(méi)有報(bào)錯(cuò)豺瘤,TextView也顯示出來(lái)為我們?cè)O(shè)置的值吆倦。驚不驚喜?意不意外坐求?

1.jpg

分析

子線程為啥不能更新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)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市陈轿,隨后出現(xiàn)的幾起案子圈纺,更是在濱河造成了極大的恐慌,老刑警劉巖济欢,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赠堵,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡法褥,警方通過(guò)查閱死者的電腦和手機(jī)茫叭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)半等,“玉大人揍愁,你說(shuō)我怎么就攤上這事∩倍” “怎么了莽囤?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)切距。 經(jīng)常有香客問(wèn)我朽缎,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任话肖,我火速辦了婚禮北秽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘最筒。我一直安慰自己贺氓,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布床蜘。 她就那樣靜靜地躺著辙培,像睡著了一般。 火紅的嫁衣襯著肌膚如雪邢锯。 梳的紋絲不亂的頭發(fā)上扬蕊,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音弹囚,去河邊找鬼厨相。 笑死领曼,一個(gè)胖子當(dāng)著我的面吹牛鸥鹉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播庶骄,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼毁渗,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了单刁?” 一聲冷哼從身側(cè)響起灸异,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎羔飞,沒(méi)想到半個(gè)月后肺樟,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡逻淌,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年么伯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卡儒。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡田柔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出骨望,到底是詐尸還是另有隱情硬爆,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布擎鸠,位于F島的核電站缀磕,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜袜蚕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一准验、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧廷没,春花似錦糊饱、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至狭归,卻和暖如春夭坪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背过椎。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工室梅, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人疚宇。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓亡鼠,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親敷待。 傳聞我的和親對(duì)象是個(gè)殘疾皇子间涵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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

  • 問(wèn)題描述 做過(guò)android開(kāi)發(fā)基本都遇見(jiàn)過(guò) ViewRootImpl$CalledFromWrongThread...
    Shawn_Dut閱讀 6,479評(píng)論 0 5
  • Android有條鐵則:子線程不能修改UI 至于為什么 ,就是修改了會(huì)報(bào)錯(cuò)唄 具體錯(cuò)誤如下 但是令人稱奇的是:如果...
    埃賽爾閱讀 7,546評(píng)論 2 14
  • 寫(xiě)過(guò)Android 代碼的同學(xué)應(yīng)該都聽(tīng)過(guò)Android不能在子線程更新UI,只能在主線程即UI線程處理視圖。 猜一...
    acemurder閱讀 5,471評(píng)論 5 36
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,504評(píng)論 25 707
  • 晴举哟,漢中市思劳。 早上還在家里,到中午時(shí)已經(jīng)折返回了漢中市妨猩。下午三點(diǎn)半上班潜叛。 晚上下班時(shí)剛好看到朋友圈里女神的直播鏈接...
    MAY阿鹿閱讀 99評(píng)論 0 1