1. 概述
内涵段子架构第一阶段已经更新完了,后面我们主要是以google源码为主,今天我带大家来看一下setContentView的源码,请先看一下如果继承自Activity去打印一个TextView与继承自AppCompatActivity去打印一个TextView分别是这样的:
继承自Activity:
android.widget.TextView{ac5cd17 V.ED..... ......ID 0,0-0,0 #7f0b002c app:id/text_view}
继承自AppCompatActivity:
android.support.v7.widget.AppCompatTextView{392562b V.ED..... ......ID 0,0-0,0 #7f0b0055 app:id/text_view}
谁能告诉我这到底是怎么啦?我布局里面明明是TextView为什么继承自AppCompatActivity就变成了AppCompatTextView,那么接下来我们就来看一下源码到底是怎么把我的TextView给拐走的。
所有分享大纲:2017Android进阶之路与你同行
视频讲解地址:https://pan.baidu.com/s/1qYl2AOO
2. Activity的setContentView源码阅读
2.1 很多人都问过我怎么看源码,我只想说怎么看?当然是坐着点进去看啊!
public void setContentView(@LayoutRes int layoutResID) {
// 获取Window 调用window的setContentView方法,发现是抽象类,所以需要找具体的实现类PhoneWindow
getWindow().setContentView(layoutResID);
}
// PhoneWindow 中的 setContentView方法
@Override
public void setContentView(int layoutResID) {
// 如果mContentParent 等于空,调用installDecor();
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
// 把我们自己的布局layoutId加入到mContentParent,我们set进来的布局原来是放在这里面的Soga
mLayoutInflater.inflate(layoutResID, mContentParent);
}
2.2 installDecor(),这个之前已经带大家看过一遍了,不过没办法再进来看看吧:
// This is the top-level view of the window, containing the window decor.
// 看到这解释木有?
private DecorView mDecor;
private void installDecor() {
if (mDecor == null) {
// 先去创建一个 DecorView
mDecor = generateDecor(-1);
}
// ......
// 省略调一些代码,看着晕,不过这也太省了。
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
}
}
// generateDecor 方法
protected DecorView generateDecor(int featureId) {
// 就是new一个DecorView ,DecorView extends FrameLayout 不同版本的源码有稍微的区别,
// 低版本DecorView 是PhoneWindow的内部类,高版本是一个单独的类,不过这不影响。
return new DecorView(context, featureId, this, getAttributes());
}
protected ViewGroup generateLayout(DecorView decor) {
// Inflate the window decor.
// 我看你到底怎么啦
int layoutResource;
// 都是一些判断,发现 layoutResource = 系统的一个资源文件,
if(){}else if(){}else if(){
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}
mDecor.startChanging();
// 把布局解析加载到 DecorView 而加载的布局是一个系统提供的布局,不同版本不一样
// 某些源码是 addView() 其实是一样的
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
// ID_ANDROID_CONTENT 是 android.R.id.content,这个View是从DecorView里面去找的,
// 也就是 从系统的layoutResource里面找一个id是android.R.id.content的一个FrameLayout
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
// 返回
return contentParent;
}
其实看源码一定要带着最初的出发点来看,要不然里面太多了根本找不到方向,如果带着思想来看那么就算跑偏了也可以从新再回来,我目前就是想弄清楚我们的 setContentView() 系统到底把我们的布局加到哪里去了。我先用文字总结一下,然后去画一张图:
- Activity里面设置setContentView(),我们的布局显示主要是通过PhoneWindow,PhoneWindow获取实例化一个DecorView。
- 实例化DecorView,然后做一系列的判断然后去解析系统的资源layoutId文件,至于解析哪一个资源文件会做判断比如有没有头部等等,把它解析加载到DecorView,资源layout里面有一个View的id是android.R.id.content。
- 我们自己通过setContentView设置的布局id其实是解析到mParentContent里面的,也就是那个id叫做android.R.id.content的FarmeLayout,好了就这么多了。
3. AppCompatActivity的setContentView
@Override
public void setContentView(@LayoutRes int layoutResID) {
// 跟我在网上看的完全不一样
getDelegate().setContentView(layoutResID);
}
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
// window 还是那个window ,留意一下就行 , 不同的版本返回 AppCompatDelegateImpl,但是都是相互继承
// 最终继承都是继承 AppCompatDelegateImplV9 有的版本V7有的V9 好麻烦 嗨!
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
if (BuildCompat.isAtLeastN()) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (sdk >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (sdk >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (sdk >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV9(context, window, callback);
}
}
// 下面其实就没啥好看的了,一个一个点进去,仔细看看就好了。与Activity没啥区别了
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
private void ensureSubDecor() {
mSubDecor = createSubDecor();
}
4. AppCompatViewInflater源码分析
看到这里还是不知道为什么我的TextView变成了AppCompatTextView,找啊找啊就找了这么个方法:
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
// 把LayoutInflater 的 Factory设置为了this,也就说待会创建View就会走自己的onCreateView方法
// 如果看不懂还需要看一下 LayoutInflater 的源码,我们的LayoutInflater.from(mContext)其实是一个单例
// 如果设置了Factory那么每次创建View都会先执行Factory的onCreateView方法
LayoutInflaterCompat.setFactory(layoutInflater, this);
} else {
if (!(LayoutInflaterCompat.getFactory(layoutInflater)
instanceof AppCompatDelegateImplV7)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
// 看一下是不是 5.0 ,5.0 都自带什么效果我就不说了
final boolean isPre21 = Build.VERSION.SDK_INT < 21;
if (mAppCompatViewInflater == null) {
mAppCompatViewInflater = new AppCompatViewInflater();
}
// We only want the View to inherit its context if we're running pre-v21
final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);
// 通过 AppCompatViewInflater 去创建View
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
isPre21, /* Only read android:theme pre-L (L+ handles this anyway) */
true, /* Read read app:theme as a fallback at all times for legacy reasons */
VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
);
}
其实在讲数据库优化我们已经看过一次AppCompatViewInflater的源码了,创建View都是用的反射,只不过做了缓存和优化而已,我们写代码其实可以仿照源码来,给我们很好的思路。
public final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
View view = null;
// 果真找到你了,哈哈 ,做了替换
// We need to 'inject' our tint aware Views in place of the standard framework versions
switch (name) {
case "TextView":
view = new AppCompatTextView(context, attrs);
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
case "Button":
view = new AppCompatButton(context, attrs);
break;
case "EditText":
view = new AppCompatEditText(context, attrs);
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.
view = createViewFromTag(context, name, attrs);
}
return view;
}
private View createView(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 = context.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
// 利用反射创建一个构造函数
constructor = clazz.getConstructor(sConstructorSignature);
sConstructorMap.put(name, constructor);
}
constructor.setAccessible(true);
// 利用反射创建View的实例
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;
}
}
5. LayoutInflater源码分析
LayoutInflater的源码我们分三个步骤去看相对来说会更加的系统:
4. 1 如何获取LayoutInflater?
4. 2 如何使用LayoutInflater?
4. 3 布局的View是如何被实例化的?
先来看看我们平时都是怎么去获取LayoutInflater的,这个我们其实并不陌生LayoutInflater.from(context):
/**
* Obtains the LayoutInflater from the given context.
*/
// 是一个静态的方法
public static LayoutInflater from(Context context) {
// 通过context获取系统的服务
LayoutInflater LayoutInflater =
// context.getSystemService()是一个抽象类,所以我们必须找到实现类ContextImpl
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
// ContextImpl 里面的实现方法
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
/**
* Gets a system service from a given context.
*/
// SystemServiceRegistry 里面的getSystemService方法
public static Object getSystemService(ContextImpl ctx, String name) {
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}
// 这是一个静态的HashMap集合
private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
new HashMap<String, ServiceFetcher<?>>();
// 静态的代码块中
static{
// 注册LayoutInflater服务
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
}
// 注册很多的其他服务......
}
接下来大致的整理一下获取LayoutInflater的思路,通过Context的实现类ContextImpl获取的,最终是通过SystemServiceRegistry.getSystemService()方法,而SYSTEM_SERVICE_FETCHERS是一个静态的HashMap,初始化是在静态代码块中通过registerService注册了很多服务。所以到目前为止我们有两个思想对于我们后面插件化的皮肤框架有很大的关系,第一LayoutInflater其实是一个系统的服务,第二每次通过LayoutInflater.form(context)是一个静态的单例类无论在哪里获取都是同一个对象。接下来我们来看一下加载布局的三种方式:
1.View.inflate(context,layoutId,parent);
2.LayoutInflater.from(context).inflate(layoutId,parent);
3.LayoutInflater.from(context).inflate(layoutId,parent,attachToRoot);
1.View.inflate(context,layoutId,parent);
// 其实就是调用的 LayoutInflater.from(context).inflate(layoutId,parent);
public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
LayoutInflater factory = LayoutInflater.from(context);
return factory.inflate(resource, root);
}
2.LayoutInflater.from(context).inflate(layoutId,parent);
// 其实就是调用的 LayoutInflater.from(context).inflate(layoutId,parent,attachToRoot);
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
3.LayoutInflater.from(context).inflate(layoutId,parent,attachToRoot); 其实最终都是调用的该方法,我们关键是要弄清楚这个参数的概念,尤其是attachToRoot:
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) + ")");
}
// 获取一个 XmlResourceParser 解析器,这个应该并不陌生,就是待会需要去解析我们的layoutId.xml文件
// 这个到后面的插件化架构再去详细讲解
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
......
//保存传进来的这个view
View result = root;
try {
// Look for the root node.
int type;
//在这里找到root标签
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!");
}
//获取这个root标签的名字
final String name = parser.getName();
......
//判断是否merge标签
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");
}
//这里直接加载页面,忽略merge标签,直接传root进rInflate进行加载子view
rInflate(parser, root, inflaterContext, attrs, false);
} else {
//通过标签来获取view
//先获取加载资源文件中的根view
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
//布局参数
ViewGroup.LayoutParams params = null;
//关键代码A
if (root != null) {
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
//temp设置布局参数
temp.setLayoutParams(params);
}
}
......
//关键代码B
//在这里,先获取到了temp,再把temp当做root传进去rInflateChildren
//进行加载temp后面的子view
rInflateChildren(parser, temp, attrs, true);
......
//关键代码C
if (root != null && attachToRoot) {
//把view添加到root中并设置布局参数
root.addView(temp, params);
}
//关键代码D
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
......
} catch (Exception e) {
......
} finally {
......
}
return result;
}
}
// 创建View
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
// ......
try {
// 创建我们的View
View view;
if (mFactory2 != null) {
// 先通过mFactory2 创建,其实在 AppCompatActivity里面会走这个方法,也就会去替换某些控件
// 所以我们就 看到了上面的内容
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
// 走mFactory
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
// ......省略
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
// 判断是不是自定义View,自定义View在布局文件中com.hc.BannerView是个全类名,
// 而系统的View在布局文件中不是全类名 TextView
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
// ........
}
}
// 创建View
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
// 做一些反射的性能优化
try {
// 先从缓存中拿,这是没拿到的情况
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
// 加载 clazz
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
// 创建View的构造函数
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
// 加入缓存集合集合
sConstructorMap.put(name, constructor);
} else {
}
// 通过反射创建View
final View view = constructor.newInstance(args);
return view;
} catch (NoSuchMethodException e) {
// ......省略部分代码
}
}
这里有两个思想比较重要第一个View的创建是通过当前View的全类名反射实例化的View,第二个View的创建首先会走mFactory2,然后会走mFactory,只要不为空先会去执行Factory的onCreateView方法,最后才会走系统的LayoutInflater里面的createView()方法,所以我们完全可以自己去实例化View,这对于我们的插件化换肤很有帮助。
基于插件式换肤框架搭建 - 资源加载源码分析和插件式换肤框架搭建 - setContentView源码阅读这两篇文章我们完全可以自己动手搭建一套换肤框架了,我们下期再见。
所有分享大纲:2017Android进阶之路与你同行