LayoutInflater“移花接木”的上篇,介绍了LayoutInflater对象的获取方式,更主要的是分析几种方式的原理,发现最终都是通过获取系统服务的方式。那么,本篇算是“移花接木”的重头,主要分析xml是如何转换为view的
获取LayoutInflater对象后,就是使用Inflate方法,从此开始,揭开“移花接木”之神秘面纱...
LayoutInflater.java
两个参数,一个xml对应的资源ID,一个是父级的view,也就是你的xml的view要绑定在谁身上
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
这里除上面的两个参数,多出一个,表示是否绑定在父view上,取决于父view是否为null
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
此处可以看出,涉及到Resources类、LayoutInflater类,执行以下两个方法,分析会围绕这两个方法展开
步骤1:public XmlResourceParser getLayout(int resourceId)
步骤2:public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)
步骤1 getLayout分析:
Resources.java
加载xml资源解析器,这里加载的是layout的,当然,还有anim等资源的解析器,参数type对应的就是“layout”,如果是动画,对应的是“anim”
public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
return loadXmlResourceParser(id, "layout");
}
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");
}
}
这里涉及getValue,loadXmlResource两个方法,
先看getValue方法内部,是在资源中查找是否存在xml的id,如果有程序继续执行,如果没有,则抛出异常,程序中断
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写错,会抛出资源找不到或者id无效的异常,就是在此校验
再看loadXmlResource方法,如下:
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; i<num; i++) {
if (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;
return block.newParser();
}
}
}
}
throw new NotFoundException(
"File " + file + " from xml type " + type + " resource ID #0x"
+ Integer.toHexString(id));
}
乍一看,很多代码,有点蒙圈,仔细一看,return位置,newParser()方法是重点需要关注的,mCachedXmlBlocks是一个XmlBlock类型数组,如下:
private final XmlBlock[] mCachedXmlBlocks = new XmlBlock[4];
这样,最终集中到XmlBlock类,如下:
XmlBlock.java
public XmlResourceParser newParser() {
synchronized (this) {
if (mNative != 0) {
return new Parser(nativeCreateParseState(mNative), this);
}
return null;
}
}
看到这,Parser是什么,Parser是一个类,实现了XmlResourceParser接口,继承了XmlPullParser接口,
XmlResourceParser虽然是接口,但基本没声明方法
也就是说,XmlBlock的内部类Parser,具体实现了XmlPullParser的方法,源码如下:
final class Parser implements XmlResourceParser{ //... public String getText() { int id = nativeGetText(mParseState); return id >= 0 ? mStrings.get(id).toString() : null; } public int getLineNumber() { return nativeGetLineNumber(mParseState); } //...
public int next() throws XmlPullParserException,IOException { if (!mStarted) { mStarted = true; return START_DOCUMENT; } if (mParseState == 0) { return END_DOCUMENT; } int ev = nativeNext(mParseState); if (mDecNextDepth) { mDepth--; mDecNextDepth = false; } switch (ev) { case START_TAG: mDepth++; break; case END_TAG: mDecNextDepth = true; break; } mEventType = ev; if (ev == END_DOCUMENT) { // Automatically close the parse when we reach the end of // a document, since the standard XmlPullParser interface // doesn't have such an API so most clients will leave us // dangling. close(); } return ev;
} Parser的代码比较繁杂,这里摘录了一部分,具体可参见XmlBlock.java源码在next()方法中,很清晰看到对于xml步步解析也是使用EventType的进行的,在XmlPullParser.java中TYPES数组定义了xml所有的EventType类型
可以看出,对xml的解析,用到很多native方法,也就是说,xml是靠c代码解析的,这点避免了java类的编译过程,尤其是xml解析这种对xml层层解析的过程,是比较耗时的操作,c代码的执行效率远高于java代码
native方法部分具体实现暂时没有研究,后续慢慢会补上
至此,getLayout分析完
步骤2 Inflate分析开始,
LayoutInflater.java,以下源码在该类中
第一个参数,步骤1 获取到的资源解析器
第二个参数,父级的view
第三个参数,是否绑定到父view上
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { //... final Context inflaterContext = mContext; final AttributeSet attrs = Xml.asAttributeSet(parser); Context lastContext = (Context) mConstructorArgs[0]; mConstructorArgs[0] = inflaterContext; View result = root; // Look for the root node. int type; //... final String name = parser.getName(); //... // 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) { // 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); } } //... // 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; } //... return result; } }
上面代码大致过程:
首先,执行通过解析器,获取到要inflate的view名称,
然后,调用createViewFromTag,参数依次,父view,view名称,上下文,view对应的AttributeSet,最终得到temp,也就是我们inflate xml布局后的view,
值得注意的,这个temp是xml布局中最顶的,比如,xml布局中最顶级是Framlayout,那么,temp对应的就是Framlayout,如果是LinearLayout,temp对应的就是LinearLayout,这也是为什么我们finViewById时,需要指定view.findViewById的原因,从顶级父view开始搜寻指定id的子view
接着,通过父view得到LayoutParams,设置view的LayoutParams,将view绑定到父view上
如果没有父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) { if (name.equals("view")) { name = attrs.getAttributeValue(null, "class"); } // Apply a theme wrapper, if allowed and one is specified. 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(); } //... 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); } if (view == null) { final Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; try { if (-1 == name.indexOf('.')) { view = onCreateView(parent, name, attrs); } else { view = createView(name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } } return view; } }
看完这段代码,是不是很爽?基本不大明白,别怕,这里我们只关注需要的,那就是最终返回的view,
上面的关键执行过程:mFactory2,mFactory,mPrivateFactory的判断逻辑,不为null,分别回调各自方法赋值给view,全部为null,调用类方法createView,onCreateView
涉及到对应接口mFactory2,mFactory,mPrivateFactory对应的方法,有类方法createView,onCreateView,也有回调方法onCreateView
先看接口mFactory2,mFactory,mPrivateFactory,是在LayoutInfalter的构造中初始化的,如下:
关于上面LayoutInfalter的构造方法,没有找到具体在哪被实例化,不过,这并不影响我们分析,这里认定mFactory2,mFactory,mPrivateFactory全部为null,那么,按照上面protected LayoutInflater(LayoutInflater original, Context newContext) { mContext = newContext; mFactory = original.mFactory; mFactory2 = original.mFactory2; mPrivateFactory = original.mPrivateFactory; setFilter(original.mFilter); }
代码逻辑,就会调用LayoutInflater的类方法createView,onCreateView,代码如下:
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { return createView(name, "android.view.", attrs); }
onCreateView内部执行的是createView方法,如下:
第一个参数,view的名称,第二个参数view对应的类名前缀,第三个参数view对应的AttributeSet
public final View createView(String name, String prefix, AttributeSet attrs){ Constructor<? extends View> constructor = sConstructorMap.get(name); Class<? extends View> clazz = null; //... if (constructor == null) { // Class not found in the cache, see if it's real, and try to add it clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); 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 { // If we have a filter, apply it to cached constructor if (mFilter != null) { // Have we seen this name before? Boolean allowedState = mFilterMap.get(name); if (allowedState == null) { // New class -- remember whether it is allowed clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class); boolean allowed = clazz != null && mFilter.onLoadClass(clazz); mFilterMap.put(name, allowed); if (!allowed) { failNotAllowed(name, prefix, attrs); } } else if (allowedState.equals(Boolean.FALSE)) { failNotAllowed(name, prefix, attrs); } } } Object[] args = mConstructorArgs; args[1] = attrs; 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; //... }
代码很长,一点点分析,首先是从sConstructorMap中取对应name的view构造,也就是从缓存中取构造,
如果存在构造,则执行else分支,如果没有构造,则执行if,获取字节码对象,获取构造,添加到sConstructorMap缓存中,方便再次使用
获取view类的构造方法后,创建view的实例对象,返回实例对象
这个过程是反射的运用
以上是假定,mFactory2,mFactory,mPrivateFactory全部为null情况
另外,关于mFactory2,mFactory,mPrivateFactory全部为null,在LayoutInflater"移花接木"-上篇中,提到,三种获取LayoutInflater方式,最终获取到的是SystemServiceRegistry类中注册的PhoneLayoutInflater的实例,构造函数以及父类构造函数,如下:
public PhoneLayoutInflater(Context context) { super(context); }
protected LayoutInflater(Context context) { mContext = context; }
这也说明,创建LayoutInfalter并未涉及到,protected LayoutInflater(LayoutInflater original, Context newContext),这种构造方法,那么,mFactory2,mFactory,mPrivateFactory自然也就全部为null
Inflate分析完成
步骤1 步骤2,整个Inflater.inflate方法执行过程分析到此结束
总结:xml布局,资源文件的解析,底层是用c代码编写的,执行效率高
xml布局转化为view最终是采用反射的方式,获取实例对象的