反思|Android LayoutInflater機制的設計與實現

反思 系列博客是個人一種新學習方式的嘗試,該系列起源和目錄請參考 這裏php

概述

Android體系自己很是宏大,源碼中值得思考和借鑑之處衆多。以LayoutInflater自己爲例,其整個流程中除了調用inflate()函數 填充佈局 功能以外,還涉及到了 應用啓動調用系統服務(進程間通訊)、對應組件做用域內單例管理額外功能擴展 等等一系列複雜的邏輯。java

本文筆者將針對LayoutInlater的整個設計思路進行描述,其總體結構以下圖:android

總體思路

一、建立流程

顧名思義,LayoutInflater的做用就是 佈局填充器 ,其行爲本質是調用了Android自己提供的 系統服務。而在Android系統的設計中,獲取系統服務的實現方式就是經過ServiceManager來取得和對應服務交互的IBinder對象,而後建立對應系統服務的代理。git

Android應用層將系統服務註冊相關的API放在了SystemServiceRegistry類中,而將註冊服務行爲的代碼放在了ContextImpl類中,ContextImpl類實現了Context類下的全部抽象方法。github

Android應用層還定義了一個Context的另一個子類:ContextWrapperActivityService等組件繼承了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中的應用很是廣泛,AppCompatActivityFragmentManager就是最有力的體現,LayoutInflater.inflate()方法的理解雖然重要,但筆者竊覺得LayoutInflater.Factory2的重要性與其相比不逞多讓。

對於LayoutInflater總體不甚熟悉的開發者而言,本小節文字描述彷佛晦澀難懂,且不免有是否過分設計的疑惑,但這些文字的本質倒是佈局填充流程總體的設計思想,讀者不該該將本文視爲源碼分析,而應該將本身代入到設計的過程當中

建立流程

1.Context:系統服務的提供者

上文提到,LayoutInflater做爲系統服務之一,獲取方式是經過ServiceManager來取得和對應服務交互的IBinder對象,而後建立對應系統服務的代理。

Binder機制相關並不是本文的重點,讀者能夠注意到,Android的設計者將獲取系統服務的接口交給了Context類,意味着開發者能夠經過任意一個Context的實現類獲取系統服務,包括不限於ActivityServiceApplication等等:

public abstract class Context {
  // 獲取系統服務
  public abstract Object getSystemService(String name);
  // ......
}
複製代碼

讀者須要理解,Context類地職責並不是只針對 系統服務 進行提供,還包括諸如 啓動其它組件獲取SharedPerferences 等等,其中大部分功能對於Context的子類而言都是公共的,所以沒有必要每一個子類都對其進行實現。

Android設計者並無直接經過繼承的方式將公共業務邏輯放入Base類供組件調用或者重寫,而是借鑑了 裝飾器模式 的思想:分別定義了ContextImplContextWrapper兩個子類:

2.ContextImpl:Context的公共API實現

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類) 在代碼中只有一處聲明和調用,大幅下降了模塊之間的耦合。

3.ContextWrapper:Context的裝飾器

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, ...)函數,該函數很重要,在【佈局流程】一節中會繼續引伸。

4.組件的局部單例

讀者也許注意到,對於單個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的窗口進行描述。

1.PhoneWindow:setContentView()的真正實現

Activity將佈局填充相關的邏輯委託給了PhoneWindowActivitysetContentView()函數,其本質是調用了PhoneWindowsetContentView()函數。

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的總體流程已經有了一個初步的掌握,須要清楚的兩點是:

  • 1.不管是哪一種方式獲取到的LayoutInflater,都是經過ContextImpl.getSystemService()獲取的,而且在Activity等組件的生命週期內保持單例;
  • 2.即便是Activity.setContentView()函數,本質上也仍是經過LayoutInflater.inflate()函數對佈局進行解析和建立。

2.inflate()流程的設計和實現

從思想上來看,LayoutInflater.inflate()函數內部實現比較簡單直觀:

public View inflate(@LayoutRes int resource, ViewGroup root, boolean attachToRoot) {
      // ...
}
複製代碼

對該函數的參數進行簡單概括以下:第一個參數表明所要加載的佈局,第二個參數是ViewGroup,這個參數須要與第3個參數配合使用,attachToRoot若是爲true就把佈局添加到ViewGroup中;若爲false則只採用ViewGroupLayoutParams做爲測量的依據卻不直接添加到ViewGroup中。

從設計的角度上思考,該函數的設計過程當中,爲何須要定義這樣的三個參數?爲何這樣三個參數就能涵蓋咱們平常開發過程當中佈局填充的需求?

2.1 三個火槍手

對於第一個資源id參數而言,UI的建立必然依賴了佈局文件資源的引用,所以這個參數無可厚非。

咱們先略過第二個參數,直接思考第三個參數,爲何須要這樣一個boolean類型的值,以決定是否將建立的View直接添加到指定的ViewGroup中呢,不設計這個參數是否能夠?

換個角度思考,這個問題的本質實際上是:是否每一個View的建立都必須當即添加在ViewGroup中?答案固然是否認的,爲了保證性能,設計者不可能讓全部的View被建立後都可以當即被當即添加在ViewGroup中,這與目前Android中不少組件的設計都有衝突,好比ViewStubRecyclerView的條目、Fragment等等。

所以,更好的方式應該是能夠經過一個boolean的開關將整個過程切分紅2個小步驟,當View生成並根據ViewGroup的佈局參數生成了對應的測量依據後,開發者能夠根據需求手動靈活配置是否當即添加到ViewGroup中——這就是第三個參數的由來。

那麼ViewGroup類型的第二個參數爲何能夠爲空呢?實際開發過程當中,彷佛並無什麼場景在填充佈局時須要使ViewGroup爲空?

讀者仔細思考能夠很容易得出結論,事實上該參數可空是有必要的——對於ActivityUI的建立而言,根結點最頂層的ViewGroup必然是沒有父控件的,這時在佈局的建立時,就必須經過將null做爲第二個參數交給LayoutInlaterinflate()方法,當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的。

2.2 xml解析流程

xml解析過程的思路很簡單;

    1. 首先根據佈局文件,生成對應佈局的XmlPullParser解析器對象;
    1. 對於單個View的解析而言,一個View的實例化依賴Context上下文對象和attr的屬性集,而設計者正是經過將上下文對象和屬性集做爲參數,經過 反射 注入到View的構造器中對單個View進行建立;
    1. 對於整個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個明顯的缺陷:

  • 1.佈局的加載流程中,每個View的實例化都依賴了Java的反射機制,這意味着額外性能的損耗;
  • 2.若是在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減小了大量反射建立控件的狀況——設計者的思路是,在AppCompatActivityonCreate()方法中,爲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文件中配置的是ButtonsetContentView()以後,生成的控件實際上是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

若是您以爲文章還差了那麼點東西,也請經過關注督促我寫出更好的文章——萬一哪天我進步了呢?

相關文章
相關標籤/搜索