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

作者: ZJ_Rocky | 来源:发表于2017-09-03 15:31 被阅读585次

    主目录见: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.《小红书欢迎页的视差效果实现》
    所以我会在代码的关键地方进行提醒!提醒!提醒!

    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。


    不明觉厉.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就完成了,我们通过图片来说明刚才我们的分析过程:

    布局.png
    最终我们的真正的布局其实是在id为Content的framework下面的。
    这个地方要提高警惕,这个知识点我们会在2.1.SnakeBar的源码分析
    2.2.利用decorView机制实现dialog中用到
    注意!注意!注意!
    前方高能.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秒。。。。。

    宝宝心里苦.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就算是最终完成了。本篇文章主要是一个引子,希望在接下来的例子里面,大家能融会贯通!!!!


    自由.jpg

    相关文章

      网友评论

      本文标题:View的绘制(1)-setContentView源码分析

      本文链接:https://www.haomeiwen.com/subject/zdadjxtx.html