View的绘制(1)-setContentView源码分析

主目录见:Android高级进阶知识(这是总目录索引)

一.目标

【framework代码可以直接用Android Studio打开SDK目录下面的resouce,这里也推荐一个网址:http://androidxref.com/6.0.1_r10/xref/这个会比较完整,当然如果不涉及NDK内容SDK下的resouce还是够用】

首先讨论这个话题重点不是为了解析源码而解析源码,主要的目标有两个:
1)了解setContentView的源码
2)利用源码中的原理来实现我们的效果,主要为了引出
2.1.《SnackBar的源码分析》
2.2.《利用decorView机制实现底部弹出框》.
2.3.《小红书欢迎页的视差效果实现》
所以我会在代码的关键地方进行提醒!提醒!提醒!

《View的绘制(1)-setContentView源码分析》 fighting.jpg

二.setContentView源码分析

1.Activity setContentView

首先在分析之前我们明确一下在进入Activity的onCreate我们会调用setContentView(layoutResID)方法,所以我们第一步就是跳入到Acitivity的setContentView方法:

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

可以看到里面获取了window,然后调用了window的setcontentView方法,那么这个window到底是什么呢?

2.getWindow()

说起这个window是什么就比较复杂,需要从Acitivity的启动过程进行分析,这边就简单分析下,首先我们知道Activity最终都会在ActivityThread这个类中的performLaunchActivity进行启动。这个方法如下:

        Activity activity = null;
        try {
//利用类加载器来实例化Acitivity对象
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        } catch (Exception e) {
            if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                    "Unable to instantiate activity " + component
                    + ": " + e.toString(), e);
            }
        }

        try {
      ...
            if (activity != null) {
                Context appContext = createBaseContextForActivity(r, activity);
                CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
                Configuration config = new Configuration(mCompatConfiguration);
                if (r.overrideConfig != null) {
                    config.updateFrom(r.overrideConfig);
                }
                if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
                        + r.activityInfo.name + " with config " + config);
                Window window = null;
                if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
                    window = r.mPendingRemoveWindow;
                    r.mPendingRemoveWindow = null;
                    r.mPendingRemoveWindowManager = null;
                }
//调用其attach方法为其关联运行过程中所依赖的一系列上下文环境变量
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window);

在Activity的attach方法里,系统会创建Activity所属的Window对象并为其设置回调接口,具体方法如下:

        attachBaseContext(context);
        mFragments.attachHost(null /*parent*/);
       //从这里可以知道Activity的window是PhoneWindow
        mWindow = new PhoneWindow(this, window);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);

这样我们就知道我们getWindow()方法得到的是PhoneWindow。

《View的绘制(1)-setContentView源码分析》 不明觉厉.jpg

3.PhoneWindow setContentView

这样我们回到源代码getWindow().setContentView(layoutResID);
进入我们PhoneWindow的setContentView代码:

  // 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;

1).从代码中我们看到首先会判断mContentParent是否为空,如果为空则调用installDecor方法,如果不为空则removeAllViews即清楚所有子Views,然后通过 mLayoutInflater.inflate(layoutResID, mContentParent);方法将我们的布局放进mContentParent 中,所以我们猜想installDecor就是为了初始化我们的mContentParent,接下来我们会进行分析。
 2).然后程序会通过getCallBack()方法获取到一个CallBack对象,这里的CallBack对象又是什么呢?其实我们上面已经在Acitivity的attach方法有mWindow.setCallback(this),所以这个Callback对象其实就是Acitivity自己。
 3).当PhoneWindow接收到系统分发给它的触摸、IO、菜单等相关的事件时,可以回调相应的Activity进行处理。至于Callback可以回调哪些方法,自己看下这个接口的声明方法即可。当然了这里不是我们的关键,因为我们的setContentView里面只是回调了onContentChanged,而onContentChanged在Activity中是空实现。

4.PhoneWindow installDecor()

接下来我们就可以查看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 {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
//初始化mContentParent 对象
            mContentParent = generateLayout(mDecor);

            // 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());
                    ......
            } else {
                mTitleView = (TextView) findViewById(R.id.title);
                if (mTitleView != null) {
                         ......
                 }
            }

从代码可以看到mDecor为空的话会先生成一个DecorView对象,然后会通过DecorView对象初始化我们的mCntentParent,那这些过程到底是什么样的呢?接下来我们再来一一分析。

5.PhoneWindow generateDecor

首先我们知道这个方法是初始化一个DecorView对象,那具体到底是什么样的呢?方法如下:

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;
      .......
        return new DecorView(context, featureId, this, getAttributes());
}

代码很简单,只是new出了一个DecorView对象,我们跟进DecorView类发现

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks{
}

其实DecorView只是一个FrameLayout布局(这个DecorView是在WindowManager中attach到Window里的)。

6.PhoneWindow generateLayout

那么我们接下来就看DecorView这个对象传进generateLayout方法到底做了什么呢?

  protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.
//首先通过WindowStyle中设置的各种属性,对Window进行requestFeature或者setFlags  
        TypedArray a = getWindowStyle();

        mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
//中间省略一些代码
.................
        // 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;
        } 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);
..............
        mDecor.finishChanging();

        return contentParent;
    }

从这个方法我们可以看出根据一些feature我们代码会去加载不同的layoutResource ,我们挑一个最简单的layoutResource 来进行查看下即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" />
//这个地方就是我们set进来布局存放的地方
    <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>

我们猜想一下这个地方这个布局就是后面add到我们DecorView里面的布局,所以我们接下来进行验证下。

7.DecorView onResourcesLoaded

这个方法就是把我们的layout设置进decorview中,具体代码如下:

  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);
//看重点,这句就是inflate出我们的布局
        final View root = inflater.inflate(layoutResource, null);
        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();
    }

从代码可以看出首先会在这句final View root = inflater.inflate(layoutResource, null);把我们的布局创建出来,然后后面会加到mDecorCaptionView中,这样我们的setContentView就完成了,我们通过图片来说明刚才我们的分析过程:

《View的绘制(1)-setContentView源码分析》 布局.png

最终我们的真正的布局其实是在id为Content的framework下面的。

这个地方要提高警惕,这个知识点我们会在2.1.SnakeBar的源码分析
2.2.利用decorView机制实现dialog中用到

注意!注意!注意!

《View的绘制(1)-setContentView源码分析》 前方高能.jpg

故事到这里还没有结束,我们刚才看到了在创建我们布局的时候用到final View root = inflater.inflate(layoutResource, null),那我们这里又要分析一下inflater这个对象到底是怎么inflate出我们布局的,这个地方有个知识点到时会在
小红书视差动画实现案例中用到。

8.LayoutInflater.from(context) inflate

上面的inflater对象我们看源码都知道是从PhoneWindow中的generateLayout时传进onResourcesLoaded方法中的,所以我们直接在PhoneWindow这个类中找出这个对象,我们知道这个inflater对象会在构造函数调用的时候就进行初始化:

 public PhoneWindow(Context context) {
        super(context);
        mLayoutInflater = LayoutInflater.from(context);
    }

接下来我们跟进from方法:

  public static LayoutInflater from(Context context) {
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }

所以他其实就是从上下文对象中获取系统的一个服务,具体getSystemService方法做了些神马这个地方就不深究,我们直接看重点。直接跟进inflate方法:

 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;
//解析XML的根标签。 
            try {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }

                final String name = parser.getName();
                
                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }
//如果是merge,调用rInflate进行解析。它会把merge所有子view添加到根标签中。
                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
//如果是普通标签,调用createViewFromTag进行解析。 
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }
//用rInflate解析temp根元素下的子view。并添加到temp中。 
                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);

                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(parser.getPositionDescription()
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
//最后返回root
            return result;
        }
    }

从源码中添加的注释我们可以看到,如果xml布局中的标签是普通标签则会调用createViewFromTag方法进行解析,那么我们看看createViewFromTag方法到底做了些啥:

  View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
//此处省略一些代码,减少眼疲劳
............
            try {
            View view;
//前方高能!高能!高能!
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }

            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } catch (InflateException e) {
            throw e;

        } catch (ClassNotFoundException e) {
..........
        }
    }

我们在高能区域看到首先会判断mFactory2 ,mFactory 来进行createView即创建view,那么两个又是什么东西呢?心累60秒。。。。。

《View的绘制(1)-setContentView源码分析》 宝宝心里苦.jpg

心痛完继续前行,这两个玩意可谓是作用很大
(这两个东西会用来做拦截view的创建过程,从而可以做一些换肤和之后要说的小红书视差效果实现)是不是感觉顿时有了激情。

其实mFactory2 ,mFactory 这两个东西基本是一样的,mFactory2 是SDK>=11后引入的。v4包下有个类LayoutInflaterCompat帮我们完成了兼容性的操作。这里又有一个知识点要说
为什么我们的support包可以支持在低版本下使用高版本的控件,其实也是用的这个功能,我们从support包进去有个类AppCompatDelegateImplV9,在这里面会默认设置一个factory来拦截视图创建过程,从而达到可以使用高版本控件的能力

  @Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory(layoutInflater, this);
        } else {
            if (!(LayoutInflaterCompat.getFactory(layoutInflater)
                    instanceof AppCompatDelegateImplV9)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");
            }
        }
    }

这里如果有需要以后会分析一下support包的源码。所以我们知道我们也可以在LayoutInflater中调用setFactory设置我们的factory来进行拦截和自定义解析xml内容,我们看下fatory接口的一个说明:

 public interface Factory {
//当我们使用LayoutInflater渲染View的时候便会回调该Hook方法,从而我们可以自定义自己tag的名称
        /**
         * Hook you can supply that is called when inflating from a LayoutInflater.
         * You can use this to customize the tag names available in your XML
         * layout files.
         * 
         * <p>
         * Note that it is good practice to prefix these custom names with your
         * package (i.e., com.coolcompany.apps) to avoid conflicts with system
         * names.
         * 
         * @param name Tag name to be inflated.
         * @param context The context the view is being created in.
         * @param attrs Inflation attributes as specified in XML file.
         * 
         * @return View Newly created view. Return null for the default
         *         behavior.
         */
        public View onCreateView(String name, Context context, AttributeSet attrs);
    }

到这我们的setContentView就算是最终完成了。本篇文章主要是一个引子,希望在接下来的例子里面,大家能融会贯通!!!!

《View的绘制(1)-setContentView源码分析》 自由.jpg

    原文作者:ZJ_Rocky
    原文地址: https://www.jianshu.com/p/1c1f871cca1f
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞