Activty視圖加載流程淺析

通過本文你可能會了解以下幾個方面

  • 1.Activity視圖從準備到繪制顯示的基本流程
  • 2.視圖繪制和Activity生命周期的關系
  • 3.子線程不能更新UI的原因和原理
  • 4.invalidate和postInvalidate機制
  • 5.ViewRootImpl和View的綁定流程

上篇文章中我們分析了一個Activity的視圖結構,但是直到setContentView執行完畢,我們也僅僅是得到了一個布局,若不做任何處理,他是顯示出來的,今天我們就來介紹一個視圖的加載。首先說一下setContentView之后視圖是在哪加載的。
這還要從Activity的創建流程開始看,這里不多敘述,如果看我的的關于四大組件中Context的分析話應該清楚,在初始化Activity的地方,也就是ActivityThread中的performLaunchActivity方法中,從這個方法向前看,也就是調用的地方,在handleLaunchActivity方法中:

    private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
        ....
        Activity a = performLaunchActivity(r, customIntent);

        if (a != null) {
            ....
            handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);

           ....
    }

Activity初始化之后,就調用了handleResumeActivity方法:

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

            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 (r.mPreserveWindow) {
                    a.mWindowAdded = true;
                    r.mPreserveWindow = false;
                    ViewRootImpl impl = decor.getViewRootImpl();
                    if (impl != null) {
                        impl.notifyChildRebuilt();
                    }
                }
                if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        wm.addView(decor, l);
                    } else {
                        a.onWindowAttributesChanged(l);
                    }
                }
            }
          ......
                if (r.activity.mVisibleFromClient) {
                    r.activity.makeVisible();
                }
            }

      ....
    }

首先執行了performResumeActivity,在這里面回調了onResume方法,也就是一個activity創建完成前的最后一個生命周期回調。下面先獲得了activity的DecorView,也就是一個完整的視圖,之后先把他的可見性設為隱藏。然后調用了wm.addView(decor, l);wm就是ViewManager 。如果你了解或者看過我的Toast源碼分析,應該知道Toast也是通過這種方式顯示出來的。所以到這一步才能把一個activity的視圖顯示出來,隨后調用了activity的makeVisible再把DecorView設為可見:

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

這里的mDecor在Activity自身沒有初始化的地方,只在handleResumeActivity方法中通過a.mDecor = decor;初始化。

這里才是把一個activity顯示出來。下面我么重點看一下wm.addView的實現。首先看一下wm 也就是ViewManager 來歷,來自于getWindowManager:

    public WindowManager getWindowManager() {
        return mWindowManager;
    }

只是簡單返回一下,再看他初始化的地方,在attach方法中通過mWindowManager = mWindow.getWindowManager();初始化。

    final void attach(...) {
       ...
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        ....
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        ...
        mWindowManager = mWindow.getWindowManager();
        ...
    }

mWindow是一個PhoneWindow對象,所以去看看他的getWindowManager方法,發現并沒有實現,所以用的是父類Window中的:

    public WindowManager getWindowManager() {
        return mWindowManager;
    }

又是簡單取值,所以還要看初始化的地方:

    public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        mAppToken = appToken;
        mAppName = appName;
        mHardwareAccelerated = hardwareAccelerated
                || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }

這個setWindowManager其實是在activity中的attch方法中調用的,先傳入了一個WindowManager ,也就是獲取的系統服務,然后調用了其內部方法createLocalWindowManager,在setWindowManager中的類型轉換告訴了我們實際調用的是WindowManagerImpl:

android\frameworks\base\core\java\android\view\WindowManagerImpl.java
    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow);
    }

發現實際上new了一個WindowManagerImpl對象。WindowManagerImpl實現了WindowManager接口。到這里基本上是跟蹤清楚了,上文中調用的addView實際上調用的WindowManagerImpl的addView方法:

    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        android.util.SeempLog.record_vg_layout(383,params);
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

發現又調用了mGlobal的addview。mGlobal是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);
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }

這個方法主要是給要添加的view設置LayoutParams,然后將View,ViewRootImpl和LayoutParams分別存在三個集合中,然后調用ViewRootImpl的setView方法:

android\frameworks\base\core\java\android\view\ViewRootImpl.java

這個方法比較長,但是重要的地方是調用了 requestLayout():

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

這里先檢查了線程,所謂子線程不能跟新UI的原因就在這,這個問題在文末再具體講。緊接著調用了scheduleTraversals方法

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

重點在mTraversalRunnable:

    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

這里執行了performTraversals,這個方法是ViewRootImpl的核心。ViewRootImpl是具體更新View的管理類,所有關于View的更新操作都是在這里執行的,performTraversals就是view繪制的起始,這個方法很長也和很復雜,需要詳細了解的可以去看完整源碼。這里僅簡單介紹一下整個流程:

  • 1.預測量:對整個控件第一次策略,各個控件會上報各自期望的尺寸
  • 2.窗口布局:根據預測量的結果對窗口進行重新布局
  • 3.最終測量:根據布局結果,進行最后的測量,并回調view的measure
  • 4.布局階段:根據最終測量結果進行布局,確定控件位置,回調view的layout
  • 5.繪制階段:最后回調onDraw將一個view畫出來

在performTraversals方法中,會先后執行下面幾個方法:

private void performTraversals() {  
        ......  
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        ......  
        if (triggerGlobalLayoutListener) {
            mAttachInfo.mRecomputeGlobalAttributes = false;
            mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
        }
        .....
        performDraw();
        }
        ......  
    }

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

在這里面調用了mView的measure方法,mView在setView中初始化,就是wm.addView中傳進來的DecorView,但是DecorView并沒有measure的實現,調用的是父類中的,這個方法中調用了onMeasure方法,在DecorView中又會調用父類也就是FrameLayout的onMeasure,在這里會遍歷所有子view,并調用其的measure,就這樣測量了整個視圖的view。

    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        mLayoutRequested = false;
        mScrollMayChange = true;
        mInLayout = true;

        final View host = mView;
        if (host == null) {
            return;
        }
        if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
            Log.v(mTag, "Laying out " + host + " to (" +
                    host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
        }

        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

           ....
    }

同樣調用了DecorView的layout,也是父類的layout,然后調用onLayout,在DecorView的onLayout中,先調用了FrameLayout的onLayout,FrameLayout中調用了layoutChildren來對每個子view進行布局,也是調用子view的layout方法。

    private void performDraw() {
        ...
        draw(fullRedrawNeeded);
        ...
    }
    private void draw(boolean fullRedrawNeeded) {
        Surface surface = mSurface;
        ....
        drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)
        ....
     }
    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {

        // Draw with software renderer.
        final Canvas canvas;
        try {
            ....

            canvas = mSurface.lockCanvas(dirty);

            ....

                mView.draw(canvas);

               ....
    }

performDraw最后調用到了drawSoftware,先獲取了canvas,然后執行了DecorView的draw,具體流程和上面兩點類似。

到這里就把DecorView繪制出來了,setView中在執行完requestLayout之后,又調用了addToDisplay從而把整個視圖顯示出來。。

最后總結一下,一個Activity的視圖界面,在setContentView時被準備出來,但是還沒有繪制顯示(界面的測量布局繪制都沒有執行),一直到生命周期中onResume中也沒有執行,而是在ActivityThread的handleResumeActivity中,回調完onResume后,在wm.addView時才做了測量布局繪制,所以我們在onCreate或者onResume都拿不到控件的某些信息,如getWidth。若要解決這個問題,可以添加一個回調,如view.getViewTreeObserver().addOnGlobalLayoutListener。這個回調在performTraversals中調起,他的位置在performMeasure及performLayout之后,在performDraw之前(見上文代碼片段),所以能獲得正常的數據。

文末再來說一下文中遺留的問題,就是子線程為什么不能更新UI。

首先我們如果在子線程中更新UI,直觀的就會有這樣錯誤:



其實到這里需要糾正一點,出現這樣錯誤并不能說子線程不能更新UI,或者UI界面只能在主線程內更新,好像主線程比子線程神圣很多似的。正如錯誤信息中所說的哪那樣,只能在視圖創建的那個線程中更新UI。

首先先看一下這個錯誤的來歷,在ViewRootImpl的checkThread方法中,也就是requestLayout中執行的第一個方法:

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

我們說他檢測了線程,就是檢測當前線程是不是創建該視圖的線程。mThread 在構造中被初始化,賦值為構造函數執行時的那個線程。為什么更新UI時會報錯呢?一般更新一個view時會調用invalidate方法,我們看一下流程:

    public void invalidate() {
        invalidate(true);
    }
    public void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }
    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
        ...
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                p.invalidateChild(this, damage);
            }

           .........
        }
    }

ViewParent 是一個接口,ViewRootImpl就實現了這個接口,最后會調用到ViewRootImpl的invalidateChild方法:

   @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();
         ....
    }

可見這個方法第一步就在檢查線程。至于為什么要原始線程呢,還是和線程安全相關的.

為了打消大家的疑慮,我來給大家做一個關于在子線程創建視圖,而不能在主線程更新的例子:

    public void click(View view) {
        System.out.println(bt.getWidth());
        if (f){
            f = false;
            new Thread(){
                @Override
                public void run() {
                    super.run();
                    Looper.prepare();
                    level = new Level(MainActivity.this);
                    WindowManager mWindowManager = (WindowManager) getApplicationContext()
                            .getSystemService(Context.WINDOW_SERVICE);

                    View mView = level;
                    final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
                    params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
                    int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                            | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;

                    params.flags = flags;
                    params.format = PixelFormat.TRANSLUCENT;
                    params.width = WindowManager.LayoutParams.MATCH_PARENT;
                    params.height = WindowManager.LayoutParams.WRAP_CONTENT;
                    params.gravity = Gravity.TOP;

                    mWindowManager.addView(mView, params);
                    level.setXY(10,10);
                    Looper.loop();
                }
            }.start();
        }else{
            level.setXY(20,20);
        }
    }

    public void setXY(int x,int y){
        rx = x;
        ry = y;
        invalidate();
    }

就是創建一個懸浮窗(目前能想到的在子線程創建UI的只有這個,activity界面都是在主線程創建的,我們干涉不了),第一次執行click,啟動一個子線程創建一個懸浮窗,第二字執行click是調用level.setXY(20,20);,這個肯定是在主線程,但是報錯了,還是一樣的錯誤,就是只能在原始線程更新。同樣在子線程中,我也調用了setXY,是沒有問題的。

這個例子大家可以自己嘗試驗證,level類是我項目中一個自定義view,大家嘗試時隨意一個view都可以。

同樣大家肯定都做過在子線程彈出Toast,也進一步驗證了子線程也是可以執行UI操作的,只不過要一定條件。另外如果一定要在子線程更新,可以調用postInvalidate,可以看一下他的實現還有和Invalidate的區別。

    public void postInvalidate() {
        postInvalidateDelayed(0);
    }
    public void postInvalidateDelayed(long delayMilliseconds) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }

這里最后調用了attachInfo.mViewRootImpl.dispatchInvalidateDelayed。attachInfo是View內部類AttachInfo的一個對象,通過他將view和ViewRootImpl進行了綁定,關于它的綁定我們下面再說,先看dispatchInvalidateDelayed:

    public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }
    
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MSG_INVALIDATE:
                ((View) msg.obj).invalidate();
                break;
              .....
        }

看到了吧,同Handler機制,進行了更新,mHandler隨著ViewRootImpl創建,所以是一個線程,間接在一個線程調用view的invalidate,所以沒問題。

最后說一下ViewRootImpl和View的綁定。可見View中有很多方法都直接用到了mViewRootImpl,那么在哪里綁定的呢?還是看AttachInfo這個View的內部類,他在ViewRootImpl的構造中初始化:

    public ViewRootImpl(Context context, Display display) {
        ....
        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
                context);
        ...
    }

這里AttachInfo持有了ViewRootImpl對象,隨后在setView中設置了根viewmAttachInfo.mRootView = view;,也就是DecorView。到這里viewmAttachInfo持有了ViewRootImpl和根view。然后在performTraversals方法中,調用了

host.dispatchAttachedToWindow(mAttachInfo, 0);

    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        mAttachInfo = info;
        ....
    }

就這樣DecorView獲得了AttachInfo 對象,這樣就可以獲得ViewRootImpl

    public ViewRootImpl getViewRootImpl() {
        if (mAttachInfo != null) {
            return mAttachInfo.mViewRootImpl;
        }
        return null;
    }

但具體某個view是如何和ViewRootImpl綁定的呢?不要忘了DecorView的所有孩子都是addView進去的,看一下addview的實現:

    public void addView(View child) {
        addView(child, -1);
    }
    public void addView(View child, int index) {
        ...
        addView(child, index, params);
    }
    public void addView(View child, int index, LayoutParams params) {
        ...
        addViewInner(child, index, params, false);
    }
    private void addViewInner(View child, int index, LayoutParams params,
            boolean preventRequestLayout) {

       ...

        AttachInfo ai = mAttachInfo;
        if (ai != null && (mGroupFlags & FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW) == 0) {
            boolean lastKeepOn = ai.mKeepScreenOn;
            ai.mKeepScreenOn = false;
            child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));
            ...
        }
    }

看到了吧,子view把父view的mAttachInfo綁定到自己身上的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,772評論 3 422
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,947評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,960評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,350評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,549評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,104評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,914評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,089評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,340評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,834評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,106評論 2 375