你需要知道的Android View的创建

来源:互联网 发布:电吉他 知乎 编辑:程序博客网 时间:2024/06/05 10:13

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()方法。源码如下:

@Overridepublic 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的创建的整套流程,大家可以通过下图来对上面的知识分析进行再次理解。

这里写图片描述

0 0