视图XML加载过程
Android-activity中我们使用setContentView函数给Android设置View对应的XML的id,那么这个视图是怎么被加载的呢?
再说加载时,我们还需要先看看Actvity中的视图层级
实际上我们设置的视图并不是最根本的视图,下面还有好几层视图。具体如下图所示:
下面我们就看这些是如何加载的
加载流程
1. acitivyt的创建
这里需要说明以下activity本身并不是视图,这里只是为了更好的说明他们的关系,所以家avtivity加入进去
class android.app.ActivityThread.java
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent){
...
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
...
if (activity != null) {
//这里给activity设置基础参数
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, r.configCallback,
r.assistToken, r.shareableActivityToken);
}
}
class android.app.Activity.java
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
IBinder shareableActivityToken) {
....
//创建Window---PhoneWindwo,这里可以看出来Activity中的Window实际上就是PhoneWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
mWindow.setUiOptions(info.uiOptions);
}
mUiThread = Thread.currentThread();
...
//绑定WindowWanage
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
}
这里可以看出来Activity中都包含一个Window它是PhoneWindow,这里要说以下activity是没有显示能力的,他的是显示都是借助PhoneWindow
setContontView
下面我们回过来从我们最长用的setContent开始分析:(下列源码中省略了与当前流程相关性不高的代码,如果需要看源码可以通过提供的类全类名自行去找)
class android.app.Activity.java
public void setContentView(@LayoutRes int layoutResID) {
//这里的getWindow获取的就是activity中的mWindow成员变量,也是就上面加入的PhoneWindow
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
class com.android.internal.policy.PhoneWindow.java
@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();//这里就是创建上图中DecorView,源码见下面
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {//执行动画操作
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//将layoutResID设置进ContentParent
mLayoutInflater.inflate(layoutResID, mContentParent);
}
······
}
private void installDecor() {
....
if (mDecor == null) {//这里初始是null
mDecor = generateDecor(-1);//这里创建DecorView
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);//这里创建DecorView中ContentParent (严紧来说ContentParent是获取DecorView中的一个控件),所以DecorView关系就是DecorView包含ContentParent
}
....
}
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.
....
//创建DecorView 对象,里面使用的layout是R.layout.decor_caption
return new DecorView(context, featureId, this, getAttributes());
}
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
//设置一些参数(还有很多设置参数被省略)
if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) {
requestFeature(FEATURE_ACTION_BAR_OVERLAY);
}
//这里是一些默认的布局,就是android studio在创建activity是可以选择很多初始布局
int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
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;
}
}
....
mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//获取mContentParent ID_ANDROID_CONTENT 定义: public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
return contentParent;
}
这里就看出来PhoneWindow中包含了DecirView,DecirView包含了mContentParent
inflate(layoutResID, mContentParent)
上面可以看到xml的设定是通过该函数,下面我们更加深入的看看是如何设置的
class android.view.LayoutInflater
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) + ")");
}
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
//获取xml解析器
XmlResourceParser parser = res.getLayout(resource);
try {
//解析XML创建View
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
View result = root;
try {
advanceToRootNode(parser);
final String name = parser.getName();
//TAG_MERGE = "merge",判断布局文件是否时一个merge
if (TAG_MERGE.equals(name)) {
//如果布局是一个merge 那么他必须有父布局,并且attachToRoot==true
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
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);
//1.如果attachToRoot==false加入xml的布局配置例如内边距,位置等信息
//2.这里需要注意的时 这里的前提时root!=null,如果root==null,那么这个布局的边距等信息就不会生效
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
// Inflate all children under temp against its context.
//创建布局中的子布局
rInflateChildren(parser, temp, attrs, true);
//如果存在父布局并且attachToRoot是true,那么将这个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.
//如果父布局不存在那么直接返回创建的View
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(
getParserStateDescription(inflaterContext, attrs)
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
}
/**
*这里就可以看出来如果attachToRoot==false 或者root==null则result就是我们这个xml
*创建的View,如果attachToRoot==true 并且root!=null则会将我们的布局放入父布局并返回
*父布局
*/
return result;
}
}
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
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)) { // TAG_REQUEST_FOCUS = "requestFocus"
pendingRequestFocus = true;
//里面只是跳过requestFocus标签中的子布局,并不会绘制,因为该标签是用于指定屏幕内的焦点View
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) { //TAG_TAG = "tag"
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) { //TAG_INCLUDE = "include"
if (parser.getDepth() == 0) {//这里判断了以下include是否为当前xml的根标签,如果是则抛出异常
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {//TAG_MERGE = "merge",merge标签只能作为标签的根标签
throw new InflateException("<merge /> must be the root element");
} else {
//创建View
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 (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
parent.onFinishInflate();
}
}
//解析Include
@UnsupportedAppUsage
private void parseInclude(XmlPullParser parser, Context context, View parent,
AttributeSet attrs) throws XmlPullParserException, IOException {
int type;
//如果父控件不是ViewGroup 则抛出异常
if (!(parent instanceof ViewGroup)) {
throw new InflateException("<include /> can only be used inside of a ViewGroup");
}
// Apply a theme wrapper, if requested. This is sort of a weird
// edge case, since developers think the <include> overwrites
// values in the AttributeSet of the included View. So, if the
// included View has a theme attribute, we'll need to ignore it.
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
final boolean hasThemeOverride = themeResId != 0;
if (hasThemeOverride) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
// If the layout is pointing to a theme attribute, we have to
// massage the value to get a resource identifier out of it.
int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
if (layout == 0) {
final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
if (value == null || value.length() <= 0) {
throw new InflateException("You must specify a layout in the"
+ " include tag: <include layout=\"@layout/layoutID\" />");
}
// Attempt to resolve the "?attr/name" string to an attribute
// within the default (e.g. application) package.
layout = context.getResources().getIdentifier(
value.substring(1), "attr", context.getPackageName());
}
// The layout might be referencing a theme attribute.
if (mTempValue == null) {
mTempValue = new TypedValue();
}
if (layout != 0 && context.getTheme().resolveAttribute(layout, mTempValue, true)) {
layout = mTempValue.resourceId;
}
if (layout == 0) {
final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
throw new InflateException("You must specify a valid layout "
+ "reference. The layout ID " + value + " is not valid.");
}
final View precompiled = tryInflatePrecompiled(layout, context.getResources(),
(ViewGroup) parent, /*attachToRoot=*/true);
if (precompiled == null) {
final XmlResourceParser childParser = context.getResources().getLayout(layout);
try {
final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
final String childName = childParser.getName();
if (TAG_MERGE.equals(childName)) {//当导入的布局时marge时
// The <merge> tag doesn't support android:theme, so
// nothing special to do here.
rInflate(childParser, parent, context, childAttrs, false);
} else {
final View view = createViewFromTag(parent, childName,
context, childAttrs, hasThemeOverride);
final ViewGroup group = (ViewGroup) parent;
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.Include);
final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
final int visibility = a.getInt(R.styleable.Include_visibility, -1);
a.recycle();
// We try to load the layout params set in the <include /> tag.
// If the parent can't generate layout params (ex. missing width
// or height for the framework ViewGroups, though this is not
// necessarily true of all ViewGroups) then we expect it to throw
// a runtime exception.
// We catch this exception and set localParams accordingly: true
// means we successfully loaded layout params from the <include>
// tag, false means we need to rely on the included layout params.
ViewGroup.LayoutParams params = null;
try {
params = group.generateLayoutParams(attrs);
} catch (RuntimeException e) {
// Ignore, just fail over to child attrs.
}
if (params == null) {
params = group.generateLayoutParams(childAttrs);
}
view.setLayoutParams(params);
// Inflate all children.
rInflateChildren(childParser, view, childAttrs, true);
//如果这个Include标签有ID那么将Include导入XML的root标签改为Include的ID
//注意:也就是所当Include有ID时它导入的xml的root标签的原有id就会失效
if (id != View.NO_ID) {
view.setId(id);
}
//添加进父布局
group.addView(view);
}
} finally {
childParser.close();
}
}
LayoutInflater.consumeChildElements(parser);
}
@UnsupportedAppUsage
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
@UnsupportedAppUsage
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
try {
//创建View,这里是使用预设工厂来创建的,如果预设工厂是null那么返回null
View view = tryCreateView(parent, name, context, attrs);
//如果没有预设工厂处理则使用createView处理
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
//这里的点是判断是否为自定义控件,如果这个标签中有点表示自定义控件
//例如:
//LinearLayout没有“.”表示非自定控件
//androidx.constraintlayout.widget.ConstraintLayout 中有点“.”表示这个是自定义控件
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);//onCreateView中实际上也是使用的createView
} else {
view = createView(context, name, null, attrs);
}
}
}
//返回创建的View
return view;
}
}
//这里是使用预设View解析工厂来创建View,我们可以自己实现这些工厂,来控制View的创建(实现换肤)
//有三个工厂
//1. mFactory //只能拿到自身信息
//2. mFactory2 //可以获取到父布局信息
//3. mPrivateFactory //融合了mFactory与mFactory2
public final View tryCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context,
@NonNull AttributeSet attrs) {
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
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);
}
return view;
}
public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable AttributeSet attrs)
throws ClassNotFoundException, InflateException {
static final Class<?>[] mConstructorSignature = new Class[] {
Context.class, AttributeSet.class};
//获取两个参数的构造函数
constructor = clazz.getConstructor(mConstructorSignature);
Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = viewContext;
Object[] args = mConstructorArgs;
args[1] = attrs;
try {
//反射创建。所以一般在自定义View是都不能缺少这个构造函数
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;
} finally {
mConstructorArgs[0] = lastContext;
}
}
通过分析inflate我们可以知道
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)参数作用
- resource
- resource的根时merge时,必须root!=null attachToRoot==true, 否则抛出异常
- resource的根不能是Include
- root与attachToRoot
- **root==null&&attachToRoot==false:**resource不会加入父布局,并且resource的根节点设置的参数无效
- root==null&&attachToRoot==true: resource不会加入父布局,并且resource的根节点设置的参数无效
- root!=null&&attachToRoot==false: resource不会加入父布局,但是resource的根节点设置的参数有效,当他实际加入某一个布局时生效
- root!=null&&attachToRoot==true: resource加入父布局,并且resource的根节点设置的参数有效
同时我们还了解到可以通过设置mFactory、mFactory2、mPrivateFactory来控制View的创建
这三个工厂的优先级时mFactory2>mFactory>mPrivateFactory
创建View的流程概括
- 创建Activivty,并创建PhoneWindow
- Activivty调用setContentView-->实际调用也就是PhoneWindow中setContentView
- installDecor();//加载DecorView,以及ContentParent
- mLayoutInflater.inflate(layoutResID, mContentParent)-->inflate(resource, root, root != null)-->inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
- 错误检查(主要的,实际上还有很多):
- merge必须是根并且root == null || !attachToRoot
- include必须有父布局并且必须是ViewGroup
- 加载ViewcreateViewFromTag
- tryCreateView 首先检查是否存在自定义创建View的工厂,如果有则使用自定义的工厂
- 如果工厂不存在则调用createView(使用反射创建View)
- installDecor();//加载DecorView,以及ContentParent
- Activivty调用setContentView-->实际调用也就是PhoneWindow中setContentView
评论区