【Android源碼解析】從Window層開始,玩轉Activity、View!

前言:這是我第一次寫源碼解析類的文章,閱讀源碼真的能學到不少,驚訝大牛設計思路的同時,感覺到更多的是自己好多知識都不清楚。由于水平有限,文中難免有些錯誤,歡迎指正互相學習~

Window概述

Window,正如它的直譯,表示一個窗口。以前我們常說,Activity是直接可與用戶交互的UI界面,而這些交互界面都要依附在Window窗口下才能工作、顯示!從某種程度甚至可以這么說,Android中的視圖(Activity,Dialog,PopupWindow......)都是依附于Window來呈現的,所有的Event事件也都是從Window層下發的。關于Window,Activity,View的關系,這里可以先給出一張粗糙的二維關系圖:


Window,Activity,View

從面向對象的角度來說,Window是一個抽象的概念,它對應著一個頂級view,還有一個ViewRootImpl,通過這個實現類中的ViewRootImpl,我們可以操作具體的View,并向它們下發事件。這從抽象Window的實現類PhoneWindow中可以找到如下源碼:

    @Override
    public void injectInputEvent(InputEvent event) {
        getViewRootImpl().dispatchInputEvent(event);
    }

    private ViewRootImpl getViewRootImpl() {
        if (mDecor != null) {
            ViewRootImpl viewRootImpl = mDecor.getViewRootImpl();
            if (viewRootImpl != null) {
                return viewRootImpl;
            }
        }
        throw new IllegalStateException("view not added");
    }

關于view事件的dispatch(分發)、onIntercept(攔截)、onTouch(消耗)就不在這宣兵奪主詳細講了。

除此之外,Window內部還向我們提供了一個方便各種狀態下回調的CallBack接口,主要回調方法如下:

    /**
     * API from a Window back to its caller.  This allows the client to
     * intercept key dispatching, panels and menus, etc.
     */
    public interface Callback {

        public boolean dispatchKeyEvent(KeyEvent event);

        public boolean dispatchTouchEvent(MotionEvent event);

        public boolean onMenuItemSelected(int featureId, MenuItem item);

        public void onContentChanged();

        public void onWindowFocusChanged(boolean hasFocus);

        public void onAttachedToWindow();

        public void onDetachedFromWindow();

    }

很熟悉把~原來我們在Activiy中的各種回調方法都是這其中來的,這些方法回調的周期相信大家也都知道把,這里只說一下onContentChanged(),還記得在Activiy的creat方法中,我們必須給界面通過setContenViewt設置布局,而當布局設置完成后,Window便會回調此方法。至于到底什么是ContentView?后面會分析。

而外部訪問Window則必須通過WindowManager的實現類WindowMangerImpl,可以發現其中向外部提供了三個增加、更新、刪除View的方法:

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

    @Override
    public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.updateViewLayout(view, params);
    }
    @Override
    public void removeView(View view) {
        mGlobal.removeView(view, false);
    }

通過方法名我們可以很清楚地辨別這三個方法的作用,但是mGlobal又是什么呢?其實這只是一個工作業務的橋接類,將addView的工作通過WindowManagerGlobal全部橋接給了ViewRootImpl來實現了。這也就與上文中說的``Window是一個抽象的概念,它對應著一個頂級view,還有一個ViewRootImpl,通過這個實現類中的ViewRootImpl,我們可以操作具體的View,向它們下發事件`對應起來了,我們主要分析addView栗子:

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        if (display == null) {
            throw new IllegalArgumentException("display must not be null");
        }
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        if (parentWindow != null) {
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        }
        synchronized (mLock) {
            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;
        }
    }

這里很清晰地看到new了一個ViewRootImpl實例,隨后將Window中的view,root(ViewRootImpl),wparams(布局參數)存入了相應列表中,之后又通過root.setView(view, wparams, panelParentView);將事情全部移交給了ViewRootImpl來做。setView方法比較復雜,大致思路是先通過requestLayout刷新當前布局,隨后通過IPC機制,遠程調用WindowManagerService完成View的set。給出一小部分源碼:

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
                mView = view;
                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                requestLayout();
                try {
                    mOrigWindowType = mWindowAttributes.type;
                    mAttachInfo.mRecomputeGlobalAttributes = true;
                    collectViewAttributes();
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
                } catch (RemoteException e) {
                    mAdded = false;
                    mView = null;
                    mAttachInfo.mRootView = null;
                    mInputChannel = null;
                    mFallbackEventHandler.setView(null);
                    unscheduleTraversals();
                    setAccessibilityFocus(null, null);
                    throw new RuntimeException("Adding window failed", e);
                } finally {
                    if (restore) {
                        attrs.restore();
                    }
                }
    }

最后在這里簡單提及下DecorView(PhoneWindow中的內部類),具體的會在下文Activity中講更好理解,這里只是說明下Window中有這么一個頂級view。
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
可以看出DecorView本質還是一個FrameLayout,這里面就負責我們的各種UI顯示,各種事件消耗、分發。通過Activity,我們就可以將自己的UI布局載入Window中的DecorView!

這里總結一下Window中的知識點:

  • 1.一個Window可以抽象理解為一個View和一個ViewRootImpl的組合。
  • 2.Window內部有一個十分豐富的CallBack接口,可以滿足我們大部分的回調需求。
  • 3.通過WindowManager的實現類WindowManagerImpl,管理修改我們的Window,本質上是將這些操作橋接給了ViewRootImpl。
  • 4.Window中的UI承載體--DecorView。
Activity概述

關于Activity的啟動流程和Thread這里不會講述(其實是我也弄不清楚,_~),主要講Activity是如何和Window建立起聯系的。
當新建一個Activity時,通常ide工具都會自動幫我setContentView,之前一直以為這是將我們的布局文件通過ID直接設置給Activity,見多了也就以為這是一種定理了。其實看源碼,很清楚地可以發現它本質還是獲取的Window:

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

先不急著看Window中的setContentView方法,我們先看Window是如何獲取的。

    public Window getWindow() {
        return mWindow;
    }

對比源碼發現,這個mWindow在L、M版本上初始化的方式還不太一樣,如圖:

L版本----M版本

L版本是通過一個策略類PolicyManager,使用反射機制獲取IPolicy來實例化mWindow,而M版本直接在attach方法中,mWindow = new PhoneWindow(this);直接實例化。不知道這其中是不是考慮到了性能的優化。

通過mWindow.setCallback(this);,便在Activity綁定了Window中的回調接口Callback。

接下來我們看PhoneWindow中的setContentView具體邏輯:

    @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

當我們初次設置contentView時,會執行installDecor(),否則只是清除所有子view,接著inflate我們的布局進decorView的內容區域,最后回調onContentChanged(),通知Activity,DecorView裝載完畢了!

再追蹤installDecor()方法前,我們先了解一下DecorView的具體結構:


DecorView

不難看出,一個DecorView可以非為兩部分,第一部分就是上放的titleBar(標題欄),第二部分就是contentParent(內容區),我們的布局便是裝載進contentParent區域。

接著我們看installDecor()方法:

    private void installDecor() {
        if (mDecor == null) {
            mDecor = generateDecor();
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
        }
    .................
    .................
    .................
    }

    protected DecorView generateDecor() {
        return new DecorView(getContext(), -1);
    }

很好理解,如果我們的DecorView不存在,則為我們生成一個,接著在生成一個內容區域以供裝載布局。

我們主要看generateLayout()的源碼:

    protected ViewGroup generateLayout(DecorView decor) {

    //省略ViewGroup參數、樣式設置部分//

        mDecor.startChanging();

        View in = mLayoutInflater.inflate(layoutResource, null);
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        mContentRoot = (ViewGroup) in;

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        if (getContainer() == null) {
            final Drawable background;
            if (mBackgroundResource != 0) {
                background = getContext().getDrawable(mBackgroundResource);
            } else {
                background = mBackgroundDrawable;
            }
            mDecor.setWindowBackground(background);

            final Drawable frame;
            if (mFrameResource != 0) {
                frame = getContext().getDrawable(mFrameResource);
            } else {
                frame = null;
            }
            mDecor.setWindowFrame(frame);

            mDecor.setElevation(mElevation);
            mDecor.setClipToOutline(mClipToOutline);

            if (mTitle != null) {
                setTitle(mTitle);
            }

            if (mTitleColor == 0) {
                mTitleColor = mTextColor;
            }
            setTitleColor(mTitleColor);
        }

        mDecor.finishChanging();

        return contentParent;
    }

發現contentParent內容區也是通過ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);找到我們傳入布局文件的ID,為我們生成一個ViewGroup,在對其做一系列UI優化后,就可以正常使用了。

當然,我們并不一定每次都必須通過布局文件的ID來setContentView,完全可以自己動態地設置內容,栗:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_main);
        LinearLayout linearLayout = new LinearLayout(this);
        linearLayout.setOrientation(LinearLayout.VERTICAL);
        for (int i= 0; i < 5; i++) {
            TextView textView = new TextView(this);
            textView.setTextSize(12);
            textView.setText(i+"");
            linearLayout.addView(textView);
        }
        setContentView(linearLayout);
    }

不過要想讓用戶正常看到這些布局,還需要等待Activity的onResume生命周期中執行如下方法:

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

我想這也是初識Android時,會說必須經過onResume生命周期后,Activity才能"獲取焦點"的原因吧!

其實從另一個常用的方法,我們也能清楚地理解到Activity,Window,DecorView的關系。

findViewById :

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

推薦閱讀更多精彩內容