您的位置:首页 > 移动开发 > Android开发

Android自定义View-xml解析角度理解

2017-12-25 11:29 459 查看

Android自定义View-xml解析角度理解

起因

在浏览工程的时候,看到如下的自定义View

class CustomView extends ViewGroup {
...
CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LayoutInflater.inflate(R.layout.layout_custom_view,this,true);
...
}
}


遂进行改写

// layout_custom_view.xml 布局文件

<xx.xx.xx.CustomView
...
>
...
</xx.xx.xx.CustomView>

class CustomView extends ViewGroup {
CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LayoutInflater.inflate(R.layout.layout_custom_view,this,true);
}

@Override
protected void onFinishInflate() {
super.onFinishInflate();
...
}
}


在运行以后,爆出NullPointException以致应用crash



查看出错代码,发现布局文件为

<...>
...
<xx.xx.xx.CustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
...
/>
...
</...>


在一下午的时间里百思不得其解,在下班回家的路上突然想到是在报错文件的布局中,CustomView作为一个父容器没有包含任何的子view,导致在onFinishInflate()方法中获取的子元素都为空,正确使用的布局文件应该为:

...
<include
android:layout_width="wrap_content"
android:layout_height="wrap_content"
layout="@layout/layout_custom_view"
/>
...


趁这个时机,了解下Android从xml文件中生成视图的过程。

XmlPullParser

next():获取下一个parsing Event

START_DOCUMENT:文档开始,没有内容被读。只能在next(),nextToken(),nextTag()之前获取

START_TAG:TAG开始被读取,tag的名字可以使用getName()获取,namespace和prefix可以使用gatNameSpace()和getPrefix()获取

TEXT:字符出具被读取,可以使用getText()获取内容

END_TAG:TAG结尾被读取,tag的名字可以使用getName()获取,namespace和prefix可以使用gatNameSpace()和getPrefix()获取

END_DOCUMENT:逻辑上的文档结尾。在next(),nextToken()调用后,到达文档的结尾

getEventType():获取parser当前事件状态

流程

最终会调用到LayoutInflater.inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

// LayoutInflater

/**
* Inflate a new view hierarchy from the specified xml resource. Throws
* {@link InflateException} if there is an error.
*
* @param resource ID for an XML layout resource to load (e.g.,
*        <code>R.layout.main_page</code>)
* @param root Optional view to be the parent of the generated hierarchy (if
*        <em>attachToRoot</em> is true), or else simply an object that
*        provides a set of LayoutParams values for root of the returned
*        hierarchy (if <em>attachToRoot</em> is false.)
* @param attachToRoot Whether the inflated hierarchy should be attached to
*        the root parameter? If false, root is only used to create the
*        correct subclass of LayoutParams for the root view in the XML.
* @return The root View of the inflated hierarchy. If root was supplied and
*         attachToRoot is true, this is root; otherwise it is the root of
*         the inflated XML file.
*/
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();
}
}


在这里将layout中的布局xml文件解析成XmlResourceParser,并调用LayoutInflater.inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)

// LayoutInflater

/**
* Inflate a new view hierarchy from the specified XML node. Throws
* {@link InflateException} if there is an error.
* <p>
* <em><strong>Important</strong></em>   For performance
* reasons, view inflation relies heavily on pre-processing of XML files
* that is done at build time. Therefore, it is not currently possible to
* use LayoutInflater with an XmlPullParser over a plain XML file at runtime.
*
* @param parser XML dom node containing the description of the view
*        hierarchy.
* @param root Optional view to be the parent of the generated hierarchy (if
*        <em>attachToRoot</em> is true), or else simply an object that
*        provides a set of LayoutParams values for root of the returned
*        hierarchy (if <em>attachToRoot</em> is false.)
* @param attachToRoot Whether the inflated hierarchy should be attached to
*        the root parameter? If false, root is only used to create the
*        correct subclass of LayoutParams for the root view in the XML.
* @return The root View of the inflated hierarchy. If root was supplied and
*         attachToRoot is true, this is root; otherwise it is the root of
*         the inflated XML file.
*/
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)) {
// 对<merge />标签进行特殊判断,不能作为根布局
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;
// XXX 在这里进行root和attachToRoot参数的处理
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) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;

Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}

return result;
}
}


开始解析XML文件,读取TAG,并进行处理,并对进行特殊判断,不能作为根布局。调用createViewFromTag创建在布局里的根View

/**
* Creates a view from a tag name using the supplied attribute set.
* <p>
* <strong>Note:</strong> Default visibility so the BridgeInflater can
* override it.
*
* @param parent the parent view, used to inflate layout params
* @param name the name of the XML tag used to define the view
* @param context the inflation context for the view, typically the
*                {@code parent} or base layout inflater context
* @param attrs the attribute set for the XML tag used to define the view
* @param ignoreThemeAttr {@code true} to ignore the {@code android:theme}
*                        attribute (if set) for the view being inflated,
*                        {@code false} otherwise
*/
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();
}

if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}

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;
} catch (InflateException e) {
throw e;

} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;

} catch (Exception e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}


对View的属性进行处理,并调用createView(String name, String prefix, AttributeSet attrs)生成真实的View

/**
* Low-level function for instantiating a view by name. This attempts to
* instantiate a view class of the given <var>name</var> found in this
* LayoutInflater's ClassLoader.
*
* <p>
* There are two things that can happen in an error case: either the
* exception describing the error will be thrown, or a null will be
* returned. You must deal with both possibilities -- the former will happen
* the first time createView() is called for a class of a particular name,
* the latter every time there-after for that class name.
*
* @param name The full name of the class to be instantiated.
* @param attrs The XML attributes supplied for this instance.
*
* @return View The newly instantiated view, or null.
*/
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;

try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);

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 lastContext = mConstructorArgs[0];
if (mConstructorArgs[0] == null) {
// Fill in the context if not already within inflation.
mConstructorArgs[0] = mContext;
}
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]));
}
mConstructorArgs[0] = lastContext;
return view;

} catch (NoSuchMethodException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;

} catch (ClassCastException e) {
// If loaded class is not a View subclass
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassNotFoundException e) {
// If loadClass fails, we should propagate the exception.
throw e;
} catch (Exception e) {
final InflateException ie = new InflateException(
attrs.getPositionDescription() + ": Error inflating class "
+ (clazz == null ? "<unknown>" : clazz.getName()), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}


通过反射获取对应View的构造函数,并进行初始化操作,放回rootView后,回到public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot),调用final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,

boolean finishInflate),对包含的子View进行处理

/**
* Recursive method used to descend down the xml hierarchy and instantiate
* views, instantiate their children, and then call onFinishInflate().
* <p>
* <strong>Note:</strong> Default visibility so the BridgeInflater can
* override it.
*/
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

final int depth = parser.getDepth();
int type;

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)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
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 (finishInflate) {
parent.onFinishInflate();
}
}


对子View进行递归createViewFromTag生成子View,并加入子View的容器,最后调用xml布局根View的onFinishInflate()

总结

出现空指针的原因就在于在布局中使用了自定义View,但是没有子View导致获取的参数都为null

在onFinishInflate()调用时,还没有加入到显示的视图中

github地址
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android 自定义view
相关文章推荐