前言
setContentView
应该是我们刚开始使用Android 就使用的Api了 来看一下setContentView
具体实现
先看一下setContentView时序图
解释一下几个类的作用
-
AppCompatDelegateImpl
AppCompatActivity
的代理实现类,AppCompatActivity
的具体实现会交由它实现 -
LayoutInflater
我用google翻译了一下
布局充气机
👏 感觉有点gaygay的 这个类的作用就是解析xml 遍历创建view -
Factory2
这个接口只有一个方法
onCreateView
顾名思义 就是创建viewAppCompatDelegateImpl
就继承了这个接口 我们可以实现这个接口来创建我们需要的view 比如AppCompatDelegateImpl
就会将所有的TextView
转换为AppCompatTextView
一会可以看一下代码
接下来上一下源码🤠 全都以AppCompatActivity为例哦
onCreate
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
我们看到 AppCompatActivity
的操作都是交由代理类来实现
重点看一下installViewFactory()
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);//1
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");//2
}
}
}
我们看注释1的地方 发现在OnCreate
方法中 会默认设置一个Factory2
对象 所以我们需要在Activity.OnCreate
之前设置Factory2
对象 否则就会出现注释2的报错
setContentView
今天的重头戏 我们看一下上面的时序图 大致的流程其实就是解析xml 然后反射生成view 具体根据时序图 我们来看一下源码分析
我们看到 setContentView
完全都是交由delegate实现
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
//delegate
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
//通过LayoutInflater和resId 创建View
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
之前的时序图有说明 delegate会通过LayoutInflater
创建View
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;
}
//生成xml解析器
XmlResourceParser parser = res.getLayout(resource);
try {
//1. 通过反射生成view
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
上面这段代码会将xml文件进行解析 然后通过inflate方法创建view 并返回 我们看一下下面的部分精简代码
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
.......
try {
//将parser前进到第一个START_TAG
advanceToRootNode(parser);
final String name = parser.getName();
//如果是merger标签
if (TAG_MERGE.equals(name)) {
......
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
//1.根据tag生成view Tag就是我们写在xml的带包名的标签 比如TextView
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
//设置LayoutParams
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.
// 递归实例化子View 这里也会根据include等标签 调用不同方法 大家可以自己看一下
rInflateChildren(parser, temp, attrs, true);
//setContentView的话 会将View 添加到android.R.id.Content中
if (root != null && attachToRoot) {
root.addView(temp, params);
}
if (root == null || !attachToRoot) {
result = temp;
}
}
}
......
return result;
}
}
上面的代码我稍微精简了一下 流程主要分为3步
- 前进到第一个
START_TAG
解析xml 生成View,但是ViewGroup都有子View - 递归生成所有子View
- 因为是
setContentView
所以attachToRoot
时钟为tree 将View 添加到android.R.id.content中
我们关注的重点主要还是createViewFromTag
看下面的代码 发现createViewFromTag
是交由Factory2
实现
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
......
try {
//这里会交由Factory2实现 如果Factory没有处理这个Tag 那么会交由系统实现 就是下面的onCreateView和createView
View view = tryCreateView(parent, name, context, attrs);
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
//比如TextView等不需要包名
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
view = createView(context, name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
}
.......
}
我们重点还是关注tryCreateView
,onCreateView
等方法大家可以自己看一下 就是反射生成view
tryCreateView
会通过Factory2接口实现 还记得我们之前说 AppDelegateImpl
继承了Factory2
这就是AppCompatActivity
对一些Tag进行了拦截创建 我们也可以自己实现Factory2
来进行拦截 实现一些像换肤的功能 大家可以看一下我之前写的文章手撸动态换肤框架(一)
感觉有收获的同学点点赞呐😘
扯远了 我们看一下tryCreateView
方法
public final View tryCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context,
@NonNull AttributeSet attrs) {
if (name.equals(TAG_1995)) {
// 这里好像致敬了JAVA诞生
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
View view;
//这里就是我们可以做的Hook点 我们以AppCompatActivity为例 看一下AppCompatActivity的实现
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;
}
上面方法我们发现 我们如果想Hook系统的setContentView
方法的话 可以通过Factory2
来实现 我们以AppCompatActivity
为例 看一下AppCompatActivity Factory
的实现
我们上面说过 AppCompatActivity
的实现都交由AppCompatDelegate
实现 具体实现类为AppCompatDelegateImpl
而AppCompatDelegateImpl
继承了Factory2
接口 所以我们看一下AppCompatDelegateImpl
的onCreateView
伪代码
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
......
mAppCompatViewInflater = new AppCompatViewInflater();
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
IS_PRE_LOLLIPOP,true, VectorEnabledTintResources.shouldBeUsed());
}
感觉有点绕 但其实逻辑又非常清楚👍 符合单一职责 创建View都是通过LayoutInflate来实现
final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
......
switch (name) {
case "TextView":
view = createTextView(context, attrs);
verifyNotNull(view, name);
break;
case "ImageView":
view = createImageView(context, attrs);
verifyNotNull(view, name);
break;
case "Button":
view = createButton(context, attrs);
verifyNotNull(view, name);
break;
case "EditText":
view = createEditText(context, attrs);
verifyNotNull(view, name);
break;
......
}
if (view == null && originalContext != context) {
// If the original context does not equal our themed context, then we need to manually
// inflate it using the name so that android:theme takes effect.
//注释1
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check its android:onClick
//检查onClick 如果存在 就调用view.setonClickListener
checkOnClickListener(view, attrs);
}
return view;
}
看到AppCompatViewInflater
对TextView等做了兼容处理 重点看一下注释1的地方 里面通过反射获取View 但是众所周知 反射是一个比较耗时的操作 所以我在布局优化的文章中写过 可以通过一些X2C等框架 来解决反射问题 但是可能会有一些兼容问题 需要处理一下
private View createViewFromTag(Context context, String name, AttributeSet attrs) {
......
if (-1 == name.indexOf('.')) {
for (int i = 0; i < sClassPrefixList.length; i++) {
final View view = createViewByPrefix(context, name, sClassPrefixList[i]);
if (view != null) {
return view;
}
}
return null;
} else {
return createViewByPrefix(context, name, null);
}
}
......
}
private View createViewByPrefix(Context context, String name, String prefix)
throws ClassNotFoundException, InflateException {
//先从缓存中取 避免每次都反射获取
Constructor<? extends View> constructor = sConstructorMap.get(name);
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
//反射生成
Class<? extends View> clazz = Class.forName(
prefix != null ? (prefix + name) : name,
false,
context.getClassLoader()).asSubclass(View.class);
constructor = clazz.getConstructor(sConstructorSignature);
sConstructorMap.put(name, constructor);
}
constructor.setAccessible(true);
return constructor.newInstance(mConstructorArgs);
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
}
}
至此View已经通过反射生成了 再看一次时序图 来回顾一下整体的流程
总结
在学习setContentView的过程中 可以参考上面的那个时序图来分析 我们需要了解其中的几个类的职责是什么 分析清楚之后其实逻辑也就相当清楚了