你需要知道的 Android View 的创建

844 阅读10分钟
原文链接: blog.csdn.net

View的创建与绘制一向是很多人望而止步的问题。然而我们在平常的应用开发中是最经常运用到的setContentView(),我们都会用在Activity的onCreate()的时候调用setContentView()来加载编辑好的XML布局。但是实际上创建与绘制一个View,内部的实现方式的确比我们表面所编写的代码复杂得多,导致大家没能深入去了解View的创建与绘制。接下来我们一步步来了解View的创建与绘制。

在研究setContentView()方法前,我们首先先看一下这图:

这里写图片描述

上图中,DecorView是个应用窗口的最顶层View。(Decor的英文全称是Decoration,即“修饰”的意思)。DecorView只有一个子元素是垂直LinearLayout。在LinearLayout下有两个子布局,第一个是ViewStub,ViewStub就是ActionBar,它会根据theme判断有没使用ActionBar来决定是否引入ActionBar布局。第二个是FrameLayout,这就是我们应用真实使用的父布局。

大家可以通过sdk工具Hierarchy Viewer来查看验证一下ViewTree的情况。

这里写图片描述

Window类 位于/frameworks/base/core/Java/Android/view/Window.java

Window,中文解析“窗口”。它是一个宏观的概念。该类是一个抽象类,提供了绘制窗口的通用API,我们可以理解为它是一个载体。

接着我们看一下PhoneWindow,它是Android中Window的具体实现类。

PhoneWindow位于/frameworks/policies/base/phone/com/android/internal/policy/impl/PhoneWindow.java

PhoneWindow继承于Window类,我们可以通过实现具体抽象方法去绘制窗口,该类还包含DecorView内部类。我们平时调用的setContentView()方法设置Activity的用户界面时,实际上是对PhoneWindow的ViewTree的设置。

我们通过一个比喻来理解他们之间的关系。Window类相当于一幅画(抽象概念,什么画我们未知) ,PhoneWindow为一副齐白石先生的山水画(具体概念,我们知道了是谁的、什么性质的画),DecorView则为该山水画的具体内容(有山、有水、有树,各种界面)。DecorView呈现在PhoneWindow上。好了,有了这部分的认识之后,我们就开始从源码的角度去认识View的创建。

当我们自定义Activity继承Android.app.Activity时候,调用的setContentView()如下:

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

getWindow()方法返回一个PhoneWindow对象,那就是说调用的是PhoneWindow的setContentView()方法。源码如下:

@Override
public void setContentView(int layoutResID) {
    // 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) { 
        // mContentParent即为上面提到的ContentView的父容器,若为空则调用installDecor()生成
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        // mContentParent不为null,则移除decorView的所有子View
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        // 一般情况会来到这里,调用mLayoutInflater.inflate()方法来填充布局
        // 填充布局也就是把我们设置的ContentView加入到mContentParent中
        mLayoutInflater.inflate(layoutResID, mContentParent); // 2
    }
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        // 调用onContentChanged()回调方法通知Activity窗口内容发生了改变
        cb.onContentChanged();
    }
}

首先判断了mContentParent是否为null,如果为空则执行installDecor()方法,那么这个mContentParent又是什么呢?我们看一下它的注释。

// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
private ViewGroup mContentParent;

它是一个ViewGroup类型,结合2处代码,可以得知,这个mContentParent是我们设置的布局的父布局。

梳理下:Activity通过PhoneWindow的setContentView方法来设置布局,而设置布局之前,会先判断是否存在mContentParent,而我们设置的布局文件则是mContentParent的子元素。

接着我们看一下installDecor(),我们看一下PhoneWindow#installDecor:

private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor(); // 1
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor); // 2
        ...
        } 
    }
}

首先,会执行1处代码,调用PhoneWindow#generateDecor方法:

protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
}

可以看出,这里实例化了DecorView,而DecorView则是PhoneWindow类的一个内部类,继承于FrameLayout,由此可知它也是一个ViewGroup。DecorView上面我们已经对它进行过一次解析了,大家可以返回去回顾下。接下来我们看2处PhoneWindow#generateLayout方法的代码:

protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.
        // 从主题文件中获取样式信息
        // 加载TitleBar方法一
        TypedArray a = getWindowStyle();

        ...

        if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
            requestFeature(FEATURE_NO_TITLE);
        } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
            // Don't allow an action bar if there is no title.
            requestFeature(FEATURE_ACTION_BAR);
        }

        if(...){
            ...
        }

        // Inflate the window decor.
        // 加载窗口布局
        int layoutResource;
        // 加载TitleBar方法二
        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(...){
            ...
        }

        View in = mLayoutInflater.inflate(layoutResource, null);    //加载layoutResource
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); //往DecorView中添加子View,即mContentParent
        mContentRoot = (ViewGroup) in;

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); // 这里获取的就是mContentParent
        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();
        }

        // Remaining setup -- of background and title -- that only applies
        // to top-level windows.
        ...

        return contentParent;
    }

由以上代码可以看出,该方法还是做了相当多的工作的,首先根据设置的主题样式来设置DecorView的风格,比如说有没有titlebar之类的,就是解析我们为Activity设置theme的地方,至于Theme的设置:
1.我们可以在AndroidManifest里面进行设置,为我们的Activity配置相应属性,即android:theme=”“,PhoneWindow对象调用getWindowStyle()方法获取值。
2.也可以在setContentView()前调用requestFeature,指定requestFeature()指定窗口修饰符,PhoneWindow对象调用getLocalFeature()方法获取值;

对Theme操作完,我们才对layoutResource赋值的,因此我相信有不少人都曾经遇到一个错误——“requestFeature() must be called before adding content”。

接着通过对features和mIsFloating的判断,设置窗口的风格修饰为layoutResource进行赋值。得到了layoutResource以后,通过LayoutInflater.inflate()方法生成View对象。并加入到decor中。这就是为DecorView添加子View,而这里的子View则是上面提到的mContentParent。如果上面设置了FEATURE_NO_ACTIONBAR,那么DecorView就只有mContentParent一个子View,这也解释了mContentParent对象注释:mContentParent是DecorView本身或者是DecorView的一个子元素。

小结:DecorView是顶级View,内部有titlebar和contentParent两个子元素,而内部根据theme设置TitleBar,和选择系统中的布局文件,将布局文件通过inflate转化为view,加入到mDecor中;这些布局文件中都包含一个id为content的FrameLayout,将其引用返回给mContentParent。

了解完PhoneWindow#installDecor后我们接着PhoneWindow#setContentView(),看到那部分2处代码:mLayoutInflater.inflate(layoutResID, mContentParent);相信LayoutInflater大家跟setContentView()一样常用。因为在一些动态加载View和BaseAdapter适配器的代码编写中我们都会用到。我们来看一下它的代码:

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
        return inflate(parser, root, root != null);
    }

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

LayoutInflater.inflate()将上面创建的decorView作为root的参数。上面的代码比较简单,重点是return那行的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;

            try {
                // Look for the root node.
                int type;
                // 一直读取xml文件,直到遇到开始标记
                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>标签
                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 {
                    // Temp is the root view that was found in the xml
                    // 能在XML发现根View
                    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
                        // 获取父容器的布局参数(LayoutParams)
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            // 若attachToRoot参数为false,则我们只会将父容器的布局参数设置给根View
                            temp.setLayoutParams(params);
                        }
                    }

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

                    // Inflate all children under temp against its context.
                    // 递归加载根View的所有子View
                    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.
                    // 若父容器不为空且attachToRoot为true,则将父容器作为根View的父View包裹上来
                    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.
                    // 若root为空或是attachToRoot为false,则以根View作为返回值
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

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

            Trace.traceEnd(Trace.TRACE_TAG_VIEW);

            return result;
        }
    }

在上面的源码中,LayoutInflater其实就是使用Android提供的pull解析方式来解析布局文件的。首先对于布局文件中的标签进行单独处理,调用rInflate()方法来填充布局。非标签情况下会调用一个createViewFromTag()方法。从方法名的命名方式我们可以猜到这是通过xml节点来创建View对象的。

无论是标签还是非标签,它们实际都是通过xml节点来创建View对象,并添加到父布局中。

到这来setContentView()的整体执行流程我们就分析完了,至此我们已经完成了Activity的ContentView的创建与设置工作。但是我们的View还是不可见的,因为我们只是做了创建和设置加载而已。接下来就到View的绘制流程步骤。但是在测量、布局、绘制工作前还有一个步骤,那就是把DecorView添加至Window中。

每一个Activity组件都有一个关联的Window对象,用来描述一个应用程序窗口。每一个应用程序窗口内部又包含有一个View对象,用来描述应用程序窗口的视图。上文分析了创建DecorView的过程,现在则要把DecorView添加到Window对象中。而要了解这个过程,我们首先要简单先了解一下Activity的创建过程:

首先,在ActivityThread#handleLaunchActivity中启动Activity,在这里面会调用到Activity#onCreate方法,从而完成上面所述的DecorView创建动作,当onCreate()方法执行完毕,在handleLaunchActivity方法会继续调用到ActivityThread#handleResumeActivity方法,我们看看这个方法的源码:

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) { 
    //...
    ActivityClientRecord r = performResumeActivity(token, clearHide); // 这里会调用到onResume()方法

    if (r != null) {
        final Activity a = r.activity;

        //...
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow(); // 获得window对象
            View decor = r.window.getDecorView(); // 获得DecorView对象
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager(); // 获得windowManager对象
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (a.mVisibleFromClient) {
                a.mWindowAdded = true;
                wm.addView(decor, l); // 调用addView方法
            }
            //...
        }
    }
}

在该方法内部,获取该activity所关联的window对象,DecorView对象,以及windowManager对象,而WindowManager是抽象类,它的实现类是WindowManagerImpl,所以后面调用的是WindowManagerImpl#addView方法,我们看看源码:

public final class WindowManagerImpl implements WindowManager {    
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    ...
    @Override
    public void addView(View view, ViewGroup.LayoutParams params) {
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
}

接着调用了mGlobal的成员函数,而mGlobal则是WindowManagerGlobal的一个实例,那么我们接着看WindowManagerGlobal#addView方法:

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            ...

            root = new ViewRootImpl(view.getContext(), display); // 1

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView); // 2
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
    }

先看1代码处,实例化了ViewRootImpl类,接着,在2处代码,调用ViewRootImpl#setView方法,并把DecorView作为参数传递进去,在这个方法内部,会通过跨进程的方式向WMS(WindowManagerService)发起一个调用,从而将DecorView最终添加到Window上。这个过程中ViewRootImpl、DecorView和WMS会彼此关联,至于详细过程这里不展开来说了。

最后通过WMS调用ViewRootImpl#performTraverals方法开始View的测量、布局、绘制流程,这将在下篇继续分析View的绘制。下面一张图总结我们View的创建的整套流程,大家可以通过下图来对上面的知识分析进行再次理解。

这里写图片描述