布局解析-LayoutInflater分析
一般添加布局或控件有两种方式,一种是直接new对应的View,然后通过addView方法添加到父控件中,一种是将布局写在layout的xml文件中,然后调用调用接口添加到父控件中,而这里就涉及到将xml布局转为View控件,一般都是使用LayoutInflater的inflate方法来讲布局xml转化为View对象的,可能有些地方如Activity设置布局调用setContentView来直接添加布局xml对应的资源id,但其内部实现中仍需要将资源转化为View对象,其也是使用LayoutInflater的inflate方法来进行转化的
1.LayoutInflater对象获取
代码中获取LayoutInflater对象的方式有多种,如在Activity中调用getLayoutInflater方法、或者调用LayoutInflater.from(context)方法等都可以获取到LayoutInflater对象,虽然方法多种,但其实殊途同归,这里以Activity的getLayoutInflater方法为例走读下代码(android10),Activity的getLayoutInflater方法代码如下:
public LayoutInflater getLayoutInflater() {
return getWindow().getLayoutInflater();
}
显然,我们在代码中也可以这么调用来获取LayoutInflater对象,言归正传,继续先看下getWindow的逻辑
public Window getWindow() {
return mWindow;
}
getWindow方法只是返回Activity的私有变量mWindow,在Activity的attach方法中找到了mWindow赋值的地方
mWindow = new PhoneWindow(this, window, activityConfigCallback);
显然,mWindow是一个PhoneWindow的对象,查看PhoneWindow中的getLayoutInflater方法代码:
public LayoutInflater getLayoutInflater() {
return mLayoutInflater;
}
同样,查看mLayoutInflater赋值的地方
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}
可以看到,在PhoneWIndow的构造方法中,调用LayoutInflater.from(context)方法获取LayoutInfater对象,查看其代码
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
显然,这里是获取Context.LAYOUT_INFLATER_SERVICE=layout_inflater系统服务,查看下getSystemService源码,注意这里context实际是Activity的对象,而Activity的getSystemService代码如下
public Object getSystemService(@ServiceName @NonNull String name) {
if (getBaseContext() == null) {
throw new IllegalStateException(
"System services not available to Activities before onCreate()");
}
if (WINDOW_SERVICE.equals(name)) {
return mWindowManager;
} else if (SEARCH_SERVICE.equals(name)) {
ensureSearchManager();
return mSearchManager;
}
return super.getSystemService(name);
}
显然,对于LAYOUT_INFLATER_SERVICE服务的获取还要查看其父类,Activity是继承ContextThemeWrapper的,查看下ContextThemeWrapper中getSystemService方法
public Object getSystemService(String name) {
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
if (mInflater == null) {
mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
}
return mInflater;
}
return getBaseContext().getSystemService(name);
}
这里代码逻辑很简单,如果是LAYOUT_INFLATER_SERVICE服务,则直接调用LayoutInflater.from方法来赋值返回,否则调用getBaseContext获取的context对象的getSystemService方法,这里关键在于getBaseContext方法
getBaseContext实现在ContextThemeWrapper的父类ContextWrapper中
public ContextWrapper(Context base) {
mBase = base;
}
protected void attachBaseContext(Context base) {
if (mBase != null) {
throw new IllegalStateException("Base context already set");
}
mBase = base;
}
/**
* @return the base context as set by the constructor or setBaseContext
*/
public Context getBaseContext() {
return mBase;
}
而Activity并没有含Context参数的构造方法,那看下Activity中关于attachBaseContext方法调用
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
attachBaseContext(context);
...
}
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
if (newBase != null) {
newBase.setAutofillClient(this);
newBase.setContentCaptureOptions(getContentCaptureOptions());
}
}
在Activity中虽有重写attachBaseContext方法,但其仍调用父类的attachBaseContext,查看代码其最后会调到ContextWrapper的attachBaseContext方法,也即会设置mBase,而在Activity的attach方法中有调用attachBaseContext方法,其调用主要在ActivityThread的performLaunchActivity方法中,即在启动Activity时会通过反射创建Activity对象然后调用其attach方法等,这里就不深究该逻辑,贴上部分关键attach调用关键代码
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
ActivityInfo aInfo = r.activityInfo;
if (r.packageInfo == null) {
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
Context.CONTEXT_INCLUDE_CODE);
}
ComponentName component = r.intent.getComponent();
if (component == null) {
component = r.intent.resolveActivity(
mInitialApplication.getPackageManager());
r.intent.setComponent(component);
}
if (r.activityInfo.targetActivity != null) {
component = new ComponentName(r.activityInfo.packageName,
r.activityInfo.targetActivity);
}
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
try {
...
if (activity != null) {
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
...
appContext.setOuterContext(activity);
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback,
r.assistToken);
...
}
...
...
return activity;
}
这里可以看到,其调用Activity的attach方法传入的context对象为通过createBaseContextForActivity方法获取的
private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
final int displayId;
try {
displayId = ActivityTaskManager.getService().getActivityDisplayId(r.token);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
ContextImpl appContext = ContextImpl.createActivityContext(
this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);
final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
// For debugging purposes, if the activity's package name contains the value of
// the "debug.use-second-display" system property as a substring, then show
// its content on a secondary display if there is one.
String pkgName = SystemProperties.get("debug.second-display.pkg");
if (pkgName != null && !pkgName.isEmpty()
&& r.packageInfo.mPackageName.contains(pkgName)) {
for (int id : dm.getDisplayIds()) {
if (id != Display.DEFAULT_DISPLAY) {
Display display =
dm.getCompatibleDisplay(id, appContext.getResources());
appContext = (ContextImpl) appContext.createDisplayContext(display);
break;
}
}
}
return appContext;
}
这里可以看到createBaseContextForActivity方法返回的是一个ContextImpl的对象
回到前面ContextThemeWrapper中getSystemService方法的地方
public Object getSystemService(String name) {
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
if (mInflater == null) {
mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
}
return mInflater;
}
return getBaseContext().getSystemService(name);
}
查看LayoutInflater的from方法
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
显然,这里会调用到ContextImpl的getSystemService方法获取LayoutInflater对象
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
ContextImpl的getSystemService方法其实是获取在SystemServiceRegistry中注册的服务
static {
...
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
...
}
public static Object getSystemService(ContextImpl ctx, String name) {
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}
private static <T> void registerService(String serviceName, Class<T> serviceClass,
ServiceFetcher<T> serviceFetcher) {
SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
}
显然,通过getSystemService方法获取的LAYOUT_INFLATER_SERVICE的服务对象其实是PhoneLayoutInflater对象,而前面ContextThemeWrapper中getSystemService方法中的cloneInContext方法,查看PhoneLayoutInflater中代码如下,可知其也只是重新创建了一个PhoneLayoutInflater对象
public LayoutInflater cloneInContext(Context newContext) {
return new PhoneLayoutInflater(this, newContext);
}
当然,查看PhoneLayoutInflater的代码可以看到,其代码并不多,大部分逻辑还是在LayoutInflater中
2.inflate方法加载布局返回View对象
从应用常用的infalte方法开始看,查看infalte方法是定义在LayoutInflater中的
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
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) + ")");
}
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
这里有两个地方有return view的地方,一个是tryInflatePrecompiled返回view,一个是inflate方法返回view
其中tryInflatePrecompiled方法相关部分如下,目前包括查看android10和android-12.0.0_r3的代码都是mUseCompiledView为false,即未使用相关逻辑,该处启动的话应该是对部分应用有个预编译的文件compiled_view.dex,然后在tryInflatePrecompiled方法中会通过该处获取的加载器获取对应的view。
View tryInflatePrecompiled(@LayoutRes int resource, Resources res, @Nullable ViewGroup root,
boolean attachToRoot) {
if (!mUseCompiledView) {
return null;
}
...
}
private void initPrecompiledViews() {
// Precompiled layouts are not supported in this release.
boolean enabled = false;
initPrecompiledViews(enabled);
}
private void initPrecompiledViews(boolean enablePrecompiledViews) {
mUseCompiledView = enablePrecompiledViews;
if (!mUseCompiledView) {
mPrecompiledClassLoader = null;
return;
}
...
}
/**
* @hide for use by CTS tests
*/
@TestApi
public void setPrecompiledLayoutsEnabledForTesting(boolean enablePrecompiledLayouts) {
initPrecompiledViews(enablePrecompiledLayouts);
}
目前代码看mUseCompiledView为false,即未使用,所以获取view是通过下面代码获取
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
这里关键查看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 {
advanceToRootNode(parser);
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("<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;
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;
}
}
}...
return result;
}
}
上面逻辑中主要区分了merge标签和其他标签,对于merge标签,其调用了rInflate方法,循环遍历标签然后解析控件添加到root即父控件中,其对标签处理差不多,这里主要看下非merge标签的,在非merge标签的代码中,关于生成控件的地方主要有如下两个代码调用
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
rInflateChildren(parser, temp, attrs, true);
其中createViewFromTag方法是根据tag标签属性来创建对应的view,在这里即是解析布局的根节点生成对应控件
rInflateChildren方法会调用到rInflate方法,其会遍历标签解析控件然后添加到父控件中,在这里即解析布局的根节点下的子节点添加到根节点生成的控件下
查看createViewFromTag方法的逻辑,其会调用到如下方法
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 = 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;
}...
}
这里有三个关键方法
View view = tryCreateView(parent, name, context, attrs);//优先调用,返回空时再根据条件调用另外两个方法
view = onCreateView(context, parent, name, attrs);//标签名中不包含'.'时调用
view = createView(context, name, null, attrs);//标签名中包含'.'时调用
2.1、tryCreateView
public final View tryCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context,
@NonNull AttributeSet attrs) {
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
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);
}
return view;
}
上述代码中mFactory和mFactory2可通过setFactory和setFactory2方法进行设置,但一般情况下这两个变量为空,暂不细看,mPrivateFactory可通过setPrivateFactory方法设置,但该方法有hide标签,主要是用于框架层调用,目前查看代码主要有两个地方调用
一个是在Activity的attach方法中有调用如下代码
mWindow.getLayoutInflater().setPrivateFactory(this);
查看Activity实现的onCreateView方法:
public View onCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context, @NonNull AttributeSet attrs) {
if (!"fragment".equals(name)) {
return onCreateView(name, context, attrs);
}
return mFragments.onCreateView(parent, name, context, attrs);
}
public View onCreateView(@NonNull String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
return null;
}
显然,根据上面代码,在标签不为fragment时,应用又没有重写该方法时,这里会返回空,如果标签为fragment,则会调用FragmentController的onCreateView方法
在Fragment中也有地方调用setPrivateFactory方法,这些应该是为了能够处理fragment相关标签,这里不细讲
2.2、createView
在标签中包含'.'时会调用createView方法来返回view(因为onCreateView会调用到createView,所以先分析createView方法)
view = createView(context, name, null, attrs);
public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Objects.requireNonNull(viewContext);
Objects.requireNonNull(name);
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 = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, viewContext, 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 = 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;
}
}...
}
代码看似有点多但逻辑其实并不复杂,这里主要是根据传入的name和prefix通过反射创建对应的对象
2.3、onCreateView
在标签名中不含'.'时会调用onCreateView方法来返回view
view = onCreateView(context, parent, name, attrs);
public View onCreateView(@NonNull Context viewContext, @Nullable View parent,
@NonNull String name, @Nullable AttributeSet attrs)
throws ClassNotFoundException {
return onCreateView(parent, name, attrs);
}
protected View onCreateView(View parent, String name, AttributeSet attrs)
throws ClassNotFoundException {
return onCreateView(name, attrs);
}
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
在上面LayoutInflater代码中可知其会调用到onCreateView的两个参数的方法,而在PhoneLayoutInflater中有重写该方法
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
@Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {
// In this case we want to let the base class take a crack
// at it.
}
}
return super.onCreateView(name, attrs);
}
结合前面createView方法逻辑可知,onCreateView逻辑主要是尝试通过预置的几个prefix结合name来通过createView方法反射获取对应view,这里的几个prefix分别是
"android.widget."
"android.webkit."
"android.app."
"android.view."
根据上面逻辑是否说明在布局中标签写完整控件类名(包名+类名)会比简写加载效率要高那么一点?
2.4、rInflate
前面也提到过rInflate方法,根据前面的分析,createViewFromTag主要是用于单个控件类型的标签的加载,rInflateChildren用于加载标签下的子标签,而rInflate可以说是用于加载同一父标签下的同深度的平行标签,这里会遍历同一父标签下的标签,根据标签不同进行不同的处理
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();
}
}
3.布局中一般节点标签
在上述代码中有一些标签的解析和处理,这里简单列举下
1、控件各种view:对应具体的View控件
2、ViewStub:ViewStub也是继承View的,不过相较于一般的view比较特殊,ViewStub和include标签一样可以引入一个布局,但其默认不会显示也不占用位置,在调用对应的接口后才会显示
3、merge:只能用于根标签,作用相当于一个虚拟控件,merge下的子标签对应的控件在加载时直接添加到父控件中
4、fragment:主要用于fragment的加载
5、requestFocus:可用于设置默认焦点,如一个布局中有多个EditText,在其中一个EditText中添加requestFocus标签后,进入该界面后默认含requestFocus标签的EditText拥有焦点
6、tag:tag标签只支持id和value值,添加后其会对标签对应父控件调用view.setTag(key, value)方法,其中key和value分别对应标签中的id和value值
7、include:引入布局
8、blink:其对应LayoutInflater中的BlinkLayout,不过目前应该是已经不使用了
标签:解析,return,LayoutInflater,布局,name,attrs,context,null,view From: https://www.cnblogs.com/luoliang13/p/18227115