View是如何被添加到手機屏幕的?

前言

在 Android 中,咱們知道咱們能看到的界面都是 Activity ,可是咱們能看到的這些 View 是如何被添加到View中的暱?今天這篇文章咱們就經過源碼來追蹤溯源,看看 View 到底是如何被添加到手機屏幕上的。本文篇幅較長請你們耐心閱讀。前端

View 被添加到Activity的步驟

咱們寫一個Activity的時候通常都是以下所示的寫法來將資源資源加載到了Activity 。android

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

這裏咱們調用的是 Activity 的 setContentView(@LayoutRes int layoutResID),那麼下面咱們繼續追蹤 Activity 的源碼,看看它的 setContentView(@LayoutRes int layoutResID)方法。windows

一、Activity.setContentView(int resourceId)

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

咱們這裏看到它調用的 getWindow() 獲取了一個對象調用了它的 setContentView(layoutResID) 方法。那麼咱們看下 getWindow() 返回的是什麼對象。app

/**
     * Retrieve the current {@link android.view.Window} for the activity.
     * This can be used to directly access parts of the Window API that
     * are not available through Activity/Screen.
     *
     * @return Window The current window, or null if the activity is not
     *         visual.
     */
    public Window getWindow() {
        return mWindow;
    }

咱們看到這裏直接返回了 mWindow,這裏咱們看註釋它說這個方法返回的是當前的 Activity 的 window ,若是當前Activity是不可見的那麼返回的是 nullide

咱們繼續跟蹤代碼,找到 mWindow 的建立,咱們會發如今 attach()方法中,發現對mWindow的賦值。佈局

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
       
       /***********省略部分代碼*****************/
       
        }

咱們這裏看到mWindow是一個 PhoneWindow對象,因此這裏調用了 PhoneWindowsetContentView(layoutResID) 方法。post

二、PhoneWindow.setContentView(int layoutResID)

咱們接着來看PhoneWindow.setContentView(int layoutResID)的源碼:學習

@Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        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();
        }
        mContentParentExplicitlySet = true;
    }

咱們從第一行看,首先判斷了mContentParent是否是爲null,若是爲null,執行了indtallDecor()方法,下面咱們繼續看它的源碼。this

2.1 installDecor()

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            //初始化 DecorView
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            //給 DecorView 設置 PhoneWindow
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            //初始化 mContentParent
            mContentParent = generateLayout(mDecor);

            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            //設置 DecorView 忽略fitsSystemWindows
            mDecor.makeOptionalFitsSystemWindows();

         /*************省略部分代碼************************/
         
        }
    }

這部分代碼中咱們首先看到在第三行對mDecor作了初始化,這裏的mDecor就是DecorView。而後若是mDecor不爲null的話將當前的Window設置到 DecorView 。後面判斷了 mContentParent 是否是爲空,爲空則初始化 mContentParent。總結起來這個方法中作了下面幾件事:spa

  1. 若是 mDevor 爲空調用generateDecor(-1)初始化 DecorView;
  2. 若是 mDecor 不爲空則給 DecorView 設置當前 PhoneWindow;
  3. 若是mContentParent爲空則初始化 mContentParent;

下面咱們分別來看這咱們首先看下generateDecor(-1)方法是如何初始化 DecorView的。

I、初始化DevoreView
protected DecorView generateDecor(int featureId) {
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());
    }

這裏咱們看到這個方法比較件單,它的返回值是直接 new 了一個 DecorView 對象。
下面咱們來看 mDecor.setWindow(this)

II、mDecor.setWindow(this)
void setWindow(PhoneWindow phoneWindow) {
        mWindow = phoneWindow;
        Context context = getContext();
        if (context instanceof DecorContext) {
            DecorContext decorContext = (DecorContext) context;
            decorContext.setPhoneWindow(mWindow);
        }
    }

這個方法也是很是件單,就是將傳入的PhoneWindow對象賦值給DecorView中的window屬性。

III、初始化mContentParent
protected ViewGroup generateLayout(DecorView decor) {
    
    /****************省略部分代碼***************************/

        // Inflate the window decor.
        int layoutResource;
        int features = getLocalFeatures();
        // System.out.println("Features: 0x" + Integer.toHexString(features));
        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            layoutResource = R.layout.screen_swipe_dismiss;
            setCloseOnSwipeEnabled(true);
        } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        R.attr.dialogTitleIconsDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else {
                layoutResource = R.layout.screen_title_icons;
            }
            // XXX Remove this once action bar supports these features.
            removeFeature(FEATURE_ACTION_BAR);
            // System.out.println("Title Icons!");
        } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
                && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
            // Special case for a window with only a progress bar (and title).
            // XXX Need to have a no-title version of embedded windows.
            layoutResource = R.layout.screen_progress;
            // System.out.println("Progress!");
        } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
            // Special case for a window with a custom title.
            // If the window is floating, we need a dialog layout
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        R.attr.dialogCustomTitleDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else {
                layoutResource = R.layout.screen_custom_title;
            }
            // XXX Remove this once action bar supports these features.
            removeFeature(FEATURE_ACTION_BAR);
        } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
            // If no other features and not embedded, only need a title.
            // If the window is floating, we need a dialog layout
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        R.attr.dialogTitleDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
                layoutResource = a.getResourceId(
                        R.styleable.Window_windowActionBarFullscreenDecorLayout,
                        R.layout.screen_action_bar);
            } else {
                layoutResource = R.layout.screen_title;
            }
            // System.out.println("Title!");
        } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
            layoutResource = R.layout.screen_simple_overlay_action_mode;
        } else {
            // Embedded, so no decoration is needed.
            layoutResource = R.layout.screen_simple;
            // System.out.println("Simple!");
        }
        
        //////////////////////我是華麗的分隔線////////////////////////////////////////
        
        mDecor.startChanging();
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }
        
       /****************省略部分代碼***************************/
      
        return  contentParent;
    }

這裏咱們看到主要有兩部分文中的代碼,咱們能夠看到分隔線以上的那一大段if/else是根據不一樣的條件給layoutResource賦值的。這一大段代碼就是根據咱們設置的Application的主題去選擇對應加載的資源佈局 Id。
而後咱們看到緊接着調用的DecorViewonResourcesLoaded(mLayoutInflater, layoutResource)方法,那麼咱們繼續看這個方法具體作了哪些事情

onResourcesLoaded
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    /**************省略部分代碼**************************/
        final View root = inflater.inflate(layoutResource, null);
       /**************省略部分代碼**************************/
            // Put it below the color views.
            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        mContentRoot = (ViewGroup) root;
        initializeElevation();
    }

咱們這裏仍是看核心部分的代碼,這裏咱們看到首先將傳進來的資源佈局進行inflate並把它賦值給root,而後將root添加到DecorView中。一句話來講就是 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);這句代碼是<font color=green>將對應的資源佈局文件加載到mDecor</font>。
下面咱們繼續來看generateLayout(DecorView decor)方法,咱們發現下面它對contentParent經過findViewById(ID_ANDROID_CONTENT)進行了賦值。那麼咱們看下ID_ANDROID_CONTENT對應的 ID 值。

/**
     * The ID that the main layout in the XML layout file should have.
     */
    public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

註釋中說這個 ID 是主佈局的 XML 中必須存在的。也就是以前賦值的layoutResource中必須存在的。咱們隨便找一個上面的 xml 佈局文件來來看看.

R.layout.screen_simple
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

咱們看到R.id.content確實是存在的,而且它是一個FrameLayout。到這裏咱們來看下如今前面這些具體作了哪些?
以下圖所示,到這裏 Activity 持有一個 PhoneWindow 對象,PhoneWindow 中有一個 DecorView ,DecorView 中加載了一個基礎的資源佈局,裏面有 title,ActionBar 等,確定存在一個 id 爲 R.id.ccontent 的資源佈局。
Acctivity 根佈局.png

看到這裏就完了?不不不,還有咱們繼續往下看!!!

2.2 mLayoutInflater.inflate(layoutResID, mContentParent)

咱們繼續回到PhoneWiondowsetContentView(int layoutResID),以下:

@Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        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();
        }
        mContentParentExplicitlySet = true;
    }

咱們前面看完了installDecor()方法,咱們繼續往下看,咱們看到下面緊接着調用了 mLayoutInflater.inflate(layoutResID, mContentParent);將以前 Activity 中從傳入的資源佈局加載到咱們前面初始化的mContentParent中,也就是在資源ID爲R.id.ccontent的 Framlayout 中。如今咱們來看看咱們傳進來的資源佈局文件被加載到哪裏了?
資源佈局文件.png

總結

這篇文章咱們從 Ativity 的 setContentView(layoutResourceId) 開始,一步步追蹤 Android FrameWork 層的源碼。探究了咱們平時代碼中寫的資源佈局是如何被加載到 Activity 中的。咱們根據源碼簡單畫了一個流程圖以下:
View被添加到Activity流程圖.png

歡迎在評論區留下你的觀點你們一塊兒交流,一塊兒成長。若是今天的這篇文章對你在工做和生活有所幫助,歡迎 轉發分享給更多人。

同時歡迎你們加入我組建的大前端學習交流羣,羣裏你們一塊兒學習交流 Android、Flutter等知識。從這裏出發咱們一塊兒討論,一塊兒交流,一塊兒提高。

羣號:872749114

個人公衆號

相關文章
相關標籤/搜索