反思 系列博客是個人一種新學習方式的嘗試,該系列起源和目錄請參考 這裏 。java
Android
體系自己很是宏大,源碼中值得思考和借鑑之處衆多。以LayoutInflater
自己爲例,其整個流程中除了調用inflate()
函數 填充佈局 功能以外,還涉及到了 應用啓動、調用系統服務(進程間通訊)、對應組件做用域內單例管理、額外功能擴展 等等一系列複雜的邏輯。android
本文筆者將針對LayoutInlater
的整個設計思路進行描述,其總體結構以下圖:git
顧名思義,LayoutInflater
的做用就是 佈局填充器 ,其行爲本質是調用了Android
自己提供的 系統服務。而在Android
系統的設計中,獲取系統服務的實現方式就是經過ServiceManager
來取得和對應服務交互的IBinder
對象,而後建立對應系統服務的代理。github
Android
應用層將系統服務註冊相關的API
放在了SystemServiceRegistry
類中,而將註冊服務行爲的代碼放在了ContextImpl
類中,ContextImpl
類實現了Context
類下的全部抽象方法。性能優化
Android
應用層還定義了一個Context
的另一個子類:ContextWrapper
,Activity
、Service
等組件繼承了ContextWrapper
, 每一個ContextWrapper
的實例有且僅對應一個ContextImpl
,造成一一對應的關係,該類是 裝飾器模式 的體現:保證了Context
類公共功能代碼和不一樣功能代碼的隔離。markdown
此外,雖然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
)。oop
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。
若是您以爲文章還差了那麼點東西,也請經過關注督促我寫出更好的文章——萬一哪天我進步了呢?