Activity的构成

前言

最近打算把 Android 中和 View 相关的知识做一下更新,本来另一篇博客已经写的差不多了,但是发现缺少了这一块关于 Activity 的构成知识的话,在讲述事件分发机制的时候不太好开展,所以这个知识点也就相当于给后面的 View 相关知识做一个补充!废话不多说,我们现在开始。

先下结论

在解析源码之前,我们先把最终的结论得出来,方便我们后面对源码部分进行分析,在我们的 Android 设备上,我们的 Activity 构成应当是这样子的:

如果你之前从未了解过这部分知识,初看可能会觉得相当的陌生。别急,接下来我将为你从外到内介绍这幅图的含义。

  • 首先是最外层的 Activity,每次系统为我们默认建好的 MainActivity ,是继承自 AppCompatActivity 的,其实 AppCompatActivity 也是间接继承自 Activity 的一个类,所以我们的最外框层,就是 Activity
  • 接下来是 PhoneWindow,我们在 MainActivity 使用的 setContentView 加载布局方法实际上调用的是 PhoneWindow 里面的 setContentView 方法。
  • 然后就到了 DecorView 了,在 PhoneWindow 中我们会生成一个 DecorView,而且 DecorView 是继承自 FrameLayout 的一个类。
  • 最后就是 DecorView 里面的 TitleViewContentView 了,其中 TitleView 是用于显示标题的,而 ContentView 是我们平时打交道最多的地方,我们写的布局(例如 activity_main.xml)实际上都是放置在这里的,所以这也是我们加载布局的方法要叫做 setContentView 而不是直接叫做 setView 的一个原因。

好了,讲完了我们 Activity 的构成,我们现在就开始从源码的角度来看看源码是如何构成上面这幅图的吧!

源码解析

首先,我们的布局文件加载都是通过 ActivitysetContentView 方法加载进来的,也就是说一切的活动从这里开始,那么我们就来看看这个方法里面是什么吧。

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

这里可以看到,代码里面通过 getWindow 调用了 setContentView 方法,getWindow 方法明显是会返回一个对象的,我们就来看看 getWindow 方法的源码是什么。

public Window getWindow() {
        return mWindow;
}

这个方法非常的简单,只是简单地返回了一个 mWindow 的变量,这个 mWindow 从前面的定义我们可以得知这是一个 Window 类型的对象,那么它又是从哪里来的呢?

我们接下来把目光聚集到 Activityattach 方法上来,它的源码如下所示:

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);  // 1
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
            mWindow.setSoftInputMode(info.softInputMode);
        }
        if (info.uiOptions != 0) {
            mWindow.setUiOptions(info.uiOptions);
        }
        mUiThread = Thread.currentThread();

        mMainThread = aThread;
        mInstrumentation = instr;
        mToken = token;
        mIdent = ident;
        mApplication = application;
        mIntent = intent;
        mReferrer = referrer;
        mComponent = intent.getComponent();
        mActivityInfo = info;
        mTitle = title;
        mParent = parent;
        mEmbeddedID = id;
        mLastNonConfigurationInstances = lastNonConfigurationInstances;
        if (voiceInteractor != null) {
            if (lastNonConfigurationInstances != null) {
                mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
            } else {
                mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
                        Looper.myLooper());
            }
        }

        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;

        mWindow.setColorMode(info.colorMode);
    }

我们看到代码的第12行处,这里 mWindownew 了出来,至于为什么要 new 成一个 PhoneWindow 类型的对象,原因是 Window 是一个抽象类,而 PhoneWindowWindow 的唯一实现类。从这里我们也就能得到一个重要的信息mWindow 就是一个 PhoneWindow 对象。

接下来我们继续回到我们 ActivitysetContentView 方法,为了方便阅读我这里把 setContentView 的代码重新贴一遍:

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

从上面的分析我们知道了 getWindow 返回的是一个 PhoneWindow 对象,也就是我们前面那幅图说的,Activity 里面包含了一个 PhoneWindow。所以这个 setContentView 就是 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();
        }
        mContentParentExplicitlySet = true;
    }

我们看到第4行的代码,这是这个方法的一个关键语句,一般情况下,我们都可以进入到这个 if 里面执行 installDecor 方法,我们接下来看看 installDecor 方法里面是什么吧!

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {  // 关键点1
            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);  // 关键点2

            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();

            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);

            if (decorContentParent != null) {
                mDecorContentParent = decorContentParent;
                mDecorContentParent.setWindowCallback(getCallback());
                if (mDecorContentParent.getTitle() == null) {
                    mDecorContentParent.setWindowTitle(mTitle);
                }

                final int localFeatures = getLocalFeatures();
                for (int i = 0; i < FEATURE_MAX; i++) {
                    if ((localFeatures & (1 << i)) != 0) {
                        mDecorContentParent.initFeature(i);
                    }
                }

                mDecorContentParent.setUiOptions(mUiOptions);

              ......
    }

这段代码非常的长,我们从中截取关键的部分来看下这个方法做了什么。首先我们看到关键点1处,这里面有一个变量 mDecor,它是一个 DecorView 类型的变量,如果 mDecor 为空的话,就会执行 generateDecor 方法,而我们第一次加载布局文件的时候,mDecor 肯定为空,所以这个方法也就会得到执行。我们来看看这个方法里面做了什么:

protected DecorView generateDecor(int featureId) {
        // System process doesn't have application context and in that case we need to directly use
        // the context we have. Otherwise we want the application context, so we don't cling to the
        // activity.
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext().getResources());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());
    }

我们直接看到最后一个返回语句,它会返回一个 DecorView 类型的对象,这个类位于 PhoneWindow 的内部。这里要特别点出:DecorView 是继承自 FrameLayout。好了,知道了 generateDecor 方法的功能就是为 mDecor 创建一个 DecorView 类型的对象之后,我们再调过头去看看 installDecor 方法的关键点2处,如果 mContentParent 为空的话,就会调用 generateLayout 方法,我们第一次加载的话,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");
        }

        if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
            ProgressBar progress = getCircularProgressBar(false);
            if (progress != null) {
                progress.setIndeterminate(true);
            }
        }

        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            registerSwipeCallbacks(contentParent);
        }

        // Remaining setup -- of background and title -- that only applies
        // to top-level windows.
        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;
    }

同样这个方法的代码也是巨长无比,但是它的功能其实还是比较清晰的。这段代码的主要功能是根据不同的情况加载不同的布局给 layoutResource,然后返回给 mContentParent。这个布局也就是我们 DecorView 里面的布局了,我们可以拿其中一个布局的源代码来看看,例如 R.layout.screen_title,它的布局代码如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:fitsSystemWindows="true">
    <!-- Popout bar for action modes -->
    <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:layout_width="match_parent" 
        android:layout_height="?android:attr/windowTitleSize"
        style="?android:attr/windowTitleBackgroundStyle">
        <TextView android:id="@android:id/title" 
            style="?android:attr/windowTitleStyle"
            android:background="@null"
            android:fadingEdge="horizontal"
            android:gravity="center_vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
    <FrameLayout android:id="@android:id/content"
        android:layout_width="match_parent" 
        android:layout_height="0dip"
        android:layout_weight="1"
        android:foregroundGravity="fill_horizontal|top"
        android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

可以看到,它的根布局是 LinearLayout,里面包含了1个 ViewStub 和2个 FrameLayoutViewStub 其实就是我们用来显示 ActionBar 的。下面的两个 FrameLayout,上面一个是 title,用来显示标题,下面一个是 content,用来显示内容。

所以通过这个我们也就知道了,DecorView 在一般情况下,会将自身分成两个区域,一个用来显示标题,另一个用来显示内容。我们平时的布局文件,例如 activity_main,就是放置在 content 里面的,所以我们的布局文件其实一直是嵌套在一个 FrameLayout 下面的。至此,我们的源码分析就结束了。

总结一下我们的 installDecor 方法:它的作用就是产生一个 DecorView 对象,然后根据不同情况加载进不同的布局(一般情况下是分成一个 title 和一个 content)。而 installDecor 方法又是在 PhoneWindow 下的 setContentView 方法里面调用的,而这个方法又是由 ActivitysetContentView 方法调用,所以我们依照这个关系,就可以得出上面的结论图了:

这部分源码还是比较简单的吧!从 ActivitysetContentView 方法层层递进,最终就可以得出这幅图了。这部分知识是为了后面 View 的知识做一个补充的,所以相对而言比较简单,后面我将会继续介绍事件分发机制和 View 的绘制流程,如果有兴趣就来看看吧!祝你学习愉快!

猜你喜欢

转载自blog.csdn.net/qq_38182125/article/details/86546813