0
点赞
收藏
分享

微信扫一扫

聊聊setContentView

花明 2021-09-29 阅读 171
Android

前言

setContentView应该是我们刚开始使用Android 就使用的Api了 来看一下setContentView具体实现

先看一下setContentView时序图

解释一下几个类的作用

  • AppCompatDelegateImpl

    AppCompatActivity的代理实现类,AppCompatActivity的具体实现会交由它实现

  • LayoutInflater

    我用google翻译了一下 布局充气机? 感觉有点gaygay的 这个类的作用就是解析xml 遍历创建view

  • Factory2

    这个接口只有一个方法onCreateView 顾名思义 就是创建view AppCompatDelegateImpl就继承了这个接口 我们可以实现这个接口来创建我们需要的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步

  1. 前进到第一个START_TAG 解析xml 生成View,但是ViewGroup都有子View
  2. 递归生成所有子View
  3. 因为是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接口 所以我们看一下AppCompatDelegateImplonCreateView伪代码

    @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的过程中 可以参考上面的那个时序图来分析 我们需要了解其中的几个类的职责是什么 分析清楚之后其实逻辑也就相当清楚了

举报

相关推荐

0 条评论