反思 系列博客是個人一種新學習方式的嘗試,該系列起源和目錄請參考 這裏 。php
Android
體系自己很是宏大,源碼中值得思考和借鑑之處衆多。以LayoutInflater
自己爲例,其整個流程中除了調用inflate()
函數 填充佈局 功能以外,還涉及到了 應用啓動、調用系統服務(進程間通訊)、對應組件做用域內單例管理、額外功能擴展 等等一系列複雜的邏輯。java
本文筆者將針對LayoutInlater
的整個設計思路進行描述,其總體結構以下圖:android
顧名思義,LayoutInflater
的做用就是 佈局填充器 ,其行爲本質是調用了Android
自己提供的 系統服務。而在Android
系統的設計中,獲取系統服務的實現方式就是經過ServiceManager
來取得和對應服務交互的IBinder
對象,而後建立對應系統服務的代理。git
Android
應用層將系統服務註冊相關的API
放在了SystemServiceRegistry
類中,而將註冊服務行爲的代碼放在了ContextImpl
類中,ContextImpl
類實現了Context
類下的全部抽象方法。github
Android
應用層還定義了一個Context
的另一個子類:ContextWrapper
,Activity
、Service
等組件繼承了ContextWrapper
, 每一個ContextWrapper
的實例有且僅對應一個ContextImpl
,造成一一對應的關係,該類是 裝飾器模式 的體現:保證了Context
類公共功能代碼和不一樣功能代碼的隔離。性能優化
此外,雖然ContextImpl
類做爲Context
類公共API
的實現者,LayoutInlater
的獲取則交給了ContextThemeWrapper
類,該類中將LayoutInlater
的獲取交給了一個成員變量,保證了單個組件 做用域內的單例。app
開發者但願直接調用LayoutInflater#inflate()
函數對佈局進行填充,該函數做用是對xml
文件中標籤的解析,並根據參數決定是否直接將新建立的View
配置在指定的ViewGroup
中。ide
通常來講,一個View
的實例化依賴Context
上下文對象和attr
的屬性集,而設計者正是經過將上下文對象和屬性集做爲參數,經過 反射 注入到View
的構造器中對View
進行建立。函數
除此以外,考慮到 性能優化 和 可擴展性,設計者爲LayoutInflater
設計了一個LayoutInflater.Factory2
接口,該接口設計得很是巧妙:在xml
解析過程當中,開發者能夠經過配置該接口對View
的建立過程進行攔截:經過new的方式建立控件以免大量地使用反射,亦或者 額外配置特殊標籤的解析邏輯以建立特殊組件(好比Fragment
)。源碼分析
LayoutInflater.Factory2
接口在Android SDK
中的應用很是廣泛,AppCompatActivity
和FragmentManager
就是最有力的體現,LayoutInflater.inflate()
方法的理解雖然重要,但筆者竊覺得LayoutInflater.Factory2
的重要性與其相比不逞多讓。
對於LayoutInflater
總體不甚熟悉的開發者而言,本小節文字描述彷佛晦澀難懂,且不免有是否過分設計的疑惑,但這些文字的本質倒是佈局填充流程總體的設計思想,讀者不該該將本文視爲源碼分析,而應該將本身代入到設計的過程當中 。
上文提到,LayoutInflater
做爲系統服務之一,獲取方式是經過ServiceManager
來取得和對應服務交互的IBinder
對象,而後建立對應系統服務的代理。
Binder
機制相關並不是本文的重點,讀者能夠注意到,Android
的設計者將獲取系統服務的接口交給了Context
類,意味着開發者能夠經過任意一個Context
的實現類獲取系統服務,包括不限於Activity
、Service
、Application
等等:
public abstract class Context {
// 獲取系統服務
public abstract Object getSystemService(String name);
// ......
}
複製代碼
讀者須要理解,Context
類地職責並不是只針對 系統服務 進行提供,還包括諸如 啓動其它組件、獲取SharedPerferences 等等,其中大部分功能對於Context
的子類而言都是公共的,所以沒有必要每一個子類都對其進行實現。
Android
設計者並無直接經過繼承的方式將公共業務邏輯放入Base
類供組件調用或者重寫,而是借鑑了 裝飾器模式 的思想:分別定義了ContextImpl
和ContextWrapper
兩個子類:
Context
的公共API
的實現都交給了ContextImpl
,以獲取系統服務爲例,Android
應用層將系統服務註冊相關的API
放在了SystemServiceRegistry
類中,而ContextImpl
則是SystemServiceRegistry#getSystemService
的惟一調用者:
class ContextImpl extends Context {
// 該成員即開發者使用的`Activity`等外部組件
private Context mOuterContext;
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
}
複製代碼
這種設計使得 系統服務的註冊(SystemServiceRegistry
類) 和 系統服務的獲取(ContextImpl
類) 在代碼中只有一處聲明和調用,大幅下降了模塊之間的耦合。
ContextWrapper
則是Context
的裝飾器,當組件須要獲取系統服務時交給ContextImpl
成員處理,僞代碼實現以下:
// class Activity extends ContextWrapper
class ContextWrapper extends Context {
// 1.將 ContextImpl 做爲成員進行存儲
public ContextWrapper(ContextImpl base) {
mBase = base;
}
ContextImpl mBase;
// 2.系統服務的獲取統一交給了ContextImpl
@Override
public Object getSystemService(String name) {
return mBase.getSystemService(name);
}
}
複製代碼
ContextWrapper
裝飾器的初始化如何實現呢?每當一個ContextWrapper
組件(如Activity
)被建立時,都爲其建立一個對應的ContextImpl
實例,僞代碼實現以下:
public final class ActivityThread {
// 每當`Activity`被建立
private Activity performLaunchActivity() {
// ....
// 1.實例化 ContextImpl
ContextImpl appContext = new ContextImpl();
// 2.將 activity 注入 ContextImpl
appContext.setOuterContext(activity);
// 3.將 ContextImpl 也注入到 activity中
activity.attach(appContext, ....);
// ....
}
}
複製代碼
讀者應該注意到了第3步的
activity.attach(appContext, ...)
函數,該函數很重要,在【佈局流程】一節中會繼續引伸。
讀者也許注意到,對於單個Activity
而言,屢次調用activity.getLayoutInflater()
或者LayoutInflater.from(activity)
,獲取到的LayoutInflater
對象都是單例的——對於涉及到了跨進程通訊的系統服務而言,經過做用域內的單例模式保證以節省性能是徹底能夠理解的。
設計者將對應的代碼放在了ContextWrapper
的子類ContextThemeWrapper
中,該類用於方便開發者爲Activity
配置自定義的主題,除此以外還經過一個成員持有了一個LayoutInflater
對象:
// class Activity extends ContextThemeWrapper
public class ContextThemeWrapper extends ContextWrapper {
private Resources.Theme mTheme;
private LayoutInflater mInflater;
@Override
public Object getSystemService(String name) {
// 保證 LayoutInflater 的局部單例
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
if (mInflater == null) {
mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
}
return mInflater;
}
return getBaseContext().getSystemService(name);
}
}
複製代碼
而不管activity.getLayoutInflater()
仍是LayoutInflater.from(activity)
,其內部最終都執行的是ContextThemeWrapper#getSystemService
(前者和PhoneWindow
還有點關係,這個後文會提), 所以獲取到的LayoutInflater
天然是同一個對象了:
public abstract class LayoutInflater {
public static LayoutInflater from(Context context) {
return (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
}
複製代碼
上一節咱們提到了Activity
啓動的過程,這個過程當中不可避免的要建立一個窗口,最終UI的佈局都要展現在這個窗口上,Android
中經過定義了PhoneWindow
類對這個UI的窗口進行描述。
Activity
將佈局填充相關的邏輯委託給了PhoneWindow
,Activity
的setContentView()
函數,其本質是調用了PhoneWindow
的setContentView()
函數。
public class PhoneWindow extends Window {
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}
// Activity.setContentView 其實是調用了 PhoneWindow.setContentView()
@Override
public void setContentView(int layoutResID) {
// ...
mLayoutInflater.inflate(layoutResID, mContentParent);
}
}
複製代碼
讀者須要清楚,activity.getLayoutInflater()
和activity.setContentView()
等方法都使用到了PhoneWindow
內部的LayoutInflater
對象,而PhoneWindow
內部對LayoutInflater
的實例化,仍然是調用context.getSystemService()
方法,所以和上一小節的結論並不衝突:
而不管
activity.getLayoutInflater()
仍是LayoutInflater.from(activity)
,其內部最終都執行的是ContextThemeWrapper#getSystemService
。
PhoneWindow
是如何實例化的呢,讀者認真思考可知,一個Activity
對應一個PhoneWindow
的UI窗口,所以當Activity
被建立時,PhoneWindow
就被須要被建立了,執行時機就在上文的ActivityThread.performLaunchActivity()
中:
public final class ActivityThread {
// 每當`Activity`被建立
private Activity performLaunchActivity() {
// ....
// 3.將 ContextImpl 也注入到 activity中
activity.attach(appContext, ....);
// ....
}
}
public class Activity extends ContextThemeWrapper {
final void attach(Context context, ...) {
// ...
// 初始化 PhoneWindow
// window構造方法中又經過 Context 實例化了 LayoutInflater
PhoneWindow mWindow = new PhoneWindow(this, ....);
}
}
複製代碼
設計到這裏,讀者應該對LayoutInflater
的總體流程已經有了一個初步的掌握,須要清楚的兩點是:
LayoutInflater
,都是經過ContextImpl.getSystemService()
獲取的,而且在Activity
等組件的生命週期內保持單例;Activity.setContentView()
函數,本質上也仍是經過LayoutInflater.inflate()
函數對佈局進行解析和建立。從思想上來看,LayoutInflater.inflate()
函數內部實現比較簡單直觀:
public View inflate(@LayoutRes int resource, ViewGroup root, boolean attachToRoot) {
// ...
}
複製代碼
對該函數的參數進行簡單概括以下:第一個參數表明所要加載的佈局,第二個參數是ViewGroup
,這個參數須要與第3個參數配合使用,attachToRoot
若是爲true
就把佈局添加到ViewGroup
中;若爲false
則只採用ViewGroup
的LayoutParams
做爲測量的依據卻不直接添加到ViewGroup
中。
從設計的角度上思考,該函數的設計過程當中,爲何須要定義這樣的三個參數?爲何這樣三個參數就能涵蓋咱們平常開發過程當中佈局填充的需求?
對於第一個資源id參數而言,UI的建立必然依賴了佈局文件資源的引用,所以這個參數無可厚非。
咱們先略過第二個參數,直接思考第三個參數,爲何須要這樣一個boolean
類型的值,以決定是否將建立的View
直接添加到指定的ViewGroup
中呢,不設計這個參數是否能夠?
換個角度思考,這個問題的本質實際上是:是否每一個View
的建立都必須當即添加在ViewGroup
中?答案固然是否認的,爲了保證性能,設計者不可能讓全部的View
被建立後都可以當即被當即添加在ViewGroup
中,這與目前Android
中不少組件的設計都有衝突,好比ViewStub
、RecyclerView
的條目、Fragment
等等。
所以,更好的方式應該是能夠經過一個boolean
的開關將整個過程切分紅2個小步驟,當View
生成並根據ViewGroup
的佈局參數生成了對應的測量依據後,開發者能夠根據需求手動靈活配置是否當即添加到ViewGroup
中——這就是第三個參數的由來。
那麼ViewGroup
類型的第二個參數爲何能夠爲空呢?實際開發過程當中,彷佛並無什麼場景在填充佈局時須要使ViewGroup
爲空?
讀者仔細思考能夠很容易得出結論,事實上該參數可空是有必要的——對於Activity
UI的建立而言,根結點最頂層的ViewGroup
必然是沒有父控件的,這時在佈局的建立時,就必須經過將null
做爲第二個參數交給LayoutInlater
的inflate()
方法,當View
被建立好後,將View
的佈局參數配置爲對應屏幕的寬高:
// DecorView.onResourcesLoaded()函數
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
// ...
// 建立最頂層的佈局時,須要指定父佈局爲null
final View root = inflater.inflate(layoutResource, null);
// 而後將寬高的佈局參數都指定爲 MATCH_PARENT(屏幕的寬高)
mDecorCaptionView.addView(root, new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
}
複製代碼
如今咱們理解了 爲何三個參數就能涵蓋開發過程當中佈局填充的需求,接下來繼續思考下一個問題,LayoutInflater
是如何解析xml
的。
xml
解析過程的思路很簡單;
XmlPullParser
解析器對象;View
的解析而言,一個View
的實例化依賴Context
上下文對象和attr
的屬性集,而設計者正是經過將上下文對象和屬性集做爲參數,經過 反射 注入到View
的構造器中對單個View
進行建立;xml
文件的解析而言,整個流程依然經過典型的遞歸思想,對佈局文件中的xml
文件進行遍歷解析,自底至頂對View
依次進行建立,最終完成了整個View
樹的建立。單個View
的實例化實現以下,這裏採用僞代碼的方式實現:
// LayoutInflater類
public final View createView(String name, String prefix, AttributeSet attrs) {
// ...
// 1.根據View的全名稱路徑,獲取View的Class對象
Class<? extends View> clazz = mContext.getClassLoader().loadClass(name + prefix).asSubclass(View.class);
// 2.獲取對應View的構造器
Constructor<? extends View> constructor = clazz.getConstructor(mConstructorSignature);
// 3.根據構造器,經過反射生成對應 View
args[0] = mContext;
args[1] = attrs;
final View view = constructor.newInstance(args);
return view;
}
複製代碼
對於總體解析流程而言,僞代碼實現以下:
void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs) {
// 1.解析當前控件
while (parser.next()!= XmlPullParser.END_TAG) {
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// 2.解析子佈局
rInflateChildren(parser, view, attrs, true);
// 全部子佈局解析結束,將當前控件及佈局參數添加到父佈局中
viewGroup.addView(view, params);
}
}
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs, boolean finishInflate){
// 3.子佈局做爲根佈局,經過遞歸的方式,層級向下一層層解析
// 繼續執行 1
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
複製代碼
至此,通常狀況下的佈局填充流程到此結束,inflate()
方法執行完畢,對應的佈局文件解析結束,並根據參數配置決定是否直接添加在ViewGroup
根佈局中。
LayoutInlater
的設計流程到此就結束了嗎,固然不是,更巧妙的設計還還沒有登場。
讀者須要清楚的是,到目前爲止,咱們的設計還遺留了2個明顯的缺陷:
View
的實例化都依賴了Java
的反射機制,這意味着額外性能的損耗;xml
佈局中聲明瞭fragment
標籤,會致使模塊之間極高的耦合。什麼叫作 fragment標籤會致使模塊之間極高的耦合 ?舉例來講,開發者在layout
文件中聲明這樣一個Fragment
:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">
<!-- 聲明一個fragment -->
<fragment android:id="@+id/fragment" android:name="com.github.qingmei2.myapplication.AFragment" android:layout_width="match_parent" android:layout_height="match_parent"/>
</android.support.constraint.ConstraintLayout>
複製代碼
看起來彷佛沒有什麼問題,但讀者認真思考會發現,若是這是一個v4包的Fragment
,是否意味着LayoutInflater
額外增長了對Fragment
類的依賴,相似這樣:
// LayoutInflater類
void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs) {
// 1.解析當前控件
while (parser.next()!= XmlPullParser.END_TAG) {
//【注意】2.若是標籤是一個Fragment,反射生成Fragment並返回
if (name == "fragment") {
Fragment fragment = clazz.newInstance();
// .....還會關聯到SupportFragmentManager、FragmentTransaction的依賴!
supportFragmentManager.beginTransaction().add(....).commit();
return;
}
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// 3.解析子佈局
rInflateChildren(parser, view, attrs, true);
// 全部子佈局解析結束,將當前控件及佈局參數添加到父佈局中
viewGroup.addView(view, params);
}
}
複製代碼
這致使了LayoutInflater
在解析fragment
標籤過程當中,強制依賴了不少設計者不但願的依賴(好比v4包下Fragment
相關類),繼續往下思考的話,還會遇到更多的問題,這裏再也不引伸。
那麼如何解決這樣的兩個問題呢?
考慮到 性能優化 和 可擴展性,設計者爲LayoutInflater
設計了一個LayoutInflater.Factory
接口,該接口設計得很是巧妙:在xml
解析過程當中,開發者能夠經過配置該接口對View
的建立過程進行攔截:經過new的方式建立控件以免大量地使用反射,亦或者 額外配置特殊標籤的解析邏輯以建立特殊組件 :
public abstract class LayoutInflater {
private Factory mFactory;
private Factory2 mFactory2;
private Factory2 mPrivateFactory;
public void setFactory(Factory factory) {
//...
}
public void setFactory2(Factory2 factory) {
// Factory 只能被set一次
if (mFactorySet) {
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
mFactorySet = true;
mFactory = mFactory2 = factory;
// ...
}
public interface Factory {
public View onCreateView(String name, Context context, AttributeSet attrs);
}
public interface Factory2 extends Factory {
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
}
複製代碼
正如上文所說的,Factory
接口的意義是在xml
解析過程當中,開發者能夠經過配置該接口對View
的建立過程進行攔截,對於View
的實例化,最終實現的僞代碼以下:
View createViewFromTag() {
View view;
// 1. 若是mFactory2不爲空, 用mFactory2 攔截建立 View
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
// 2. 若是mFactory不爲空, 用mFactory 攔截建立 View
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
// 3. 若是通過攔截機制以後,view仍然是null,再經過系統反射的方式,對View進行實例化
if (view == null) {
view = createView(name, null, attrs);
}
}
複製代碼
理解了LayoutInflater.Factory
接口設計的思路,接下來一塊兒來思考如何解決上文中提到的2個問題。
AppCompatActivity
的源碼中隱晦地配置LayoutInflater.Factory
減小了大量反射建立控件的狀況——設計者的思路是,在AppCompatActivity
的onCreate()
方法中,爲LayoutInflater
對象調用了setFactory2()
方法:
// AppCompatActivity類
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
getDelegate().installViewFactory();
//...
}
// AppCompatDelegateImpl類
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
}
}
複製代碼
配置以後,在inflate()
過程當中,系統的基礎控件的實例化都經過代碼攔截,並經過new
的方式進行返回:
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;
// ...
// Android 基礎組件都經過new方式進行建立
}
複製代碼
源碼也說明了,即便開發者在xml
文件中配置的是Button
,setContentView()
以後,生成的控件實際上是AppCompatButton
, TextView
或者ImageView
亦然,在避免額外的性能損失的同時,也保證了Android
版本的向下兼容。
爲何Fragment
沒有定義相似void setContentView(R.layout.xxx)
的函數對佈局進行填充,而是使用了View onCreateView()
這樣的函數,讓開發者填充並返回一個對應的View
呢?
緣由就在於在佈局填充的過程當中,Fragment
最終被視爲一個子控件並添加到了ViewGroup
中,設計者將FragmentManagerImpl
做爲FragmentManager
的實現類,同時實現了LayoutInflater.Factory2
接口。
而在佈局文件中fragment
標籤解析的過程當中,其實是調用了FragmentManagerImpl.onCreateView()
函數,生成了Fragment
以後並將View
返回,跳過了系統反射生成View
相關的邏輯:
# android.support.v4.app.FragmentManager$FragmentManagerImpl
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (!"fragment".equals(name)) {
return null;
}
// 若是標籤是`fragment`,生成Fragment,並返回Fragment的Root
return fragment.mView;
}
複製代碼
經過定義LayoutInflater.Factory
接口,設計者將Fragment
的功能抽象爲一個View
(雖然Fragment
並非一個View
),並交給FragmentManagerImpl
進行處理,減小了模塊之間的耦合,能夠說是很是優秀的設計。
實際上
LayoutInflater.Factory
接口的設計還有更多細節(好比LayoutInflater.FactoryMerger
類),篇幅緣由,本文不贅述,有興趣的讀者能夠研究一下。
LayoutInflater
總體的設計很是複雜且巧妙,從應用啓動到進程間通訊,從組件的啓動再到組件UI的渲染,均可以看到LayoutInflater
的身影,所以很是值得認真學習一番,建議讀者參考本文開篇的思惟導圖並結合Android
源碼進行總體小結。
Hello,我是 卻把清梅嗅 ,若是您以爲文章對您有價值,歡迎 ❤️,也歡迎關注個人 博客 或者 Github。
若是您以爲文章還差了那麼點東西,也請經過關注督促我寫出更好的文章——萬一哪天我進步了呢?