1. 概述
内涵段子架構第一階段已經更新完了,後面我們主要是以google源碼為主,今天我帶大家來看一下setContentView的源碼,請先看一下如果繼承自Activity去列印一個TextView與繼承自AppCompatActivity去列印一個TextView分别是這樣的:
繼承自Activity:
android.widget.TextView{ac5cd17 V.ED..... ......ID ,-, #7f0b002c app:id/text_view}
繼承自AppCompatActivity:
android.support.v7.widget.AppCompatTextView{b V.ED..... ......ID ,-, #7f0b0055 app:id/text_view}
誰能告訴我這到底是怎麼啦?我布局裡面明明是TextView為什麼繼承自AppCompatActivity就變成了AppCompatTextView,那麼接下來我們就來看一下源碼到底是怎麼把我的TextView給拐走的。
所有分享大綱:2017Android進階之路與你同行
視訊講解位址:周六晚上八點
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(-);
}
// ......
// 省略調一些代碼,看着暈,不過這也太省了。
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 >= ) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (sdk >= ) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (sdk >= ) {
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 < ;
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)是一個靜态的單例類無論在哪裡擷取都是同一個對象。接下來我們來看一下加載布局的三種方式:
View.inflate(context,layoutId,parent);
LayoutInflater.from(context).inflate(layoutId,parent);
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[];
mConstructorArgs[] = context;
try {
// 判斷是不是自定義View,自定義View在布局檔案中com.hc.BannerView是個全類名,
// 而系統的View在布局檔案中不是全類名 TextView
if (- == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[] = 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進階之路與你同行
視訊講解位址:周六晚上八點