当前位置: 代码迷 >> 综合 >> LayoutInflater是如何“移花接木”-下篇
  详细解决方案

LayoutInflater是如何“移花接木”-下篇

热度:86   发布时间:2023-12-16 04:01:31.0

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 xmlfinal View temp = createViewFromTag(root, name, inflaterContext, attrs);ViewGroup.LayoutParams params = null;//...              if (root != null) {// Create layout params that match root, if suppliedparams = 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的构造中初始化的,如下:

    protected LayoutInflater(LayoutInflater original, Context newContext) {mContext = newContext;mFactory = original.mFactory;mFactory2 = original.mFactory2;mPrivateFactory = original.mPrivateFactory;setFilter(original.mFilter);}
关于上面LayoutInfalter的构造方法,没有找到具体在哪被实例化,不过,这并不影响我们分析,这里认定mFactory2,mFactory,mPrivateFactory全部为null,那么,按照上面

代码逻辑,就会调用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 itclazz = 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 constructorif (mFilter != null) {// Have we seen this name before?Boolean allowedState = mFilterMap.get(name);if (allowedState == null) {// New class -- remember whether it is allowedclazz = 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最终是采用反射的方式,获取实例对象的