本文分析了从LayoutInflater类的inflate函数开始,到涉及jni的AssetManager类结束,中间到底做了什么。
本文的源码均是基于Android6.0(API=23)
首先,为了搞清楚这一切,我们从最深处往外探寻。
首先来看位于Resources类的getvalue
方法,逻辑也不复杂,方法具有三个参数。
public void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs)
throws NotFoundException {
boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
if (found) {
return;
}
throw new NotFoundException("Resource ID #0x"
+ Integer.toHexString(id));
}
先来看一下函数签名,第一个是资源id,也就是写的R.layout.XXX,第二个是一个TypedValue类的实例,API对于这个类的描述为:一个包含动态类型数据值的容器,主要用来持有Resources类的资源值。
这个类最熟悉的地方就是需要动态设置TextView的字体大小的时候,TextView提供了一个这样的方法public void setTextSize(int unit, float size)
这个方法的第一个参数unit(int类型),实际使用的时候就需要传入TypedValue的一些静态常量来指定,例如px、dp、sp、pt等。
/** {@link #TYPE_DIMENSION} complex unit: Value is raw pixels. */
public static final int COMPLEX_UNIT_PX = 0;
/** {@link #TYPE_DIMENSION} complex unit: Value is Device Independent
* Pixels. */
public static final int COMPLEX_UNIT_DIP = 1;
/** {@link #TYPE_DIMENSION} complex unit: Value is a scaled pixel. */
public static final int COMPLEX_UNIT_SP = 2;
/** {@link #TYPE_DIMENSION} complex unit: Value is in points. */
public static final int COMPLEX_UNIT_PT = 3;
/** {@link #TYPE_DIMENSION} complex unit: Value is in inches. */
public static final int COMPLEX_UNIT_IN = 4;
/** {@link #TYPE_DIMENSION} complex unit: Value is in millimeters. */
public static final int COMPLEX_UNIT_MM = 5;
也就是说,从资源文件里面取出来的值会放到这个TypedValue
类里面。
第三个参数,布尔值的resolveRefs,API是这么解释的,如果为true的话,当一个引用其他资源的资源值将会返回最终实际上真实的值。为false的话,则填充为自身的的引用值。getValue
方法的第三行调用了AssetManager
类的getResourceValue
方法,这个超出本文的范围,内部有jni的逻辑,我们只需要知道的是,如果找到了资源值,outvalue
将会带有资源值,同时返回为true。如果为false,则抛出异常。
为何要说到这个getValue
方法呢,因为在Resources类的另外一个方法loadXmlResourceParser(int id, String type)
方法中,调用了这个getValue
方法,下面我们看一下这个方法。
/*package*/ XmlResourceParser loadXmlResourceParser(int id, String type)
throws NotFoundException {
synchronized (mAccessLock) {
TypedValue value = mTmpValue;
if (value == null) {
mTmpValue = value = new TypedValue();
}
getValue(id, value, true);
if (value.type == TypedValue.TYPE_STRING) {
return loadXmlResourceParser(value.string.toString(), id,
value.assetCookie, type);
}
throw new NotFoundException(
"Resource ID #0x" + Integer.toHexString(id) + " type #0x"
+ Integer.toHexString(value.type) + " is not valid");
}
}
该方法首先取到Resources
类的成员变量mTmpValue
,赋给函数内的局部变量,并在为null的时候做了初始化处理,然后第8行执行了getValue
方法,这个方法我们刚刚已经介绍过了,它将根据id来寻找资源值赋给TypedValue
对象,然后,如果找到值的类型不为string类型的话,抛出异常。否则的话,则是执行了loadXmlResourceParser
方法的重载版本,该重载版本具有四个参数,分别对应的是xml的文件名称,xml资源的id,附加信息,和值类型。
private final int[] mCachedXmlBlockIds = { 0, 0, 0, 0 };
private final XmlBlock[] mCachedXmlBlocks = new XmlBlock[4];
/*package*/ XmlResourceParser loadXmlResourceParser(String file, int id,
int assetCookie, String type) throws NotFoundException {
if (id != 0) {
try {
// These may be compiled...
synchronized (mCachedXmlBlockIds) {
// First see if this block is in our cache.
final int num = mCachedXmlBlockIds.length;
for (int i=0; iif (mCachedXmlBlockIds[i] == id) {
//System.out.println("**** REUSING XML BLOCK! id="
// + id + ", index=" + i);
return mCachedXmlBlocks[i].newParser();
}
}
// Not in the cache, create a new block and put it at
// the next slot in the cache.
XmlBlock block = mAssets.openXmlBlockAsset(
assetCookie, file);
if (block != null) {
int pos = mLastCachedXmlBlockIndex+1;
if (pos >= num) pos = 0;
mLastCachedXmlBlockIndex = pos;
XmlBlock oldBlock = mCachedXmlBlocks[pos];
if (oldBlock != null) {
oldBlock.close();
}
mCachedXmlBlockIds[pos] = id;
mCachedXmlBlocks[pos] = block;
//System.out.println("**** CACHING NEW XML BLOCK! id="
// + id + ", index=" + pos);
return block.newParser();
}
}
} catch (Exception e) {
NotFoundException rnf = new NotFoundException(
"File " + file + " from xml type " + type + " resource ID #0x"
+ Integer.toHexString(id));
rnf.initCause(e);
throw rnf;
}
}
我去,这个方法有点长啊,但是内部逻辑并不复杂。
首先,11-18行判断是否能在缓存区根据找到相应的xml块,然后如果找到的话,直接返回相应XmlBlock
的解析器,否则的话,在22行,通过AssetManger
的openXmlBlockAsset
方法去获得XmlBlock
实例,内部原理我们也不去分析,因为有jni……从24行到36行开始的if往后都是把新获得的XmlBlock
块放到缓存里面去,只是在最后一行,将该块的解析器作为返回值返回。当然,在其中任何一步出了问题,都会抛出异常。
这样,我们就稀里糊涂的获得了一个我们需要的XmlResourceParser
,因为这个loadXmlResourceParser
四个参数的方法的返回值,同时也作为两个参数方法的返回值,被接下来这个方法给调用了……
public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
return loadXmlResourceParser(id, "layout");
}
嗯,这个方法只有一行代码……结果也就是我们刚才那个结果,那么这个getLayout
方法有什么用呢?原来这个方法,真的就是被一个LayoutInflater
类的inflate
函数调用了。
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();
}
}
至此,我们总结一下目前的发现。
inflate
函数都需要一个XmlPullParser
实例来解析xml文件,首先将layout的id传入,调用了Resources
类的public XmlResourceParser getLayout(int resource)
方法,其内部调用了/*package*/ XmlResourceParser loadXmlResourceParser(int id, String type)
方法
,在其方法中通过public void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs)
方法根据资源的id获得了一个携带xml文件信息的TypedValue
对象,再根据该TypedValue
对象的string字段,也就是xml文件的文件名,来调用/*package*/ XmlResourceParser loadXmlResourceParser(String file, int id,
方法来获得一个Xml解析器。
int assetCookie, String type) throws NotFoundException
LayoutInflater
类的inflate
函数有数个重载版本,包括
1. public View inflate(@LayoutRes int resource, @Nullable ViewGroup root)
2. public View inflate(XmlPullParser parser, @Nullable ViewGroup root)
3. public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
4. public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
所有方法最后都调用了上面的第四个方法。
其中第一个参数XmlPullParser
区别与我们平时指定的layout的id有所区别,所以在分析这个方法之前,我们要知道这个XmlPullParser
是如何来的。
接下来,有了XmlPullParser
对象,我们就可以分析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;
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("**************************");
}
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException(" 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);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
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) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
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;
}
}
这个方法也是一如既往的长,不过这次我们不能在深入的往里面跳了……似乎无穷尽的样子。我们平时都是要么就是写inflater(R.layout.xxx,null);
或者是inflater(R.layout.xxx,parent,false);
但是似乎没人关系到底为何这么写,接下来我们就看一发。
为了不造成混淆,我们把第一个参数统一称为parser,为int类型的时候成为id,第二个参数称为root,第三个参数我们姑且称为attachToRoot。
我们已经做好了准备工作,也将R.layout.xxx换成了一个XmlPullParaser
,我们先来看第9行,将你传进来的root赋给了一个变量result,顺便说一句,当你调用两个参数的inflater函数时,实际上你是调用了inflate(parser, root, root != null)
。接下来第14行到第22行,是对xml结构的一次检查,这里我们暂且不讨论,因为要涉及到XmlBlock#Parser
的实现。接下来24行到40行,也是一个特殊情况,涉及到
标签,我们也先跳过,再往后,就是真正的重点了。
// Temp is the root view that was found in the xml
/*此处开始为42行*/ 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);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
42行,调用了createViewFromTag
方法来获取一个View,来自41行的注释表示,此时返回的View实际上就是你layout的根节点View。接下来44行到58行,表示如果你第二个参数parent不为空,而且第三个参数为false的话,我们就给你的创建View的根布局装上一个LayoutParams
,而且这个params是根据Xml解析器parser解析出来的属性来创建的,参见第6行。
/*此处为第6行*/ final AttributeSet attrs = Xml.asAttributeSet(parser);
通常我们当你使用Java代码来添加一个View至一个ViewGroup中时,View的LayoutParams类型一定和该ViewGroup
的内部类ViewGroup#LayoutParams
同属一个类型。52行的引用的attrs就是xml文件中的属性,至此,创建的View已经有了自己的LayoutParams
。如果你第二个参数root传递的为null,inflater方法会认为你创建的View暂时不需要父布局,这样的话你View的根布局自然也没有约束,后期再使用这个View的话,就需要重新去指定一次LayoutParams
。
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
//此处为65行 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.
//此处为73行 if (root != null && attachToRoot) {
root.addView(temp, params);
}
第65行,来自64行的注释表示这一行是实例化temp(也就是创建出来的View)的所有子View。第73行到75行,如果你的父布局parent为空且第三个参数为true,则将创建的View加入到父布局中,同时也指定了LayoutParams
。这段代码实际上就是因为下面这种情况而执行的。
inflater(R.layout.xxx,parent,true);
此时,如果你inflater函数是这样调用的话,那么整个过程就已经结束了,根据102行的返回,你将直接获得的是你传入的第二个参数,也就是你期望将创建出来的View加入的那个父布局。如果这个时候你再次尝试将你创建出来的View来addView
方法来加入到其他的ViewGroup
中,则会抛出异常,因为这个View已经有父布局了。
如果你是这么调用的呢?
inflater(R.layout.xxx,null,false);
inflater(R.layout.xxx,null,true);
inflater(R.layout.xxx,null);
inflater(R.layout.xxx,parent,false);
//此处为79行 if (root == null || !attachToRoot) {
result = temp;
}
则都是执行了第79行到81行的逻辑。就是将你最后创建的View返回。但是上面这四种情况,获得的结果也不一样。前三种效果是一样的,因为第三种的实现实际上就等于第一种,然后第二种因为所有关于attachtoRoot的逻辑全部建立在root不为空上面,所以也是一样,前面三种实际上都只是执行了第42行和第65行,46行至58行的逻辑被跳过,然后返回一个没有LayoutParams的View,这样的View因为丢失了根节点的属性,所以在xml根节点设置的属性都会失效。
上面第四种实现方式,完整的执行了46行到58行的逻辑,同时因为attachtoRoot为false的原因,没有执行73行到75行,所以返回了一个具有完整属性的View,所以就可以将这个View作为一个ViewGroup的addView
方法的参数来传入,甚至这个ViewGroup
不为先前提到的root。
所以还是要根据自己的实际所需,选择对inflater函数做不同的实现方式。