Android LayoutInflater&LayoutInflaterCompat源码解析
Posted Sun‘刺眼的博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android LayoutInflater&LayoutInflaterCompat源码解析相关的知识,希望对你有一定的参考价值。
本文分析版本: android API 23,v4基于 23.2.1
1 简介
实例化布局的XML文件成相应的View对象。它不能被直接使用,应该使用
getLayoutInflater()
或getSystemService(Class)
来获取已关联了当前Context
并为你正在运行的设备正确配置的标准LayoutInflater实例对象。 例如:
LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
为了创建一个对于你自己的View来说,附加了
LayoutInflater.Factory
的LayoutInflater
,你需要使用cloneInContext(Context)
来克隆一个已经存在LayoutInflater
,然后调用setFactory(LayoutInflater.Factory)
来替换成你自己的Factory。由于性能原因,View的实例化很大程度上依赖对于xml文件在编译时候的预处理。因此,目前使用
LayoutInflater
不能使用直接通过原始xml文件获取的XmlPullParser
,只能使用一个已编译的xml资源返回的XmlPullParser
((R.something file.)。
2 获取LayoutInflater的三种方式:
LayoutInflater inflater = getLayoutInflater();
//调用Activity的getLayoutInflater()LayoutInflater inflater = LayoutInflater.from(context);
LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
但是,这三种方式本质上还是一样的:
-
getLayoutInflater()
调用的还是Activity中的方法:public LayoutInflater getLayoutInflater() { return getWindow().getLayoutInflater(); }
其中
Window
是一个抽象类,它 唯一 实现类是PhoneWindow
。com/android/internal/policyPhoneWindow.java
public PhoneWindow(Context context) { super(context); mLayoutInflater = LayoutInflater.from(context); } /** * Return a LayoutInflater instance that can be used to inflate XML view layout * resources for use in this Window. * * @return LayoutInflater The shared LayoutInflater. */ @Override public LayoutInflater getLayoutInflater() { return mLayoutInflater; }
可以看到,通过·
Activity
的getLayoutInflater()
最终调用的还是第二种方法。 -
通过
LayoutInflater.from(context);
获取LayoutInflater
对象。android/view/LayoutInflater.java
/** * Obtains the LayoutInflater from the given context. */ public static LayoutInflater from(Context context) { //通过获取系统服务的方式获取到LayoutInflater实例对象 LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); if (LayoutInflater == null) { throw new AssertionError("LayoutInflater not found."); } return LayoutInflater; }
以上两种方法,最终还是调用了context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
获取LayoutInflater实例对象。
最终:不管以什么样的方式获取到LayoutInflater对象,最终都是通过系统服务来获取实例对象。
3 主要方法预览
-
public static LayoutInflater from(Context context);
获取LayoutInflater实例化对象。
-
public void setFactory(Factory factory);
设置使用当前LayoutInflater创建View的自定义实例化工厂。通过设置自定义工厂,可以在系统实例化View的时候进行一些拦截操作,比如可以把本来的TextView拦截成Button、给TextView统一指定字体等。
-
public void setFactory2(Factory2 factory);
同上,区别是工厂2多了对实例化View的时候Parent的支持。在API 11引入。
-
public void setFilter(Filter filter);
给当前LayoutInflater设置过滤器,如果要被填充的View不被这个过滤器允许,则会抛出InflateException。这个过滤器会覆盖当前LayoutInflater之上的任何之前设置过的过滤器。
-
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root);
把指定的布局资源填充成View。如果root不为空则把填充的View添加到root上,如果root为空则不添加。
-
public View inflate(XmlPullParser parser, @Nullable ViewGroup root);
通过布局xml资源的解析器把布局资源填充成View。如果root不为空则把填充的View添加到root上,如果root为空则不添加。
-
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot);
把指定的布局资源填充成View。如果root不为空并且attachToRoot为true,则把填充的View添加到root上,否则不添加。
-
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot);
通过布局xml资源的解析器把布局资源填充成View。如果root不为空并且attachToRoot为true,则把填充的View添加到root上,否则不添加。
-
public final View createView(String name, String prefix, AttributeSet attrs);
通过View的名称,前缀和attrs属性实例化View。
-
protected View onCreateView(String name, AttributeSet attrs);
通过View的父View,View名称和attrs属性实例化View(最终调用的是
createView(String name, String prefix, AttributeSet attrs);
)。 -
protected View onCreateView(View parent, String name, AttributeSet attrs);
通过View的父View,View名称、前缀和attrs属性实例化View(最终调用的是
createView(String name, String prefix, AttributeSet attrs);
)。 -
View createViewFromTag(View parent, String name,Context context, AttributeSet attrs);
通过View的父View,View的名称、attrs属性实例化View(内部调用
onCreateView()
和createView()
)。 -
void rInflate(XmlPullParser parser, View parent, Context context, final AttributeSet attrs, boolean finishInflate);
解析Parent的子View并添加到Parent上(递归调用)。
4 流程预览
// 把xml布局资源或者通过资源解析器实例化View
inflate{
if(merge标签){
// 递归实例化根节点的子View
rInflate();
// 返回是父View(因为根节点是merge标签)
}else{
// 实例化根节点的View
createViewFromTag();
// 递归实例化跟节点的子View
rInflateChildren()
// 这个需要注意
if(父View是空或者不把填充的View添加到父View){
返回根节点View
}else{
返回父View
}
}
}
// 通过View的名称实例化View
createViewFromTag{
//各个工厂先onCreateView()
onCreateView()或者createView()
}
// 递归实例化Parent的子View
rInflate{
// 解析请求焦点
parseRequestFocus()
// 解析include标签
parseInclude()
// 通过View的名称实例化View
createViewFromTag()
rInflate
}
注意 ,在调用inflate
方法的时候,传入的参数不一样,返回的View可是有区别的,总结起来就是:
- 传入的父View为空或者不添加到父View上,则返回根节点的View。
- 其他(也就是根节点是merge、父View不为空且添加到父View上,返回的是父View)。
5 流程详情
关键字段:
/************************字段定义区**********************/
// 反射调用构造方法的两个参数
final Object[] mConstructorArgs = new Object[2];
// 反射构造方法的参数类型
static final Class<?>[] mConstructorSignature = new Class[] {
Context.class, AttributeSet.class};
protected final Context mContext;
注:以下我们用 “当前View”来指代该操作的View,用“父View”指代“当前View”的父View。
5.1 inflate方法解析
inflate 方法主要是把布局资源实例化成View并返回。
通过 获取LayoutInflater的三种方式 我们知道,通过布局文件填充成View对象最终调用的是下面两个方法:
public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
if (DEBUG) System.out.println("INFLATING from resource: " + resource);
//获取布局资源的xml解析器,注意:在开头的时候我们强调过,普通的xml是不被支持的,必须是经过编译器处理过的。
XmlResourceParser parser = getContext().getResources().getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
最终调用的是此方法:
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
// from传入的Context
final Context inflaterContext = mContext;
// 判断parser是否是AttributeSet,如果不是则用XmlPullAttributes去包装一下。
final AttributeSet attrs = Xml.asAttributeSet(parser);
// 保存之前的Context
Context lastContext = (Context) mConstructorArgs[0];
// 赋值为传入的Context
mConstructorArgs[0] = inflaterContext;
// 默认返回的是传入的Parent
View result = root;
try {
// 查找开始标签
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
//如果没找到有效的开始标签则抛出InflateException
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)) {
// 根节点为空或者不添加到根节点上,则抛出异常。
// 因为“merge”标签必须是要被添加到父节点上的,不能独立存在。
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
// 递归实例化root(也就是传入Parent)下所有的View
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// temp是当前xml的根节点的View。通过父View、View名、Context、属性,来实例化View。也即实例化根节点的View。
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
// 如果传入Parent不为空
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// 创建父View类型的LayoutParams参数
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 如果不把填充的View 关联在父View上,则把父View的LayoutParams参数设置给它
// 如果把填充的View关联在父View上,则会走下面addView的逻辑
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// 实例化根节点View下面的所有子View。
// TODO ..................
rInflate(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// Google建议关联所有找到的View
// 如果根节点不为null,并且需要把根节点View关联到Parent上,则使用addView方法把布局填充成的View树添加到Parent上。
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// 决定返回的RootView(也即传入的Parent)还是xml中的根节点的View。
// 如果传入的Parent为空 或 实例化的View不添加到Parent上,则返回布局文件的根节点的View
// 否则,返回Parent
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
InflateException ex = new InflateException(e.getMessage());
ex.initCause(e);
throw ex;
} catch (IOException e) {
InflateException ex = new InflateException(
parser.getPositionDescription()
+ ": " + e.getMessage());
ex.initCause(e);
throw ex;
} finally {
// Don‘t retain static reference on context.
// 把这之前保存的Context从新放回全局变量中。
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return result;
}
}
5.2 createViewFromTag方法解析
createViewFromTag 方法主要通过要实例化View的父View、要实例化View的名称、Context上下文、属性值来实例化View。该方法主要做的操作有:
- 先尝试用用户设置的Factory以及自己私有的Factory来实例化View。
- 如果这几个Factory都没有实例化View,则调用
onCreateView
或者createView
来实例化View。
代码分析:
/*
* 缺省方法可见性,好让BridgeInflater能重写它。
* 根据父View、View名称、Context、属性实例化View。
*/
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标签,则用class指向的类的完整名称来替换当前名称。(我们都知道,用Fragment的时候,可指定 class="Fragment完整路径名",其他widget控件也类似)
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// 应用主题包装,如果允许并且已经被指定
// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
// 获取Context中主题属性
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
// 如果包含了主题,则用ContextThemeWrapper包装一下
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
// 如果根标签是“1995”,则创建一个“BlinkLayout”(其实就是一个FrameLayout)
// ps:这个没见过在哪里用到过。
if (name.equals(TAG_1995)) {
// Let‘s party like it‘s 1995!
return new BlinkLayout(context, attrs);
}
try {
View view;
// 尝试通过 mFactory2 或者 mFactory来创建View,这两个是通过setFactory和setFactory2来设置的。
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
// 如果没有设置自定义工厂并且LayoutInflater本身私有的View工厂不为空,则用私有View工厂创建View。
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
// 如果View还为空,也即没有工厂,或者工厂未能正确创建View,则尝试通过自身的方法实例化View
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf(‘.‘)) {
// 如果View标签中没有".",则代表是系统的widget,则调用onCreateView,这个方法会通过"createView"方法创建View
// 不过前缀字段会自动补"android.view."。
view = onCreateView(parent, name, attrs);
} else {
// 非系统控件,则name本身就是控件的完整路径名。
//通过widget完整路径名以及属性创建View。
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);
ie.initCause(e);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name);
ie.initCause(e);
throw ie;
}
}
5.3 onCreateView和createView方法解析
onCreateView 有两个重载方法,最终调用的是createView(String name, String prefix, AttributeSet attrs)
。
createView 主要做的操作有:
- 先通过Filter,看是否过滤。
- 利用反射实例化View对象。
源码分析:
protected View onCreateView(View parent, String name, AttributeSet attrs)
throws ClassNotFoundException {
return onCreateView(name, attrs);
}
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
// 系统控件,前缀自动补上"android.view."
return createView(name, "android.view.", attrs);
}
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
// 通过以View的name为key,查询构造函数的缓存map中时候已经有该View的构造函数。
Constructor<? extends View> constructor = sConstructorMap.get(name);
Class<? extends View> clazz = null;
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
// 构造函数在缓存的map中没有,则尝试去创建并添加。
if (constructor == null) {
// 通过 类名去加载控件的字节码
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
//如果有自定义的过滤器并且加载到字节码,则通过过滤器判断是否允许加载该View。
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
// 如果不允许则抛出异常。
failNotAllowed(name, prefix, attrs);
}
}
// 得到构造函数
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
// 缓存构造函数
sConstructorMap.put(name, constructor);
} else {
// setFilter()可能会在类的构造函数被添加到map之后,所以获取到map中的构造函数后还需要判断是否过滤。
if (mFilter != null) {
// 过滤的map中是否已经包含了此类名。
Boolean allowedState = mFilterMap.get(name);
// 当前类名没有被放到过滤的缓存map中
if (allowedState == null) {
// 重新加载类的字节码
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
// 重新通过过滤器判断是否过滤。
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
// 把过滤结果放到过滤的缓存map中。
mFilterMap.put(name, allowed);
if (!allowed) {
// 如果要过滤,则抛出异常。
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
// 缓存构造函数的map中已经保存了当前要实例化的View的构造函数并且是要过滤的,抛出异常。
failNotAllowed(name, prefix, attrs);
}
}
}
// 实例化类的参数数组,0 是获取LayoutInflater传入的Context,1 是View的属性
Object[] args = mConstructorArgs;
args[1] = attrs;
// 通过构造函数实例化View(看到此行就知道,为什么自定义View或者ViewGroup的时候,如果在布局中使用的话,必须重写两个参数的构造函数了。)
final View view = constructor.newInstance(args);
// 如果当前View是ViewStub,则把布局填充器设置给它。(因为ViewStub在此刻并不会填充期子View,而是等需要的时候由用户手动触发。)
if (view instanceof ViewStub) {
// 把当前LayoutInflater的克隆传递给ViewStub,让ViewStub实例化的时候用,因为ViewStub只是在需要的时候才会实例化View。
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;
} catch (NoSuchMethodException e) {
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class "
+ (prefix != null ? (prefix + name) : name));
ie.initCause(e);
throw ie;
} catch (ClassCastException e) {
// If loaded class is not a View subclass
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Class is not a View "
+ (prefix != null ? (prefix + name) : name));
ie.initCause(e);
throw ie;
} catch (ClassNotFoundException e) {
// If loadClass fails, we should propagate the exception.
throw e;
} catch (Exception e) {
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class "
+ (clazz == null ? "<unknown>" : clazz.getName()));
ie.initCause(e);
throw ie;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
5.4 rInflateChildren和rInflate方法解析
rInflate方法主要是遍历传入的Parent的子节点,实例化Parent的所有子View。
当遍历的时候发现当前View还有子View则递归调用此方法继续实例化当前View的子View。
这里面主要的操作有:
- parseRequestFocus(),处理请求焦点。
- parseInclude(),处理include标签。
- 实例化1995或一般View并添加到当前View的父View上。
源码分析:
/**
* 循环方法用来深入xml的层级并且实例化内部的View(非根节点View),这个方法通过调用rInflate,并使用Parent的Context作为
* 实例化View的Context。
*/
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
/**
* 循环方法用来深入xml的层级并且实例化view以及View的子View,
* 最后调用onFinishInflate()方法
*/
void rInflate(XmlPullParser parser, View parent,Context context, final AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
// 获取当前xml解析的深度
final int depth = parser.getDepth();
int type;
// 循环遍历xml节点
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)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
// 解析<tag>元素,并且设置键控标签在它包含的View上。最终调用的是View的view.setTag(key, value);方法
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
//处理include标签
if (parser.getDepth() == 0) {
// 最外层使用include标签抛出异常。
throw new InflateException("<include /> cannot be the root element");
}
// 解析include标签引入的布局
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
// 如果是merge标签则抛出异常(因为此方法实例化的是xml根节点的子View,所以非根节点不能使用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;
// 获取父View的LayoutParams,并在把View添加到父View的时候带过去
//(这里解释了,为什么自己手动new 一个View,添加到父View上的时候需要new父View的LayoutParams参数而不是自己的LayoutParams参数。)
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// 递归遍历实例化
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
// 如果父View下的所有View都完成填充,则调用父View的onFinishInflate()方法。
if (finishInflate) parent.onFinishInflate();
}
5.5 parseInclude方法解析
/**
* 解析include标签
*/
private void parseInclude(XmlPullParser parser, Context context, View parent,
AttributeSet attrs) throws XmlPullParserException, IOException {
int type;
// 如果include标签的父View是ViewGroup,则继续,否则抛出异常。
if (parent instanceof ViewGroup) {
// 使用主题包装。如果include中的View有自己的attr属性,则忽略。
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();
// 获取Layout标签指向布局资源id
int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
if (layout == 0) {
// 获取Layout的value值
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\" />");
}
// 尝试解析"?attr/name"成id资源
layout = context.getResources().getIdentifier(value.substring(1), null, null);
}
// include的布局可能引用了主题属性
if (mTempValue == null) {
mTempValue = new TypedValue();
}
// 尝试从主题中获取布局资源id,如果有的话。
if (layout != 0 && context.getTheme().resolveAttribute(layout, mTempValue, true)) {
layout = mTempValue.resourceId;
}
// 如果还是无法找到布局id,抛出异常。
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.");
} else {
// 获取include标签中布局的解析器
final XmlResourceParser childParser = context.getResources().getLayout(layout);
try {
final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
// 查找根节点
while ((type = childParser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
以上是关于Android LayoutInflater&LayoutInflaterCompat源码解析的主要内容,如果未能解决你的问题,请参考以下文章