搜索
您的当前位置:首页正文

View工作原理三:从setContentView说起

来源:哗拓教育

自定义布局,各种自定义View是如何添加到手机屏幕上面的?
通常我们都是在Activity中使用setContentView方法设置自定义布局,当运行到对应的Activity的时候,屏幕上显示的就是我们的自定义布局。

开发过程中我们并没有直接接触到实际的Activity的页面结构,我们的开发都是基于系统提供的Api。实际的手机屏幕的控件组织结构如图:


Avtivity的页面结构图

我们平常开发的setContentView的自定义布局就是添加到上图的ContentView中。

Activity的setContentView(...)

自定义的Activity都是继承Activity的。想要搞清楚页面的结构,我们就从开发中接触最多的Activity类的setContentView(...)入手。
Activity中的平时使用最多的是setContentView(@LayoutRes int layoutResID)
Activity#setContentView

    // Activity中onCreate调用的setContentView方法
    public void setContentView(@LayoutRes int layoutResID) {
    //    getWindow()获得的是mWindow对象
    //    mWindow = new PhoneWindow(this, window);
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

    final void attach(...) {
        ...
        mWindow = new PhoneWindow(this);
        ...

Activity的setContentView是调用了Window的对象的setContentView,PhoneWindow是Window的具体实现类,是最基本的窗口系统,每个Activity 均会创建一个PhoneWindow对象,是Activity和整个View系统交互的接口。

PhoneWindow的setContentView(...)

PhoneWindow#setContentView:

 @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
        //初始化DecorView
            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 {
      //1***将对应的布局id转成view,然后加入mContentParent中
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback(); //当前的Activity对象
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

DecorView继承了FrameLayout,一般情况下,它会在先添加一个预设的布局mContentParent。
我们关心的是自定义布局是如何加载到手机屏幕上的即源码中//1:
mLayoutInflater.inflate(layoutResID, mContentParent);
所以PhoneWindow#setContentView主要针对如下分析:

  • mContentParent父容器DecorView的初始化
  • mContentParent的获取

先来看看DecorView的初始化PhoneWindow#installDecor()方法:

 private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
       //generateDecor()调用了DecorView 的构造方法构建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) {
        //根据features生成Activity的layout资源的父节点
            mContentParent = generateLayout(mDecor);
         ...
    }

PhoneWindow#installDecor()不仅初始化mDecor 对象,而且也获得mContentParent 对象。

先看看mDecor对象的初始化PhoneWindow#generateDecor:

    protected DecorView generateDecor(int featureId) {
        Context context;
        //获取Context对象调用DecorView的构造方法
         ...
        return new DecorView(context, featureId, this, getAttributes());
    }

主要过程就是获取Context对象,然后调用DecorView的构造方法创建DecorView对象。

mContentParent的获取过程比较麻烦,实现方法是PhoneWindow#generateLayout首先根据Theme和Style计算features,然后根据features获得不同布局加载到DecorView下,然后从该布局中获得id是content的View就是mContentParent。

PhoneWindow#generateLayout代码:

    protected ViewGroup generateLayout(DecorView decor) {
 //根据Theme和Style计算features
    ...
//根据属性给DecorView设置样式
     if (a.getBoolean(R.styleable.Window_windowLightStatusBar, false)) {
            decor.setSystemUiVisibility(
                    decor.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
        }
       ...
//根据不同的情况加载DecorView的布局
        int layoutResource;
        int features = getLocalFeatures();
// 根据features计算layoutResource,layoutResource是DecorView中的子布局
        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            layoutResource = R.layout.screen_swipe_dismiss;
        } 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;
            }
            removeFeature(FEATURE_ACTION_BAR);
        } 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;
            }
        } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
            layoutResource = R.layout.screen_simple_overlay_action_mode;
        } else {
            layoutResource = R.layout.screen_simple;
        }
//*********************************************************************************************
//*********************************************************************************************
        mDecor.startChanging();
// layoutResource对应的View加到 DecorView中,并赋值给DecorView中的mContentRoot
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);


 //layoutResource对应的布局文件,它其中有一个ID叫做ID_ANDROID_CONTENT的ViewGroup
 //找到该ViewGroup,赋值给contentParent,contentParent就表示我们Activity的layout资源文件的父布局
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }
  ...
        mDecor.finishChanging();
        return contentParent;
    }

一开始的图中DecorView的TitleView和ContentView就是PhoneWindow#generateLayout中获得的布局layoutResource中的子节点。
DecorView里面TitleView:标题,可以设置requestWindowFeature(Window.FEATURE_NO_TITLE)
取消掉 ;ContentView:是一个id为content的FrameLayout。 我们平常在Activity使用的setContentView就是设置在这里。

简单看下DecorView中子节点的某个布局
layoutResource = R.layout.screen_simple;的时候
R.layout.screen_simple的布局:

 <LinearLayout 
    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>

到这里PhoneWindow中的DecorView属性初始化,DecorView中的mContentRoot的初始化,mContentRoot中的mContentParent初始化已经完成,并且开发者自定义的布局也已经添加到mContentParent中。(我们给Activity设置的布局就是添加到mContentParent中的),但是此时的DecorView只是PhoneWindow中的属性还不能真正显示到屏幕。所以我们接着看DecorView是如何展示到屏幕上的。

DecorView添加到Window

先来看看Window

Window中有一个重要的类WindowManager,简单理解它就是Window的管理者。
WindowManager的创建Window#setWindowManager

public void setWindowManager(WindowManager wm, 
    IBinder appToken, 
    String appName, 
    boolean hardwareAccelerated) {
        ...
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
            //获取了一个WindowManager
        }
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }

上面获取到的WindowManager实际是WindowManagerImpl类型的。

然后Window#setWindowManager的调用是在Activity#attach(),attach()方法是在onCreate()之前被调用的。
Activity#attach

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){
        ...
        mWindow = new PhoneWindow(this, window);
        //创建Window
        ...
        mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
         mToken, mComponent.flattenToString(),
         (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        //注意!这里就是在创建WindowManager。
        //这个方法在前面已经说过了。
        if (mParent != null) {
           mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
 }

到这里我们基本就明白了WindowManager的创建,并且它是WindowManagerImpl类型

我们再来看看ActivityThread

ActivityThread#handleLaunchActivity中启动Activity,它会调用到Activity#onCreate方法,从而完成上面所述的DecorView创建动作,当onCreate()方法执行完毕,handleLaunchActivity方法会继续调用到ActivityThread#handleResumeActivity,它会执行对应Activity中的onResume,在onResume()后界面就要开始渲染了。

    final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String 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;
            ...
                if (a.mVisibleFromClient && !a.mWindowAdded) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);
                }
        ...
    }

WindowManagerImpl是WindowManager的具体实现类,WindowManagerImpl的addView(...)调用WindowManagerGlobal的addView(...),简单理解WindowManagerImpl就是一个代理类。
WindowManagerGlobal#addView:

public final class WindowManagerGlobal {
    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {    
            ...
            //创建ViewRootImpl
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            //DecorView和ViewRootImpl建立联系
            root.setView(view, wparams, panelParentView);
    }
}

就这样将DecorView添加到ViewRootImpl中。
当View添加到ViewRootImpl中相对应的触发执行measure,layout,draw等操作并最终将布局展示到页面。

Top