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

1湾宙、前言

  • 眾所周知在Android中妒御,子線程是不能更新UI的解愤;
  • 那么我在想,為什么不能乎莉,會產(chǎn)生什么問題送讲;
  • 是否真的就一定不能在子線程更新UI;

2、能否在子線程中更新UI

答案是可以的惋啃,比如以下代碼:

@Override
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = findViewById(R.id.tv);
        new Thread(new Runnable() {
            @Override
            public void run() {
                tv.setText("測試是否報出異常");
            }
        }).start();
    }

運行結(jié)果并無異常哼鬓,可以正常的在子線程中更新了TextView控件;假如讓線程休眠1000ms,就會發(fā)生錯誤:

Only the original thread that created a view hierarchy can touch its views.

這句話的意思是只有創(chuàng)建視圖層次結(jié)構(gòu)的原始線程才能更新這個視圖,也就是說只有主線程才有權(quán)力去更新UI肥橙,其他線程會直接拋異常的;
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7905)的異常路徑可以看到拋出異常的最終在ViewRootImlcheckThread方法里魄宏,ViewRootImlView的根類,其控制著View的測量存筏、繪制等操作宠互,那么現(xiàn)在我們轉(zhuǎn)到ViewRootImpl.java源碼觀察:

@Override
public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
}

void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

scheduleTraversals()里是對View進行繪制操作,而在繪制之前都會檢查當前線程是否為主線程mThread椭坚,如果不是主線程予跌,就拋出異常;這樣做法就限制了開發(fā)者在子線程中更新UI的操作善茎;
但是為什么最開始的在onCreate()里子線程對UI的操作沒有報錯呢券册,可以設想一下是因為ViewRootImp此時還沒有創(chuàng)建,還未進行當前線程的判斷垂涯;
現(xiàn)在烁焙,我們尋找ViewRootImp在何時創(chuàng)建,從Activity啟動過程中尋找源碼,通過分析可以查看ActivityThread.java源碼,并找到handleResumeActivity方法:

final void handleResumeActivity(IBinder token,boolean clearHide, boolean isForward, boolean reallyResume) {
        ···
        // TODO Push resumeArgs into the activity for consideration
        ActivityClientRecord r = performResumeActivity(token, clearHide);
        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);
                }

            } else if (!willBeVisible) {
                if (localLOGV) Slog.v(
                    TAG, "Launch " + r + " mStartedActivity set");
                r.hideForNow = true;
            }
        ···
}

內(nèi)部調(diào)用了performResumeActivity方法:
···
public final ActivityClientRecord performResumeActivity(IBinder token,
boolean clearHide) {
if (r != null && !r.activity.mFinished) { r.activity.performResume(); } catch (Exception e) { if (!mInstrumentation.onException(r.activity, e)) { throw new RuntimeException( "Unable to resume activity " + r.intent.getComponent().toShortString() + ": " + e.toString(), e); } } } return r; } ··· 在內(nèi)部調(diào)用了ActivityperformResume方法耕赘,可以肯定應該是要回調(diào)生命周期的onResume```方法了:

final void performResume() {
        ···
        mCalled = false;
        // mResumed is set by the instrumentation
        mInstrumentation.callActivityOnResume(this);
        if (!mCalled) {
            throw new SuperNotCalledException(
                "Activity " + mComponent.toShortString() +
                " did not call through to super.onResume()");
        }
        ···
    }

骄蝇,然后又調(diào)用了InstrumentationcallActivityOnResume方法:

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<N; i++) {
                    final ActivityMonitor am = mActivityMonitors.get(i);
                    am.match(activity, activity, activity.getIntent());
                }
            }
        }
    }

可以看到執(zhí)行了 activity.onResume()方法,也就是回調(diào)了Activity生命周期的onResume方法;現(xiàn)在讓我們回頭看看handleResumeActivity方法操骡,會執(zhí)行這段代碼:

···
r.activity.mVisibleFromServer = true;
                mNumVisibleActivities++;
                if (r.activity.mVisibleFromClient) {
                    r.activity.makeVisible();
                }

在內(nèi)部調(diào)用了ActivitymakeVisible方法:

void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

內(nèi)部調(diào)用了WindowManageraddView方法九火,而WindowManager方法的實現(xiàn)類是WindowManagerImp類赚窃,直接找WindowManagerImpaddView方法:

    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }

然后又調(diào)用了WindowManagerGlobaladdView方法:

public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {          
        ···
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }
        ···
    }

在該方法中,終于看到了ViewRootImpl的創(chuàng)建岔激;

結(jié)論:從以上的源碼分析可得知勒极,ViewRootImpl對象是在onResume方法回調(diào)之后才創(chuàng)建,那么就說明了為什么在生命周期的onCreate方法里虑鼎,甚至是onResume方法里都可以實現(xiàn)子線程更新UI辱匿,因為此時還沒有創(chuàng)建ViewRootImpl對象,并不會進行是否為主線程的判斷震叙;


3掀鹅、更新UI一定要在主線程實現(xiàn)

谷歌提出:“一定要在主線程更新UI”,實際是為了提高界面的效率和安全性媒楼,帶來更好的流暢性乐尊;反推一下,假如允許多線程更新UI划址,但是訪問UI是沒有加鎖的扔嵌,一旦多線程搶占了資源,那么界面將會亂套更新了夺颤,體驗效果就不言而喻了痢缎;所以在Android中規(guī)定必須在主線程更新UI


4世澜、總結(jié)

  • 子線程可以在ViewRootImpl還沒有被創(chuàng)建之前更新UI独旷;
  • 訪問UI是沒有加對象鎖的,在子線程環(huán)境下更新UI寥裂,會造成不可預期的風險嵌洼;
  • 開發(fā)者更新UI一定要在主線程進行操作;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市封恰,隨后出現(xiàn)的幾起案子麻养,更是在濱河造成了極大的恐慌,老刑警劉巖诺舔,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鳖昌,死亡現(xiàn)場離奇詭異,居然都是意外死亡低飒,警方通過查閱死者的電腦和手機许昨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來褥赊,“玉大人车要,你說我怎么就攤上這事≌柑龋” “怎么了翼岁?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長司光。 經(jīng)常有香客問我琅坡,道長,這世上最難降的妖魔是什么残家? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任榆俺,我火速辦了婚禮,結(jié)果婚禮上坞淮,老公的妹妹穿的比我還像新娘茴晋。我一直安慰自己,他們只是感情好回窘,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布诺擅。 她就那樣靜靜地躺著,像睡著了一般啡直。 火紅的嫁衣襯著肌膚如雪烁涌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天酒觅,我揣著相機與錄音撮执,去河邊找鬼。 笑死舷丹,一個胖子當著我的面吹牛抒钱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播颜凯,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼谋币,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了装获?” 一聲冷哼從身側(cè)響起瑞信,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎穴豫,沒想到半個月后凡简,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡精肃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年秤涩,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片司抱。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡筐眷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出习柠,到底是詐尸還是另有隱情匀谣,我是刑警寧澤照棋,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站武翎,受9級特大地震影響烈炭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜宝恶,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一符隙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧垫毙,春花似錦霹疫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至毫痕,卻和暖如春征峦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背消请。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工栏笆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人臊泰。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓蛉加,卻偏偏與公主長得像,于是被迫代替她去往敵國和親缸逃。 傳聞我的和親對象是個殘疾皇子针饥,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355