從源碼角度分析 - Activity.onCreate可以在子線程里更新UI么压语?

我們都知道字線程里更新不能更新UI格仲,否則系統(tǒng)會(huì)報(bào)Only the original thread that created a view hierarchy can touch its views.錯(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:7313)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1161)
        at android.view.View.requestLayout(View.java:21995)
        at android.view.View.requestLayout(View.java:21995)
        at android.view.View.requestLayout(View.java:21995)
        at android.view.View.requestLayout(View.java:21995)
        at android.view.View.requestLayout(View.java:21995)
        at android.view.View.requestLayout(View.java:21995)
        at android.view.View.requestLayout(View.java:21995)
        at androidx.recyclerview.widget.RecyclerView.requestLayout(RecyclerView.java:4202)
        at androidx.recyclerview.widget.RecyclerView$RecyclerViewDataObserver.onChanged(RecyclerView.java:5286)
        at androidx.recyclerview.widget.RecyclerView$AdapterDataObservable.notifyChanged(RecyclerView.java:11997)
        at androidx.recyclerview.widget.RecyclerView$Adapter.notifyDataSetChanged(RecyclerView.java:7070)
        at com.github.jokar.wechat_moments.view.adapter.MomentsAdapter.submitList(MomentsAdapter.java:71)
        at com.github.jokar.wechat_moments.view.MainActivity$1.run(MainActivity.java:51)

那么Activity.onCreate可以在字線程里更新UI么财著?联四,答案是可以的。但是不是全部可以撑教,如果子線程是立馬執(zhí)行的可以朝墩,若休眠了一定時(shí)間后就不可以了。 這是為什么呢伟姐?


為什么會(huì)報(bào)Only the original thread that created a view hierarchy can touch its views.錯(cuò)誤收苏?

首先我們要搞懂一個(gè)問題就是為什么會(huì)報(bào)Only the original thread that created a view hierarchy can touch its views.錯(cuò)誤?

從上面錯(cuò)誤信息堆椃弑可以看到是ViewRootImpl.requestLayout()方法里調(diào)用的checkThread里爆出了這個(gè)錯(cuò)誤:

    @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.");
        }
    }

這里就可以看到具體檢查報(bào)錯(cuò)的是在ViewRootImpl.requestLayout()方法里鹿霸,但是這個(gè)ViewRootImpl是啥?為什么我們更新view會(huì)到這里秆乳?這里就要說到了requestLayout() 方法了懦鼠。

requestLayout()

(1)如果我們修改了一個(gè) View,如果修改結(jié)果影響了它的尺寸,那么就會(huì)觸發(fā)這個(gè)方法屹堰。(2) 從方法名字可以知道肛冶,“請求布局”,那就是說扯键,如果調(diào)用了這個(gè)方法睦袖,那么對于一個(gè)子View來說,應(yīng)該會(huì)重新進(jìn)行布局流程忧陪。但是扣泊,真實(shí)情況略有不同,如果子View調(diào)用了這個(gè)方法嘶摊,其實(shí)會(huì)從View樹重新進(jìn)行一次測量延蟹、布局、繪制這三個(gè)流程叶堆,最終就會(huì)顯示子View的最終情況阱飘。

源碼分析View#requestLayout

    public void requestLayout() {
        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;
        }

        //為當(dāng)前view設(shè)置標(biāo)記位 PFLAG_FORCE_LAYOUT
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            //向父容器請求布局
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }
  • requestLayout方法中,首先先判斷當(dāng)前View樹是否正在布局流程虱颗,接著為當(dāng)前子View設(shè)置標(biāo)記位沥匈,該標(biāo)記位的作用就是標(biāo)記了當(dāng)前的View是需要進(jìn)行重新布局的
  • 接著調(diào)用mParent.requestLayout方法,這個(gè)十分重要忘渔,因?yàn)檫@里是向父容器請求布局高帖,即調(diào)用父容器的requestLayout方法,為父容器添加PFLAG_FORCE_LAYOUT標(biāo)記位畦粮,而父容器又會(huì)調(diào)用它的父容器的requestLayout方法散址,即requestLayout事件層層向上傳遞乖阵,直到DecorView,即根View
  • 而根View又會(huì)傳遞給ViewRootImpl预麸,也即是說子ViewrequestLayout事件瞪浸,最終會(huì)被ViewRootImpl接收并得到處理

縱觀這個(gè)向上傳遞的流程,其實(shí)是采用了責(zé)任鏈模式吏祸,即不斷向上傳遞該事件对蒲,直到找到能處理該事件的上級(jí),在這里贡翘,只有ViewRootImpl能夠處理requestLayout事件蹈矮。到這里我們就明白了為什么當(dāng)更新View的時(shí)候如果觸發(fā)了requestLayout方法為什么會(huì)到ViewRootImpl.requestLayout()處理。

為什么 Activity.onCreate可以在字線程里更新UI鸣驱?

上面介紹到最終報(bào)錯(cuò)是由ViewRootImpl處理的含滴,那么這里就涉及到了Activity的創(chuàng)建過程了。這里貼一個(gè)網(wǎng)上大佬畫的startActivity流程圖

image

Activity的啟動(dòng)過程丐巫,我們可以從Context的startActivity說起,其實(shí)現(xiàn)是ContextImpl的startActivity勺美,然后內(nèi)部會(huì)通過Instrumentation來嘗試啟動(dòng)Activity递胧,這是一個(gè)跨進(jìn)程過程,它會(huì)調(diào)用ams的startActivity方法赡茸,當(dāng)ams校驗(yàn)完activity的合法性后缎脾,會(huì)通過ApplicationThread回調(diào)到我們的進(jìn)程,這也是一次跨進(jìn)程過程占卧,而applicationThread就是一個(gè)binder遗菠,回調(diào)邏輯是在binder線程池中完成的,所以需要通過Handler H將其切換到ui線程华蜒,第一個(gè)消息是LAUNCH_ACTIVITY辙纬,它對應(yīng)handleLaunchActivity,在這個(gè)方法里完成了Activity的創(chuàng)建和啟動(dòng)叭喜。我們在這里主要分析ActivityThread.handleLaunchActiivty

ActivityThread.handleLaunchActiivty

    private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
        unscheduleGcIdler();
        mSomeActivitiesChanged = true;

        if (r.profilerInfo != null) {
            mProfiler.setProfiler(r.profilerInfo);
            mProfiler.startProfiling();
        }

        handleConfigurationChanged(null, null);

        if (localLOGV) Slog.v(
            TAG, "Handling launch of " + r);

        // Initialize before creating the activity
        WindowManagerGlobal.initialize();
        //創(chuàng)建Activity類實(shí)例
        Activity a = performLaunchActivity(r, customIntent);

        if (a != null) {
            r.createdConfig = new Configuration(mConfiguration);
            reportSizeConfigurations(r);
            Bundle oldState = r.state;
            //
            handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);

            if (!r.activity.mFinished && r.startsNotResumed) {
                if (r.isPreHoneycomb()) {
                    r.state = oldState;
                }
            }
        } else {
            try {
                ActivityManager.getService()
                    .finishActivity(r.token, Activity.RESULT_CANCELED, null,
                            Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
        }
    }

可以看到Activity類實(shí)例是在performLaunchActivity創(chuàng)建的贺拣,然后又調(diào)用了handleResumeActivity方法

ActivityThread.handleResumeActivity

    final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        ActivityClientRecord r = mActivities.get(token);

        ...
        //調(diào)用Activity.onResume
        r = performResumeActivity(token, clearHide, reason);

        if (r != null) {
            final Activity a = r.activity;
            
            ....

            boolean willBeVisible = !a.mStartedActivity;
            if (!willBeVisible) {
                try {
                    willBeVisible = ActivityManager.getService().willActivityBeVisible(
                            a.getActivityToken());
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            if (r.window == null && !a.mFinished && willBeVisible) {
                
                ....
                //創(chuàng)建添加ViewRootImpl
                if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        wm.addView(decor, l);
                    } else {
                        // The activity will get a callback for this {@link LayoutParams} change
                        // earlier. However, at that time the decor will not be set (this is set
                        // in this method), so no action will be taken. This call ensures the
                        // callback occurs with the decor set.
                        a.onWindowAttributesChanged(l);
                    }
                }

          
            } 

            .....
        } 
    }

這里主要關(guān)注兩個(gè)方法performResumeActivitywm.addView(decor, l);

performResumeActivity

    public final ActivityClientRecord performResumeActivity(IBinder token,
            boolean clearHide, String reason) {
        ActivityClientRecord r = mActivities.get(token);
       
        if (r != null && !r.activity.mFinished) {
            if (clearHide) {
                r.hideForNow = false;
                r.activity.mStartedActivity = false;
            }
            try {
               ...

                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;
    }

performResumeActivity里調(diào)用了ActivityperformResume()方法,這里操作了mInstrumentationcallActivityOnResume()方法里調(diào)用了Activity生命周期的onResume方法

#Activity.performResume

    final void performResume() {
        performRestart();

        mFragments.execPendingActions();

        mLastNonConfigurationInstances = null;

        mCalled = false;
        // mResumed is set by the instrumentation
        mInstrumentation.callActivityOnResume(this);
        ...

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

wm.addView(decor, l)

wm.addView(decor, l)最終調(diào)用了WindowManagerImpl.addView

  • #WindowManagerImpl.addView

 private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }
  • #WindowManagerGlobal.addView

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...

        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;

        ...

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            // Start watching for system property changes.
            ....

            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

        }
    }

到這里我們終于看到了ViewRootImpl的創(chuàng)建,從上面過程可以看到ViewRootImpl的創(chuàng)建是在Activity.onResume之后的捂蕴,這也解釋了為什么我們可以在Activity.onCreate甚至Activity.onResume里實(shí)現(xiàn)子線程里操作UI譬涡,因?yàn)榇藭r(shí)ViewRootImpl并為創(chuàng)建不會(huì)進(jìn)行線程檢查。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末啥辨,一起剝皮案震驚了整個(gè)濱河市涡匀,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌溉知,老刑警劉巖陨瘩,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腕够,死亡現(xiàn)場離奇詭異,居然都是意外死亡拾酝,警方通過查閱死者的電腦和手機(jī)燕少,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蒿囤,“玉大人客们,你說我怎么就攤上這事〔姆蹋” “怎么了底挫?”我有些...
    開封第一講書人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長脸侥。 經(jīng)常有香客問我建邓,道長,這世上最難降的妖魔是什么睁枕? 我笑而不...
    開封第一講書人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任官边,我火速辦了婚禮,結(jié)果婚禮上外遇,老公的妹妹穿的比我還像新娘注簿。我一直安慰自己,他們只是感情好跳仿,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開白布诡渴。 她就那樣靜靜地躺著,像睡著了一般菲语。 火紅的嫁衣襯著肌膚如雪妄辩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,736評(píng)論 1 312
  • 那天山上,我揣著相機(jī)與錄音眼耀,去河邊找鬼。 笑死佩憾,一個(gè)胖子當(dāng)著我的面吹牛畔塔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鸯屿,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼澈吨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了寄摆?” 一聲冷哼從身側(cè)響起谅辣,我...
    開封第一講書人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎婶恼,沒想到半個(gè)月后桑阶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體柏副,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年蚣录,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了割择。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡萎河,死狀恐怖荔泳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情虐杯,我是刑警寧澤玛歌,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站擎椰,受9級(jí)特大地震影響支子,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜达舒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一值朋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧巩搏,春花似錦吞歼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽稽坤。三九已至丈甸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間尿褪,已是汗流浹背睦擂。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留杖玲,地道東北人顿仇。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像摆马,于是被迫代替她去往敵國和親臼闻。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361