Android setContentView源碼解析

Android開發的同窗們對setContentView確定都不陌生,但凡寫到Activity,都離不開這個函數,今天咱們就來看看它內部的實現吧!html

備註:本文基於Android 8.1.0版本。android

一、Activity 與 AppCompatActivity的區別

當咱們在老版本Android SDK開發的時候新建的Project的默認繼承的是Activity,而在5.0以後默認繼承的就是AppCompatActivity。兩者的區別從AppCompatActivity的註釋中可窺一斑。windows

/**
 * Base class for activities that use the
 * <a href="{@docRoot}tools/extras/support-library.html">support library</a> action bar features.
 *
 * <p>You can add an {@link android.support.v7.app.ActionBar} to your activity when running on API level 7 or higher
 * by extending this class for your activity and setting the activity theme to
 * {@link android.support.v7.appcompat.R.style#Theme_AppCompat Theme.AppCompat} or a similar theme.
 *
 * <div class="special reference">
 * <h3>Developer Guides</h3>
 *
 * <p>For information about how to use the action bar, including how to add action items, navigation
 * modes and more, read the <a href="{@docRoot}guide/topics/ui/actionbar.html">Action
 * Bar</a> API guide.</p>
 * </div>
 */
 
複製代碼

翻譯過來就是AppCompatActivity是全部使用了Support包中 ActionBar特性的Activity的父類。bash

關係能夠這麼形容:AppCompatActivity————>FragmentActivity————>Activity。微信

二、setContentView

AppCompatActivity中的setContentView也很是簡潔,能夠看出來須要去代理類中繼續查看代碼。markdown

@Override
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }
    
複製代碼
public AppCompatDelegate getDelegate() {
        if (mDelegate == null) {
            mDelegate = AppCompatDelegate.create(this, this);
        }
        return mDelegate;
    }

    // 真正到了這裏
    private static AppCompatDelegate create(Context context, Window window,
            AppCompatCallback callback) {
        if (Build.VERSION.SDK_INT >= 24) {
            return new AppCompatDelegateImplN(context, window, callback);
        } else if (Build.VERSION.SDK_INT >= 23) {
            return new AppCompatDelegateImplV23(context, window, callback);
        } else if (Build.VERSION.SDK_INT >= 14) {
            return new AppCompatDelegateImplV14(context, window, callback);
        } else if (Build.VERSION.SDK_INT >= 11) {
            return new AppCompatDelegateImplV11(context, window, callback);
        } else {
            return new AppCompatDelegateImplV9(context, window, callback);
        }
    }
    
複製代碼

而代理類實現的setContentView是在AppCompatDelegateImplV9中實現的:app

@Override
    public void setContentView(View v) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        contentParent.addView(v);
        mOriginalWindowCallback.onContentChanged();
    }
    
複製代碼

三、createSubDecor

setContentView的第一步就是確保SubDecor被install,下面源碼中有註釋ide

// 此處能夠看出SubDecor是一個ViewGroup
    private ViewGroup createSubDecor() {
        TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);

        if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
            a.recycle();
            // 這個錯你們可能遇到過,當使用了AppCompatActivity可是沒有設置一個Theme.AppCompat的主題,則會報這個Exception。
            throw new IllegalStateException(
                    "You need to use a Theme.AppCompat theme (or descendant) with this activity.");
        }
        
        // 接下來就到了設置一些Window屬性的地方,下面會再說
        if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {
            requestWindowFeature(Window.FEATURE_NO_TITLE);// 設置無title
        } else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {
            // Don't allow an action bar if there is no title. requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR); } if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) { requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY); } if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) { requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY); } mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false); a.recycle(); // Now let's make sure that the Window has installed its decor by retrieving it
        mWindow.getDecorView();// 就是建立DecorView

        final LayoutInflater inflater = LayoutInflater.from(mContext);
        ViewGroup subDecor = null;

        // 根據標記來決定inflate哪一個layout
        if (!mWindowNoTitle) {
            if (mIsFloating) {
                // If we're floating, inflate the dialog title decor subDecor = (ViewGroup) inflater.inflate( R.layout.abc_dialog_title_material, null); // Floating windows can never have an action bar, reset the flags mHasActionBar = mOverlayActionBar = false; ...... } else if (mHasActionBar) { ...... } } else { } if (subDecor == null) { throw new IllegalArgumentException( "AppCompat does not support the current theme features: { " + "windowActionBar: " + mHasActionBar + ", windowActionBarOverlay: "+ mOverlayActionBar + ", android:windowIsFloating: " + mIsFloating + ", windowActionModeOverlay: " + mOverlayActionMode + ", windowNoTitle: " + mWindowNoTitle + " }"); } if (mDecorContentParent == null) { mTitleView = (TextView) subDecor.findViewById(R.id.title); } // Make the decor optionally fit system windows, like the window's decor
        ViewUtils.makeOptionalFitsSystemWindows(subDecor);

        final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                R.id.action_bar_activity_content);

        // 獲取PhoneWindow中的content佈局對象
        final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
        if (windowContentView != null) {
            // There might be Views already added to the Window's content view so we need to // migrate them to our content view while (windowContentView.getChildCount() > 0) { final View child = windowContentView.getChildAt(0); windowContentView.removeViewAt(0); contentView.addView(child); } // Change our content FrameLayout to use the android.R.id.content id. // Useful for fragments. windowContentView.setId(View.NO_ID); // 將contentView的id設置爲android.R.id.content contentView.setId(android.R.id.content); // The decorContent may have a foreground drawable set (windowContentOverlay). // Remove this as we handle it ourselves if (windowContentView instanceof FrameLayout) { ((FrameLayout) windowContentView).setForeground(null); } } // Now set the Window's content view with the decor
        mWindow.setContentView(subDecor);

        contentView.setAttachListener(new ContentFrameLayout.OnAttachListener() {
            @Override
            public void onAttachedFromWindow() {}

            @Override
            public void onDetachedFromWindow() {
                dismissPopups();
            }
        });

        return subDecor;
    }
    
複製代碼

3.1 requestWindowFeature

@Override
    public boolean requestWindowFeature(int featureId) {
        ......
        switch (featureId) {
            case FEATURE_SUPPORT_ACTION_BAR:
                throwFeatureRequestIfSubDecorInstalled();
                mHasActionBar = true;// 僅僅是對變量賦值
                return true;
        ......
        }

        return mWindow.requestFeature(featureId);
    }
    
    // 這個又解釋了一個緣由,咱們若是在setContentView以後再次去設置requestWindowFeature,會拋出Exception。
    private void throwFeatureRequestIfSubDecorInstalled() {
        if (mSubDecorInstalled) {
            throw new AndroidRuntimeException(
                    "Window feature must be requested before adding content");
        }
    }
    
複製代碼

3.2 mWindow.getDecorView()

各位小夥伴應該都知道Android裏的Window這個類的實現子類實際上是PhoneWindow,因此咱們直接取PhoneWindow中去查getDecorView這個函數。最終會走到這裏,注意下面兩個標註了重點的地方函數

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);// 重點
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);// 重點
            ......
        }
    }    

    // generateDecor最後只是new了一個DecorView
    protected DecorView generateDecor(int featureId) {
        ......
        return new DecorView(context, featureId, this, getAttributes());
    }
    

    // 看一下DecorView的定義能夠看出它是一個FrameLayout
    /** @hide */
    public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
    }
    
複製代碼

generateLayout函數過多,此處不貼出代碼,值只分析下過程:工具

  1. 設置一些Window的屬性;
  2. 根據Window屬性選擇一個layoutResource,這些layoutResource有一個共性是都有一個@android:id/content的佈局,由於在AppCompatDelegateImplV9的createSubDecor函數裏會用到這個content;
  3. 選出layoutResource以後會進入一句關鍵的代碼:mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);layoutResource就被inflate出來而且添加到DecorView中了。備註,添加View的時候使用的LayoutParams是MATCH_PARENT;
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
        mStackId = getStackId();

        if (mBackdropFrameRenderer != null) {
            loadBackgroundDrawablesIfNeeded();
            mBackdropFrameRenderer.onResourcesLoaded(
                    this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
                    mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
                    getCurrentColor(mNavigationColorViewState));
        }

        mDecorCaptionView = createDecorCaptionView(inflater);
        final View root = inflater.inflate(layoutResource, null);// inflate出View
        if (mDecorCaptionView != null) {
            if (mDecorCaptionView.getParent() == null) {
                addView(mDecorCaptionView,
                        new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            }
            mDecorCaptionView.addView(root,
                    new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
        } else {
            // Put it below the color views.
            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        mContentRoot = (ViewGroup) root;
        initializeElevation();
    }
    
複製代碼

3.3 再回到createSubDecor

此時就開始建立真正的subDecor了,也有四個可選的layout,根據以前設置的屬性來選擇,而後去inflate出來。

// SubDecor中也必定有這個id
        final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                R.id.action_bar_activity_content);
                
        // 這裏的content就是是PhoneWindow中的content,上面提到過
        final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
        if (windowContentView != null) {
            // There might be Views already added to the Window's content view so we need to // migrate them to our content view // 合併PhoneWindow中的view到SubDecor中的content中 while (windowContentView.getChildCount() > 0) { final View child = windowContentView.getChildAt(0); windowContentView.removeViewAt(0); contentView.addView(child); } // Change our content FrameLayout to use the android.R.id.content id. // Useful for fragments. // id在這裏發生了變化 windowContentView.setId(View.NO_ID); contentView.setId(android.R.id.content); // The decorContent may have a foreground drawable set (windowContentOverlay). // Remove this as we handle it ourselves if (windowContentView instanceof FrameLayout) { ((FrameLayout) windowContentView).setForeground(null); } } 複製代碼

3.4 mWindow.setContentView

開始設置PhoneWindow的contentView,再把代碼切到PhoneWindow中

@Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        // 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.
        // mContentParent是否是看起來有點熟悉呢?generateLayout函數的返回值就是它
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            view.setLayoutParams(params);
            final Scene newScene = new Scene(mContentParent, view);
            transitionTo(newScene);// 有過渡動畫的狀況下用到了Scene
        } else {
            // 備註,mContentParent以前是@android:id/content,如今是View.NO_ID;
            // 如今的@android:id/content是SubDecor中的action_bar_activity_content
            mContentParent.addView(view, params);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

複製代碼

備註:到了這裏,SubDecor 已經被添加到了PhoneWindow中,而且@android:id/content是SubDecor中的action_bar_activity_content。接下來別的操做是關於細節的設置。

4. 再回到setContentView

@Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mOriginalWindowCallback.onContentChanged();
    }

複製代碼

此時咱們能夠看出setContentView中最複雜的代碼就是ensureSubDecor,接下來的代碼就只是使用SubDecor中的content,將咱們傳入的layout inflate出來而後加進去。

五、總結

setContentView的過程就是經過PhoneWindow建立DecorView,而後建立SubDecor,最終將傳遞進來的佈局add進來。

這樣你們也更容易明白爲何經過一些性能分析工具查看佈局層次及數量的時候老是比咱們本身寫的Layout多,也更容易明白對Activity設置View的函數被命名爲setContentView。

廣告時間

今日頭條各Android客戶端團隊招人火爆進行中,各個級別和應屆實習生都須要,業務增加快、日活高、挑戰大、待遇給力,各位大佬走過路過千萬不要錯過!

本科以上學歷、非頻繁跳槽(如兩年兩跳),歡迎加個人微信詳聊:KOBE8242011

歡迎關注
相關文章
相關標籤/搜索