這次跟大家分享的是關於LayoutInflater的使用,在開發的過程中,LayoutInfalter經常用於載入檢視,對,今天我們們來聊的就是,關於載入檢視的一些事兒,我記得之前一位曾共事過的一位同事問到我一個問題,activity是如何載入資原始檔來顯示介面的,古話說得好,知其然不知其所以然,因此在寫這篇文章的時候我也做了不少的準備,在這裡我先引出幾個問題,然後我們通過問題在原始碼中尋找答案。
1.如何獲取LayoutInflater? 2.如何使用LayoutInflater?為什麼? 3.Activity是如何載入檢視的? 4.如何優化我們的佈局?
首先我們先看一下LayoutInflater是如何獲取的。
LayoutInflater inflater=LayoutInflater.from(context);
複製程式碼
我們通過LayoutInflater.from(Context)獲取LayoutInflater,我們繼續進入LayoutInflater.java探索一番。 LayoutInflater.java:
/**
* Obtains the LayoutInflater from the given context.
*/
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
複製程式碼
在LayoutInflater裡面,通過靜態方法from(Context),然後繼續呼叫Context中的方法getSystemService獲取LayoutInflater,我們往Context繼續看。
Context.java:
public abstract Object getSystemService(@ServiceName @NonNull String name);
複製程式碼
大家會發現,怎麼點進去這是個抽象方法,其實Context是一個抽象類,真正實現的是ContextImpl這個類,我們就繼續看ContextImpl: ContextImpl.java:
@Override
public Object getSystemService(String name) {
//繼續呼叫SystemServiceRegistry.getSystemService
return SystemServiceRegistry.getSystemService(this, name);
}
複製程式碼
SystemServiceRegistry.java:
/**
* Gets a system service from a given context.
*/
public static Object getSystemService(ContextImpl ctx, String name) {
//先獲取ServiceFetcher,在通過fetcher獲取LayoutInflate
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}
複製程式碼
SystemServiceRegistry這個類中的靜態方法getSystemService,通過SYSTEM_SERVICE_FETCHERS獲取ServiceFetcher,我們先看看SYSTEM_SERVICE_FETCHERS跟ServiceFetcher在SystemServiceRegistry中的定義。 SystemServiceRegistry.java:
//使用鍵值對來儲存
private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
new HashMap<String, ServiceFetcher<?>>();
/**
* Base interface for classes that fetch services.
* These objects must only be created during static initialization.
*/
static abstract interface ServiceFetcher<T> {
//只有一條介面,通過context獲取服務,先看一下其實現類
T getService(ContextImpl ctx);
}
複製程式碼
SYSTEM_SERVICE_FETCHERS在SystemServiceRegistry這個類中作為全域性常量,通過鍵值對的方式用來儲存ServiceFetcher,而ServiceFetcher又是什麼?在原始碼中,ServiceFetcher是一條介面,通過泛型T定義了getService(ContextImpl)來獲取服務物件。那麼具體ServiceFetcher具體的實現在什麼地方?在SystemServiceRegistry中,有一段這樣的程式碼: SystemServiceRegistry.java:
static {
.....
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
......
}
複製程式碼
在這個靜態程式碼塊中,通過registerService進行初始化註冊服務。我們先看看這個靜態方法。
/**
* Statically registers a system service with the context.
* This method must be called during static initialization only.
*/
private static <T> void registerService(String serviceName, Class<T> serviceClass,
ServiceFetcher<T> serviceFetcher) {
SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
}
複製程式碼
registerService這個一段函式的作用就是用來通過鍵值對的方式,儲存服務物件,也就是說,SystemServiceRegistry會初始化的時候註冊各種服務,而我們的也看到Context.LAYOUT_INFLATER_SERVICE作為key來獲取LayoutInfalter。
Context.java:
/**
* 定義這個常量,用於獲取系統服務中的LayoutInflate
* Use with {@link #getSystemService} to retrieve a
* {@link android.view.LayoutInflater} for inflating layout resources in this
* context.
*
* @see #getSystemService
* @see android.view.LayoutInflater
*/
public static final String LAYOUT_INFLATER_SERVICE = "layout_inflater";
複製程式碼
我們繼續看看ServiceFetcher的實現類:
/**
* Override this class when the system service constructor needs a
* ContextImpl and should be cached and retained by that context.
*/
static abstract class CachedServiceFetcher<T> implements ServiceFetcher<T> {
private final int mCacheIndex;
public CachedServiceFetcher() {
mCacheIndex = sServiceCacheSize++;
}
@Override
@SuppressWarnings("unchecked")
public final T getService(ContextImpl ctx) {
final Object[] cache = ctx.mServiceCache;
synchronized (cache) {
// Fetch or create the service.
Object service = cache[mCacheIndex];
if (service == null) {
service = createService(ctx);
cache[mCacheIndex] = service;
}
return (T)service;
}
}
public abstract T createService(ContextImpl ctx);
}
複製程式碼
CachedServiceFetcher的作用用於儲存我們的泛型T,同時這個CachedServiceFetcher有一個抽象方法createService,createService這個方法用來建立這個服務,因此使用這個類就必須重寫這個方法,我們繼續看回: ServiceFetcher.java:
staic{
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
}
複製程式碼
現在看回來這裡,註冊服務不就是通過通過鍵值對的方式進行儲存這個物件,然而我們獲取到的LayoutInflater其實是PhoneLayoutInflater。PhoneLayoutInflater繼承於LayoutInfalter.
小結: 我們獲取LayoutInflater物件,可以通過兩種方法獲取:
LayoutInflater inflater1=LayoutInflater.from(context);
LayoutInflater inflater2= (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
複製程式碼
context的實現類contextImpl,呼叫SystemServiceRegistry.getSystemService,通過鍵值對的方式獲取PhoneLayoutInflater物件,從中我們也看到,這種方式通過鍵值對的方式快取起這個物件,避免建立過多的物件,這是也一種單例的設計模式。
現在我們們來看一下,我們是如何使用LayoutInflater來獲取View,我們先從一段小程式碼看看。 我新建一個佈局檔案,my_btn.xml:
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:text="我是一個按鈕">
</Button>
複製程式碼
在佈局檔案中,我設定其layoutwidth與layout_height分別是填充螢幕。 在activity的content_main.xml佈局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:id="@+id/content_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="ffzxcom.mytest.toucheventapplication.MainActivity"
tools:showIn="@layout/activity_main">
</RelativeLayout>
複製程式碼
LayoutInflater.java: 方法一:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
複製程式碼
方法二:
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) + ")");
}
//通過資源載入器和資源Id,獲取xml解析器
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
複製程式碼
我們從程式碼中看到,無論是方法一,還是方法二,最終還是會呼叫方法二進行載入,我們就從方法二的三個引數,進行分析一下。
@LayoutRes int resource 資原始檔的Id
@Nullable ViewGroup root 根view,就是待載入view的父佈局
boolean attachToRoot 是否載入到父佈局中
複製程式碼
從方法一看到,其實就是在呼叫方法二,只是方法一的第三個傳參利用root!=null進行判斷而已,實際上最終還是呼叫方法二。 我們先利用程式碼進行分析一下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mContainer = (RelativeLayout) findViewById(R.id.content_main);
View view1 = LayoutInflater.from(this).inflate(R.layout.my_btn, null);
View view2 = LayoutInflater.from(this).inflate(R.layout.my_btn, mContainer, false);
View view3 = LayoutInflater.from(this).inflate(R.layout.my_btn, mContainer, true);
View view4 = LayoutInflater.from(this).inflate(R.layout.my_btn, mContainer);
Log.e("view1:", view1 + "");
Log.e("view2:", view2 + "");
Log.e("view3:", view3 + "");
Log.e("view4:", view4 + "");
}
複製程式碼
我們載入同一個佈局檔案my_btn.xml,獲取到view,然後分別輸出,觀察有什麼不一樣:
view1:: android.support.v7.widget.AppCompatButton{27f4a822 VFED..C. ......I. 0,0-0,0}
view2:: android.support.v7.widget.AppCompatButton{14fb5dd2 VFED..C. ......I. 0,0-0,0}
view3:: android.widget.RelativeLayout{2a6bba10 V.E..... ......I. 0,0-0,0 #7f0c006f app:id/content_main}
view4:: android.widget.RelativeLayout{2a6bba10 V.E..... ......I. 0,0-0,0 #7f0c006f app:id/content_main}
複製程式碼
問題來了,為什麼我載入同一個佈局,得到的view一個是Button,一個是RelativeLayout,我們每一個分析一下:
View1:
LayoutInflater.from(this).inflate(R.layout.my_btn, null);
我們看到,第二個引數root為空,也就是說實際上是呼叫方法二(root!=null):
LayoutInflater.from(this).inflate(R.layout.my_btn, null,false);
第三個引數attachToRoot 的意思是,是否把這個view新增到root裡面,如果為false則不返回root,而是這個的本身,如果為true的話,就是返回新增view後的root.
因此,view1得到的是Button.
View2:
LayoutInflater.from(this).inflate(R.layout.my_btn, mContainer, false);
同上可得,第三個引數attachToRoot 為false.也就是不把這個view新增到root裡面去
因此,返回的是view2,就是Button.
View3:
LayoutInflater.from(this).inflate(R.layout.my_btn, mContainer, true);
第三個引數為true,也就是意味待載入的view會附在root上,並且返回root.
因此,我們view3返回的是這個RelativeLayout,並且是新增button後的RelativeLayout.
View4:
LayoutInflater.from(this).inflate(R.layout.my_btn, mContainer);
根據方法一跟方法二的比較,root!=null.view4跟view3的載入是一樣的,同理返回的是RelativeLayout.
根據以上的結論我們繼續往下面探究,我們通過LayoutInflater.from(this).inflate(R.layout.my_btn, null)獲取到了button,再把這個Button新增到mContainer中。再觀察一下效果,注意,這個按鈕的佈局寬高是佔全屏的。
複製程式碼
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:text="我是一個按鈕">
</Button>
複製程式碼
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mContainer = (RelativeLayout) findViewById(R.id.content_main);
Button btn = (Button) LayoutInflater.from(this).inflate(R.layout.my_btn, null);
mContainer.addView(btn);
}
複製程式碼
大家看到問題了嗎?為什麼我在my_btn.xml中設定了button的佈局寬高是全屏,怎麼不起作用了?難道說在my_btn.xml中Button的layout_width和layout_height起不了作用?我們從原始碼中看一下:
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) + ")");
}
//通過資源載入器和資源Id,獲取xml解析器
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
複製程式碼
通過資源管理獲取xml解析器,繼續往下看:
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;
}
}
複製程式碼
在這一塊程式碼中,先宣告一個變數result,這個result用來返回最終的結果,在我們的演示中,如果inflate(resource,root,isAttachRoot)中的root為空,那麼佈局引數params為空,並且根據關鍵程式碼D可得,返回的result就是temp,也就是Button本身。因此在以上例子中,如果說root不為空的話,Button中宣告的layout_width與layout_height起到了作用。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mContainer = (RelativeLayout) findViewById(R.id.content_main);
View view1 = LayoutInflater.from(this).inflate(R.layout.my_btn, mContainer,false);
mContainer.addView(view1);
}
複製程式碼
注:如果通過LayoutInflater.from(this).inflate(R.layout.my_btn, mContainer)或者LayoutInflater.from(this).inflate(R.layout.my_btn, mContainer,true)載入檢視,不需要再額外的使用mContainer.addView(view),因為返回的預設就是root本身,在關鍵程式碼C中可看到:
if (root != null && attachToRoot) {
root.addView(temp, params);
}
複製程式碼
root會新增temp進去,在程式碼初始化的時候,result預設就是root,我們不需要addView,在inflate中會幫我們操作,如果我們還要addView的話,就會丟擲異常: The specified child already has a parent. You must call removeView() on the child's parent first.
小結: 如果LayoutInflater的inflate中,傳參root為空時,載入檢視的根view佈局寬高無效。反之根據關鍵程式碼C與關鍵程式碼A,分別對view進行設定佈局引數。
我們們來看一下activity是如何載入檢視,我們從這一段程式碼開始:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
複製程式碼
我們往setContentView繼續探索,找到Activity的setContentView()
/**
* Set the activity content from a layout resource. The resource will be
* inflated, adding all top-level views to the activity.
*
* @param layoutResID Resource ID to be inflated.
*
* @see #setContentView(android.view.View)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
public void setContentView(@LayoutRes int layoutResID) {
//獲取視窗,設定contentView
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
複製程式碼
getWindow()實際上是獲取window物件,但Window類是抽象類,具體的實現是PhoneWindow,我們往這個類看看: PhoneWindow.java:
@Override
public void setContentView(int layoutResID) {
//判斷mContentParent是否為空,如果為空,建立
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 清空mContentParent 所有子view
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
……
} else {
//通過layoutInflate載入檢視
mLayoutInflater.inflate(layoutResID, mContentParent);
}
……
}
複製程式碼
activity載入檢視,最後還是通過LayoutInflater進行載入檢視,activity的介面結構如下:
我們的mContentView就是ContentView,因此通過LayoutInflater載入檢視進入ContentView。而root就是mContentView,因此我們在Activity不需要自己addView().
總結: 知其然不知其所以然,這對於LayoutInflater描述再合適不過了,文章中本來還涉及到了關於如何使用LayoutInflater中遍歷view,程式碼太長就不一一展示,而且在inflate中我們可以看到,通過使用merge標籤,可以減少view的層級,直接把merge標籤內的子view直接新增到rootview中,因此佈局優化能提高檢視載入的效能,提高效率。還有獲取LayoutInflater的方式,通過鍵值對進行快取LayoutInflater,這是在android中單例設計的一種體驗。