这是Android 界面的绘制与渲染系列的第二篇。
这篇我们来从setContentView() 方法入手来看看View 间是如何布局排列的。 需要注意的是,如果我们的自定义Activity 继承自Activity 或FragmentActivity, setContentView() 方法是调用的Window 的setContentView() 方法:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
如果我们的Activity 是继承自AppCompatActivity, 那么setContentView() 方法调用的是AppCompatDelegate 类的setContentView() 方法:
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
而继承父类不同的差别可以理解为Activity 和FragmentActivity 是基于运行平台的,仅仅能使用运行平台的特性代码,适配不同Android 版本的代码需要我们自己处理,而继承AppCompatActivity ,其内部包装了AppCompatDelegate及其实现类,帮我们做了运行平台代码的适配,简化我们的代码,让我们更加专注于业务实现。其实这两者的代码在这里都是殊途同归的,最后都会调用:
LayoutInflater.from(mContext).inflate(resId, contentParent);
这篇文章所讲述的内容都在这行代码中,当然调用这句代码之前还会生成DecorView,这点就留给读者自行查看了。
继续跟进这行代码,发现它会调用下面的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) + ")");
}
//1.进行预编译,属于优化
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
//2.生成xml的解析器
XmlResourceParser parser = res.getLayout(resource);
try {
//3.返回解析好的View
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
我们来看看2处是如何生成解析器的
@NonNull
XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,
@NonNull String type)
throws NotFoundException {
if (id != 0) {
try {
synchronized (mCachedXmlBlocks) {
final int[] cachedXmlBlockCookies = mCachedXmlBlockCookies;
final String[] cachedXmlBlockFiles = mCachedXmlBlockFiles;
final XmlBlock[] cachedXmlBlocks = mCachedXmlBlocks;
// First see if this block is in our cache.
final int num = cachedXmlBlockFiles.length;
for (int i = 0; i < num; i++) {
if (cachedXmlBlockCookies[i] == assetCookie && cachedXmlBlockFiles[i] != null
&& cachedXmlBlockFiles[i].equals(file)) {
return cachedXmlBlocks[i].newParser(id);
}
}
// Not in the cache, create a new block and put it at
// the next slot in the cache.
final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
if (block != null) {
final int pos = (mLastCachedXmlBlockIndex + 1) % num;
mLastCachedXmlBlockIndex = pos;
final XmlBlock oldBlock = cachedXmlBlocks[pos];
if (oldBlock != null) {
oldBlock.close();
}
cachedXmlBlockCookies[pos] = assetCookie;
cachedXmlBlockFiles[pos] = file;
cachedXmlBlocks[pos] = block;
return block.newParser(id);
}
}
} catch (Exception e) {
final NotFoundException rnf = new NotFoundException("File " + file
+ " from xml type " + type + " resource ID #0x" + Integer.toHexString(id));
rnf.initCause(e);
throw rnf;
}
}
throw new NotFoundException("File " + file + " from xml type " + type + " resource ID #0x"
+ Integer.toHexString(id));
}
其实通过上述代码我们可以看出一些逻辑:Google内部对XML文件解析器相关资源也做了缓存优化,先在缓存中获取,如果没有生成并放在缓存中方便下次读取。那么block.newParser(id); 返回的又是什么呢?
public XmlResourceParser newParser(@AnyRes int resId) {
synchronized (this) {
if (mNative != 0) {
return new Parser(nativeCreateParseState(mNative, resId), this);
}
return null;
}
}
返回的是一个Parser类,而它所实现的接口是XmlPullParser,也就是说,Android 中的XML解析是使用Pull 方式解析的。这里就不对Pull解析进行会展开了,读者可在文后查看附带的扩展链接进行了解。
下面我们继续解析inflate() 方法:
//因为原始代码过多和有些调试代码,所以做了一些删除处理,我们只分析关键代码
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
View result = root;
try {
final String name = parser.getName();
//如果是merge标签,直接初始化XML文件内部的View并添加到root
if (TAG_MERGE.equals(name)) {
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 {
//如果是正常ViewGroup节点,先初始化该节点,再初始化内部View,最后添加到root
// 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);
}
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
// 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;
}
}
在后面的代码中我们会了解到上面的rInflateChildren() 方法会调用上面的createViewFromTag() 方法,所以,我们先来看看rInflateChildren() 方法:
/**
* 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;
boolean pendingRequestFocus = false;
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)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} 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 (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
parent.onFinishInflate();
}
}
虽然这个方法是有些长,但由于我认为它是整个setContentView() 方法调用过程中最精彩的地方,所以就全放出来。需要注意的是该方法在rInflateChildren() 方法中调用了自身,所以该方法使用了递归处理。现在让我们分三部分进行分析,第一部分:
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;
}
}
第一行代码我们可以是获取当前的深度,这个没什么,如果我们点进去发现就是直接获取深度。第二句type = parser.next(), 是通过解析器获取下一个标签的名称,next() 方法是个抽象方法,我们来看一下Parser 类内部的实现:
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;
}
可以看到next() 方法里对XML树深度有所控制,就是解析到开始标签深度就会加一,解析到结束标签深度就会减一
要知道就XML解析而言,<a></a>与<a/>是完全一样的,所以:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"/>
和
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</TextView>
解析是一样的
所以再回到第一部分的while 循环处,每个层级的View 因为都有开始标签,所以一定都会进入循环中,而在第二遍都会因为结束标签而终止,ViewGroup会因为还没遇到自身的结束标签就一直遍历其容器内的子View, View 会因为下一个就是自身的结束标签而中止再次进入循环体内。 第二部分,则是下面的代码:
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} 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 {}
这一部分其实就是获取当前解析的标签名称,并对一些特殊的标签进行处理,如果有读者感兴趣,可以自行解读。
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);
}
第三部分依然还在循环体内,可以看到在上面看到调用了createViewFromTag() 方法进行View的初始化,并添加到ViewGroup中,因为第一部分我们分析了,普通的View会因为紧随而来的结束标签而停止进入循环体内,进入循环体内的parent就一定是ViewGroup 级别的。
那么我们现在来看看createViewFromTag() 方法
try {
View view = tryCreateView(parent, name, context, attrs);
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
view = createView(context, name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
throw e;
}
在这个方法中大部分都是不需要我们关注的代码,所以,我抽取了一些主要的代码进行分析,我们可以看到Google首先调用了tryCreateView() 方法进行View的创建,读者之后可以详细查看此方法的调用,根据Google的描述这个方法适用于特定的场景,当然不在这次的解析中。我们可以看到在trycatch代码中首先对name进行了判断,根据包名分隔符的.来判断是系统内置的View还是应用包内自带的View,如果是系统内置的View,它还是要依然全路径的:
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
所以最后都调用的是createView() 方法,那么我们继续跟进:
@Nullable
public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable 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 {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
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 = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, viewContext, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, viewContext, attrs);
}
}
}
Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = viewContext;
Object[] args = mConstructorArgs;
args[1] = attrs;
try {
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;
} finally {
mConstructorArgs[0] = lastContext;
}
}
}
其实这个方法里的代码也有很多,大部分是trycatch代码,所以也是摘取了一部分,而从这部分代码中,我们也是可以看到Google为了优化而做的缓存处理,并使用了反射获取到View的构造方法,最终对View调用两个参数的构造方法初始化View。
那么到这里,setContentView()解析就算是结束了,这仅仅是将XML布局文件进行解析生成View,挂载到View树上,那么它们是什么时候绘制的呢?我们后面的文章会讲到。个人觉得整个解析过程在对递归部分代码的理解还是比较耗时的,而其他部分的代码如果已经有了一定的开发经验也会很好理解。下一篇我们将会对Activity的onCreate(), onResume() 生命周期的调用进行解析,为View的绘制打好基础。
参考: