View创建的那些事儿

时间:2022-02-19 14:47:14

文章目录:

本来是想总结view的绘制测量过程,但是一开始就来个measure、layout、draw总感觉让人有点丈二的和尚摸不着头脑。什么时候添加的这个view?谁控制添加的?它为什么能控制添加?这个view现在在哪呢?等等一系列问题都出来了。

理解几个概念

在学习研究View创建过程先明白几个概念,帮助后边理清思路,这些包括Activity、ActivityThread、Window、PhoneWindow、WindowManagerService、WindowManager、ViewRootImpl、surface、DecorView。这些都是与view有关的,大部分都是framework层,这里简单介绍一下各自的作用,为后边view的创建及绘制渲染做准备。

  • Activity:这个都认识,四大组件之一主要用于与用户进行交互处理,每个 Activity 都会获得一个用于绘制其用户界面的窗口,来呈现各种各样的布局。
  • ActivityThread:主线程或UI线程,主要的作用是根据AMS(ActivityManagerService)负责调度和执行activities、broadcasts和其它操作。
  • Window:往虚的说是视图窗口,往实的说是一个抽象类。它提供了一套标准的UI方法,比如添加背景,标题等等。Window 有三种类型,分别是应用 Window、子 Window 和系统 Window。应用类 Window 对应一个 Acitivity,子 Window 不能单独存在,需要依附在特定的父 Window 中,比如常见的一些 Dialog 就是一个子 Window。
  • PhoneWindow:Window的实现类,可以通过实现具体抽象方法去绘制窗口。DecorView作为该类的成员变量,可以在此类中获取DecorView的实例。
  • DecorView:FramLayout的子类,作为窗口中最顶层的view,起到一个基础框架作用。内部包含对view的measure、layout、draw、onAttachedToWindow、以及触摸事件处理等等所有与view绘制相关的功能。
  • ViewRootImpl:作为WindowManager和DecorView之间的纽带,主要作用是协助WindowManager将DecorView添加到Window中,并获取顶层视图的MeasureSpec,然后对ViewGroup和子View进行迭代测量绘制,最终将View展现出来。
  • WindowManager:是一个接口,主要用来管理窗口的一些状态、属性、view增加、删除、更新、窗口顺序、消息收集和处理等,继承自接口ViewManager(内含有三个方法addView、updateViewLayout、removeView),由WindowManagerImpl来进行实现。
  • WindowManagerGlobal:WindowManagerImpl内部成员变量,是一个WindowManager全局工具类,WindowManager中View的增加、删除、更新以及WindowManagerService实例的获取等最终处理都是在此工具类中处理的。
  • WindowManagerService:位于 Framework 层的窗口管理服务,它的职责就是管理系统中的所有窗口的排布和次序。
  • Surface:源码给出的解释Handle onto a raw buffer that is being managed by the screen compositor,Surface中的Canvas成员是专门用于供程序员画图的场所,就像黑板一样;其中的原始缓冲区是用来保存数据的地方;Surface本身的作用类似一个句柄,得到了这个句柄就可以得到其中的Canvas、原始缓冲区以及其它方面的内容,每一个window都对应一个surface,作为窗口的“底板”,窗口上各种绘制排版都是在此底板上展现的。由WindowManagerService进行分配。

屏幕显示过程

上边提到了Surface扮演着在一块窗口上管理绘图数据的角色。WindowManagerService职责就是管理系统中的所有窗口,每添加一个窗口的过程,其实就是 WindowManagerService 为其分配一块 Surface 的过程,一块块的 Surface 在 WindowManagerService 的管理下有序的排列在屏幕上,Android 才得以呈现出多姿多彩的界面。
View创建的那些事儿

UI界面层级结构

Activity作为四大组件之首承载着view的展示和处理的角色。无论是触发桌面Launcher还是startActivity最终的启动过程都是由ActivityThread来负责支配的。界面启动后,Activity作为一个载体承载window,然后各种视图依次展现。
上边提到数据缓存区创建因而也就能得到一个window的显示区域,在window内部中又有各种各样的view来呈现多姿多彩的内容,DecorView便是这颗ViewTree的根节点,DecorView包含两部分,TitleView和ContentView,TitleView受主题的限制有时我们不需要显示他,我们自己定义的主要内容显示在ContentView中。
UI界面架构图:
View创建的那些事儿
ViewTree结构图:
View创建的那些事儿
ContentView中展示的View有两种形式,View和ViewGroup,其实最终都归为View,下边是一副view的拓展图,可以帮助更好的理解各种view的来源。
View创建的那些事儿

从setContentView说起

这是一个老生常谈的问题,这里都认识更容起步,setContentView这一部分是将布局文件加载到DecorView中,不涉及DecorView如何填充到Window中(后续再说这一部分)。
当我们自定义Activity继承自android.app.Activity时候,调用的setContentView()方法是Activity类的,源码如下:

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

这里的getWindow()就是PhoneWindow,走的就是PhoneWindow的setContentView()方法。而PhoneWindow是在Activity的attach()方法中进行实例化的:

    final void attach(Context context,...) {
//绑定上下文
attachBaseContext(context);

mFragments.attachHost(null /*parent*/);

//创建Window的实现类PhoneWindow
mWindow = new PhoneWindow(this, window);
mWindow.setWindowControllerCallback(this);
//绑定回调监听,一旦window有相关动作回调给Activtiy
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);

......

//设置WindowManager
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if (mParent != null) {
mWindow.setContainer(mParent.getWindow());
}
//通过getWindowManager获取WindowManager实例WindowManagerImpl
mWindowManager = mWindow.getWindowManager();
mCurrentConfig = config;
}

01#PhoneWindow#setContentView()

先看一个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) {
//如果父容器空,初始化父容器
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
//如果父容器不为空,移除所有子View
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()) {
//回调方法通知Activity窗口内容发生了改变
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}

setContentView最重要的两件事:第一初始化父容器;第二加载布局文件。接着看一下installDecor()方法内部怎么实现的:

    private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
//1.实例化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) {
//2.生成父容器
mContentParent = generateLayout(mDecor);
......
}
}

在初始installdecor()方法中同样做了重要的两件事:第一实例化DecorView;第二生成父容器。实例DecoView很简单,源码就是new出来一个,哈哈。关键第二步生成父容器涉及了针对DecorView主题以及TitleView的标题、标题Icon等设定做好多工作。这里可以粗略的看几个重点:

    protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
//获取系统Window主题资源
TypedArray a = getWindowStyle();

......
//根据主题资源设定父窗口样式
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
& (~getForcedWindowFlags());
if (mIsFloating) {
setLayout(WRAP_CONTENT, WRAP_CONTENT);
setFlags(0, flagsToUpdate);
} else {
setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
}
.......

//设置系统状态栏
if (a.getBoolean(R.styleable.Window_windowLightStatusBar, false)) {
decor.setSystemUiVisibility(
decor.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}

......

return contentParent;
}

至此,PhoneWindow中setContentView()的初始化父容器过程完毕,接下来分析布局资源文件加载过程。

02#PhoneWindow#mLayoutInflater.inflate()

PhoneWindow的setContentView()方法中调用了LayoutInflater的inflate()方法来填充布局,这个方法的源码如下:

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, 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) + ")");
}

//获取Xml解析对象
final XmlResourceParser parser = res.getLayout(resource);
try {
//进一步加载布局资源
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}

这个方法的主要作用就是为了拿到XmlResourceParser实例对象进行接下来的xml解析,平时我们动态加载自定义view最终也是走的这个方法,接下来看xml解析做的工作:

    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;
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("**************************");
}


//优先处理<morge/>标签
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");
}
//解析<morge/>标签下的view并将最终添加到root父容器中
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the 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
//获取父容器的layout params
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的setLayoutParams方法,便于后边的直接返回。
temp.setLayoutParams(params);
}
}

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

// Inflate all children under temp against its context.
//根据rootView解析所有孩子的布局文件,并将解析的View依次递归合并到temp中
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) {
//将最终合并完成的子View(即大儿子temp)添加到root父容器中
root.addView(temp, params);
}

// Decide whether to return the root that was passed in or the
// top view found in xml.
//如果父容器为空或者attachToRoot为false直接将所有子view合并后的大儿子temp返回
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);
}

return result;
}
}

这部分主要功能就是解析xml,预先针对父容器通过createViewFromTag()方法生成临时的View(temp),然后并将解析后子View依次合并到tempz中,最终添加到父容器中并返回,有些时候是没有父容器的,例如我们自定义view的动态加载,因此只需要把这部分xml解析组合成View后直接返回即可。接下来看一下递归解析孩子的rInflateChildren()方法:

    final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

rInflateChildren()内部进一步调用的rInflate()方法:

    void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

final int depth = parser.getDepth();
int type;

//开始进行解析
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

//起始结点标识不是起始标识返回进行下一个结点的解析
if (type != XmlPullParser.START_TAG) {
continue;
}

//获取结点标签名字
final String name = parser.getName();

if (TAG_REQUEST_FOCUS.equals(name)) {
//解析requestFocus标签
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
//解析tag标签
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
//解析include标签
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
//处理morge标签,这里直接抛出异常,因为morge标签只能存在最初父级结点上
throw new InflateException("<merge /> must be the root element");
} else {
//如果是其他标签则根据标签生成View,并添加ViewGroup中,这部分进行递归遍历结点之下的结点
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}

if (finishInflate) {
parent.onFinishInflate();
}
}

这个过程主要处理子View的xml文件的解析,内部针对四种特殊标签单独处理,然后如果标签下仍然有子view,则继续递归处理。每次根据标签解析出来的view是由createViewFromTag()方法生成的。

    private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {

//如果标签是view,拿到view标签上的class值
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}

// Apply a theme wrapper, if allowed and one is specified.
//如果该标签与主题相关,需要对context进行包装,将主题信息加入context包装类ContextWrapper
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}

//如果是blink标签返回一个BlinkLayout,它包含的内容会一直闪烁。
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}

//设置Factory,来对View做额外的拓展
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);
}

//如果此时不存在Factory,不管Factory还是Factory2,还是mPrivateFactory都不存在,那么会直接对name直接进行解析
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
//如果name中包含.即为自定义View,否则为原生的View控件
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) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;

} catch (Exception e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}

这部分内容是最终的解析部分,无论是自定义view还是原生的view都能进行处理,细心的你会发现最终生成view的是onCreateView()方法,其内部其实是creatView(),这是LayoutInflater默认生成view的方式,内部原理是类加载器根据前缀+标签名字去加载相应的对象,有兴趣的同学可看一下这部分源码,共同学习。
View创建的那些事儿
到此View创建添加到DecorView这一过程完毕,下部分是DecorView添加至Window过程,然后接下来进行view的测量、布局、绘制,这一整套走完之后才能见到庐山真面目了。

总结

文中的源码是android7.0源码。本文主要描述的是view创建添加到DecorView过程,从熟知的setContentView()内部分析大的方面经历的两部分:
- PhoneWindow的setContentView()
+ 初始化父容器
- 实例化DecorView
- 生成父容器ContentParent(包含主题、title、titleIcon等对父容器的设置)
+ 布局资源文件的加载,直接引申到第二部分
- PhoneWindow的mLayoutInflater.inflate()。
+ 获取XmlResourceParser实例对象
+ 通过inflate进一步加载解析
- 处理父容器结点(优先处理morge标签)
- 递归处理子节点
- 递归addView(),最终指向父容器的addView()并返回。