引言
- 這是 Android 10 源碼分析系列的第 4 篇
- 分支:android-10.0.0_r14
- 全文閱讀大概 10 分鐘
通過這篇文章你將學習到以下內容,將在文末會給出相應的答案
- View 中的 INVISIBLE、VISIBLE、GONE 都有什么作用?
- 為什么 ViewStub 是大小為0的視圖
- ViewStub 有什么作用?
- ViewStub 是如何創建的?
- 為什么 ViewStub 能做到延遲加載?
- ViewStub 指定的 Layout 布局文件是什么時候被加載的?
- LayoutInflater 是一個抽象類它如何被創建的?
- 系統服務存儲在哪里?如何獲取和添加系統服務?
在上一篇文章 0xA02 Android 10 源碼分析:APK 加載流程之資源加載 中通過 LayoutInflater.inflate 方法解析 XML 文件,了解到了系統如何對 merge、include 標簽是如何處理的,本文主要圍繞以下兩方面內容
- 系統對 ViewStub 如何處理?
- LayoutInflater 是如何被創建的?
系統對 merge、include 是如何處理的
- 使用 merge 標簽必須有父布局,且依賴于父布局加載
- merge 并不是一個 ViewGroup,也不是一個 View,它相當于聲明了一些視圖,等待被添加,解析過程中遇到 merge 標簽會將 merge 標簽下面的所有子 view 添加到根布局中
- merge 標簽在 XML 中必須是根元素
- 相反的 include 不能作為根元素,需要放在一個 ViewGroup 中
- 使用 include 標簽必須指定有效的 layout 屬性
- 使用 include 標簽不寫寬高是沒有關系的,會去解析被 include 的 layout
merge 標簽為什么可以起到優化布局的效果?
解析過程中遇到 merge 標簽,會調用 rInflate 方法,部分代碼如下
// 根據元素名解析,生成對應的view
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// rInflateChildren方法內部調用的rInflate方法,深度優先遍歷解析所有的子view
rInflateChildren(parser, view, attrs, true);
// 添加解析的view
viewGroup.addView(view, params);
解析 merge 標簽下面的所有子 view,然后添加到根布局中
更多信息查看0xA02 Android 10 源碼分析:APK 加載流程之資源加載,接下來看一下系統對 ViewStub 如何處理
1. ViewStub是什么
關于 ViewStub 的介紹,可以點擊下方官網鏈接查看
官網鏈接https://developer.android.google.cn/reference/android...ViewStub
ViewStub 的繼承結構
簡單來說主要以下幾點:
- ViewStub 控件是一個不可見,
- 大小為 0 的視圖
- 當 ViewStub 控件設置可見,或者調用 inflate() 方法,ViewStub 所指定的 layout 資源就會被加載
- ViewStub 也會從其父控件中移除,ViewStub 會被新加載的 layout 文件代替
為什么 ViewStub 是大小為 0 的視圖
frameworks/base/core/java/android/view/ViewStub.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 設置視圖大小為0
setMeasuredDimension(0, 0);
}
ViewStub 的作用
主要用來延遲布局的加載,例如:在 Android 中非常常見的布局,用 ListView 來展示列表信息,當沒有數據或者網絡加載失敗時, 加載空的 ListView 會占用一些資源,如果用 ViewStub 包裹 ListView,當有數據時,才會調用 inflate() 方法顯示 ListView,起到延遲加載了布局效果
1.1 ViewStub 是如何被創建的
在上篇文章 0xA02 Android 10 源碼分析:APK 加載流程之資源加載 中,介紹了 View 的創建是通過調用了 LayoutInflater.createView 方法根據完整的類的路徑名利用反射機制構建 View 對象,因為 ViewStub 是繼承 View,所以 ViewStub 的創建和 View 的創建是相同的,來看一下 LayoutInflater.createView 方法
frameworks/base/core/java/android/view/LayoutInflater.java
...
try {
// 利用構造函數,創建View
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// 如果是ViewStub,則設置LayoutInflater
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;
} finally {
mConstructorArgs[0] = lastContext;
}
...
根據完整的類的路徑名利用反射機制構建 View 對象,如果遇到 ViewStub 將當前 LayoutInflater 設置給 ViewStub,當 ViewStub 控件設置可見,或者調用 inflate(),會調用 LayoutInflater 的 inflate 方法完成布局加載,接下來分析 ViewStub 的構造方法
1.2 ViewStub 的構造方法
在上面提到了根據完整的類的路徑名利用反射機制構建 View 對象,當 View 對象被創建的時候,會調用它的構造函數,來看一下 ViewStub 的構造方法
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.ViewStub, defStyleAttr, defStyleRes);
saveAttributeDataForStyleable(context, R.styleable.ViewStub, attrs, a, defStyleAttr,
defStyleRes);
// 解析xml中設置的 android:inflatedId 的屬性
mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
// 解析xml中設置的 android:layout 屬性
mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
// 解析xml中設置的 android:id 屬性
mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
a.recycle();
// view不可見
setVisibility(GONE);
// 不會調用 onDraw 方法繪制內容
setWillNotDraw(true);
}
- 獲取 android:inflatedId、android:layout、android:id 的值
- 調用 setVisibility 方法,設置 View 不可見
- 調用 setWillNotDraw 方法,不會調用 onDraw 方法繪制內容
在上面提到了如果想要加載 ViewStub 所指定的 layout 資源,需要設置 ViewStub 控件設置可見,或者調用 inflate() 方法,來看一下 ViewStub 的 setVisibility 方法
1.3 ViewStub 的 setVisibility 方法
setVisibility(int visibility) 方法,參數 visibility 對應三個值分別是 INVISIBLE、VISIBLE、GONE
- VISIBLE:視圖可見
- INVISIBLE:視圖不可見的,它仍然占用布局的空間
- GONE:視圖不可見,它不占用布局的空間
接下里查看一下 ViewStub 的 setVisibility 方法
frameworks/base/core/java/android/view/ViewStub.java
@Override
public void setVisibility(int visibility) {
if (mInflatedViewRef != null) {
// mInflatedViewRef 是 WeakReference的實例,調用inflate方法時候初始化
View view = mInflatedViewRef.get();
if (view != null) {
// 設置View可見
view.setVisibility(visibility);
} else {
throw new IllegalStateException("setVisibility called on un-referenced view");
}
} else {
super.setVisibility(visibility);
// 當View為空且設置視圖可見(VISIBLE、INVISIBLE),調用inflate方法
if (visibility == VISIBLE || visibility == INVISIBLE) {
inflate();
}
}
}
- mInflatedViewRef 是 WeakReference 的實例,調用 inflate 方法時候初始化
- 從 mInflatedViewRef 緩存中獲取 View,并且設置 View 可見
- 當 View 為空且設置視圖可見(VISIBLE、INVISIBLE),會調用 inflate方法
1.4 ViewStub.inflate 方法
調用了 ViewStub 的 setVisibility 方法,最后都會調用 ViewStub.inflate 方法,來查看一下
frameworks/base/core/java/android/view/ViewStub.java
public View inflate() {
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
// 解析布局視圖
// 返回的view是android:layout指定的布局文件最頂層的view
final View view = inflateViewNoAdd(parent);
// 移除ViewStub
// 添加view到被移除的ViewStub的位置
replaceSelfWithView(view, parent);
// 添加view到 mInflatedViewRef 中
mInflatedViewRef = new WeakReference<>(view);
if (mInflateListener != null) {
// 加載完成之后,回調onInflate 方法
mInflateListener.onInflate(this, view);
}
return view;
} else {
// 需要在xml中設置android:layout,不是layout,否則會拋出異常
throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
}
} else {
// ViewStub不能作為根布局,它需要放在ViewGroup中, 否則會拋出異常
throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
}
}
- 調用 inflateViewNoAdd 方法返回 android:layout 指定的布局文件最頂層的 View
- 調用 replaceSelfWithView 方法, 移除 ViewStub, 添加 View 到被移除的 ViewStub 的位置
- 添加 View 到 mInflatedViewRef 中
- 加載完成之后,回調 onInflate 方法
需要注意以下兩點:
- 使用 ViewStub 需要在 XML 中設置 android:layout,不是 layout,否則會拋出異常
- ViewStub 不能作為根布局,它需要放在 ViewGroup 中, 否則會拋出異常
來查看一下 inflateViewNoAdd 方法和 replaceSelfWithView 方法
1.5 ViewStub.inflateViewNoAdd 方法
調用 inflateViewNoAdd 方法返回 android:layout 指定的布局文件最頂層的 View
frameworks/base/core/java/android/view/ViewStub.java
private View inflateViewNoAdd(ViewGroup parent) {
final LayoutInflater factory;
// mInflater 是View被創建的時候,如果是ViewStub, 將LayoutInflater賦值給mInflater
if (mInflater != null) {
factory = mInflater;
} else {
// 如果mInflater為空,則創建LayoutInflater
factory = LayoutInflater.from(mContext);
}
// 從指定的 mLayoutResource 資源中解析布局視圖
// mLayoutResource 是在xml設置的 Android:layout 指定的布局文件
final View view = factory.inflate(mLayoutResource, parent, false);
// mInflatedId 是在xml設置的 inflateId
if (mInflatedId != NO_ID) {
// 將id復制給view
view.setId(mInflatedId);
//注意:如果指定了mInflatedId , 被inflate的layoutView的id就是mInflatedId
}
return view;
}
- mInflater 是 View 被創建的時候,如果是 ViewStub, 將 LayoutInflater 賦值給 mInflater
- 如果 mInflater 為空則通過 LayoutInflater.from(mContext) 構建 LayoutInflater
- 調用 LayoutInflater 的 inflate 方法解析布局視圖
- 將 mInflatedId 設置 View
1.6 ViewStub.replaceSelfWithView 方法
調用 replaceSelfWithView 方法, 移除 ViewStub, 添加 View 到被移除的 ViewStub 的位置
frameworks/base/core/java/android/view/ViewStub.java
private void replaceSelfWithView(View view, ViewGroup parent) {
// 獲取ViewStub在視圖中的位置
final int index = parent.indexOfChild(this);
// 移除ViewStub
// 注意:調用removeViewInLayout方法之后,調用findViewById()是找不到該ViewStub對象
parent.removeViewInLayout(this);
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
// 將xml中指定的 android:layout 布局文件中最頂層的View,添加到被移除的 ViewStub的位置
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
}
- 獲取 ViewStub 在視圖中的位置,然后移除 ViewStub
- 添加 android:layout 布局文件中最頂層的 View 到被移除的 ViewStub 的位置
1.7 ViewStub 的注意事項
- 使用 ViewStub 需要在 XML 中設置 android:layout,不是 layout,否則會拋出異常
throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
- ViewStub 不能作為根布局,它需要放在 ViewGroup 中, 否則會拋出異常
throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
- 一旦調用 setVisibility(View.VISIBLE) 或者 inflate() 方法之后,該 ViewStub 將會從試圖中被移除(此時調用 findViewById() 是找不到該 ViewStub 對象).
// 獲取ViewStub在視圖中的位置
final int index = parent.indexOfChild(this);
// 移除ViewStub
// 注意:調用removeViewInLayout方法之后,調用findViewById()是找不到該ViewStub對象
parent.removeViewInLayout(this);
- 如果指定了 mInflatedId , 被 inflate 的 layoutView 的 id 就是 mInflatedId
// mInflatedId 是在xml設置的 inflateId
if (mInflatedId != NO_ID) {
// 將id復制給view
view.setId(mInflatedId);
//注意:如果指定了mInflatedId , 被inflate的layoutView的id就是mInflatedId
}
- 被 inflate 的 layoutView 的 layoutParams 與 ViewStub 的 layoutParams 相同.
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
// 將xml中指定的 android:layout 布局文件中最頂層的View 也就是根view,
// 添加到被移除的 ViewStub的位置
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
到這里關于 ViewStub 的構建、布局的加載以及注意事項分析完了,接下來分析一下 LayoutInflater 是如何被創建的
2 關于LayoutInflater
在 0xA02 Android 10 源碼分析:APK 加載流程之資源加載 文章中,介紹了 Activity 啟動的時候通過調用 LayoutInflater 的 inflater 的方法加載 layout 文件,那么 LayoutInflater 是如何被創建的呢,先來看一段代碼,相信下面的代碼都不會很陌生
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
return new AllVh(LayoutInflater
.from(viewGroup.getContext())
.inflate(R.layout.list_item, viewGroup, false));
}
LayoutInflater的inflate方法的三個參數都代表什么意思?
- resource:要解析的 XML 布局文件 Id
- root:表示根布局
- attachToRoot:是否要添加到父布局 root中
resource 其實很好理解就是資源 Id,而 root 和 attachToRoot 分別代表什么意思:
- 當 attachToRoot == true 且 root != null 時,新解析出來的 View 會被 add 到 root 中去,然后將 root 作為結果返回
- 當 attachToRoot == false 且 root != null 時,新解析的 View 會直接作為結果返回,而且 root 會為新解析的 View 生成 LayoutParams 并設置到該View中去
- 當 attachToRoot == false 且 root == null 時,新解析的 View 會直接作為結果返回
2.1 LayoutInflater 是如何被創建的
LayoutInflater 是一個抽象類,通過調用了 from() 的靜態函數,經由系統服務 LAYOUT_INFLATER_SERVICE,最終創建了一個 LayoutInflater 的子類對象 PhoneLayoutInflater,繼承結構如下:
LayoutInflater.from(ctx) 就是根據傳遞過來的 Context 對象,調用 getSystemService() 來獲取對應的系統服務, 來看一下這個方法
frameworks/base/core/java/android/view/LayoutInflater.java
public static LayoutInflater from(Context context) {
// 獲取系統服務 LAYOUT_INFLATER_SERVICE ,并賦值給 LayoutInflater
// Context 是一個抽象類,真正的實現類是ContextImpl
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
而 Context 本身是一個抽象類,它真正的實例化對象是 ContextImpl
frameworks/base/core/java/android/app/ContextImpl.java
public Object getSystemService(String name) {
// SystemServiceRegistry 是管理系統服務的
// 調用getSystemService方法,通過服務名字查找對應的服務
return SystemServiceRegistry.getSystemService(this, name);
}
2.2 SystemServiceRegistry
SystemServiceRegistry 管理所有的系統服務,調用 getSystemService 方法,通過服務名字查找對應的服務
frameworks/base/core/java/android/app/SystemServiceRegistry.java
public static Object getSystemService(ContextImpl ctx, String name) {
// SYSTEM_SERVICE_FETCHERS 是一個map集合
// 從 map 集合中取出系統服務
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}
- ServiceFetcher 為 SystemServiceRegistry 類的靜態內部接口,定義了 getService 方法
- ServiceFetcher 的實現類 CachedServiceFetcher 實現了 getService方法
- 所有的系統服務都存儲在一個 map 集合 SYSTEM_SERVICE_FETCHERS當 中,調用 get 方法來獲取對應的服務
如果有 getSystemService 方法來獲取服務,那么相應的也會有添加服務的方法
frameworks/base/core/java/android/app/SystemServiceRegistry.java
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);
}
通過調用 SYSTEM_SERVICE_NAMES 的 put 方法,往 map 集合中添加數據,那么 registerService 是什么時候調用的,在 SystemServiceRegistry 類中搜索 registerService 方法,知道了在類加載的時候通過靜態代碼塊中添加的,來看一下
static {
// 初始化加載所有的系統服務
...
// 省略了很多系統服務
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
...
// 省略了很多系統服務
}
最終是創建了一個 PhoneLayoutInflater 并返回的,到這里 LayoutInflater 的創建流程就分析完了
總結
View 中的 INVISIBLE、VISIBLE、GONE 都有什么作用?
如果想隱藏或者顯示 View,可以通過調用 setVisibility(int visibility) 方法來實現,參數 visibility 對應三個值分別是INVISIBLE、VISIBLE、GONE
- VISIBLE:視圖可見
- INVISIBLE:視圖不可見的,它仍然占用布局的空間
- GONE:視圖不可見,它不占用布局的空間
為什么 ViewStub 是大小為0的視圖?
frameworks/base/core/java/android/view/ViewStub.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 設置視圖大小為0
setMeasuredDimension(0, 0);
}
ViewStub 有什么作用?
ViewStub 的作用主要用來延遲布局的加載
ViewStub 是如何創建的?
因為 ViewStub 是繼承 View, 所以 ViewStub 的創建和 View 的創建是相同的,通過調用了 LayoutInflater.createView 方法根據完整的類的路徑名利用反射機制構建 View 對象
frameworks/base/core/java/android/view/LayoutInflater.java
...
try {
// 利用構造函數,創建View
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// 如果是ViewStub,則設置LayoutInflater
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;
} finally {
mConstructorArgs[0] = lastContext;
}
...
為什么 ViewStub 能做到延遲加載?
因為在解析 layout 文件過程中遇到 ViewStub,只是構建 ViewStub 的對象和初始化 ViewStub 的屬性,沒有真正開始解析 View,所以可以做到延遲初始化
ViewStub 指定的 Layout 布局文件是什么時候被加載的?
當ViewStub 控件設置可見,或者調用 inflate() 方法,ViewStub 所指定的 layout 資源就會被加載
LayoutInflater 是一個抽象類它如何被創建的?
LayoutInflater 是一個抽象類,通過調用了 from() 的靜態函數,經由系統服務 LAYOUT_INFLATER_SERVICE,最終創建了一個 LayoutInflater的子類對象 PhoneLayoutInflater,繼承結構如下:
LayoutInflater.from(ctx) 就是根據傳遞過來的 Context 對象,調用 getSystemService() 來獲取對應的系統服務
系統服務存儲在哪里?如何獲取和添加系統服務?
SystemServiceRegistry 管理所有的系統服務,所有的系統服務都存儲在一 個map集合SYSTEM_SERVICE_FETCHERS 當中,調用 getSystemService 方法獲取系統服務,調用 registerService 方法添加系統服務
結語
致力于分享一系列的 Android 系統源碼、逆向分析、算法相關的文章,每篇文章都會反復檢查之后才會發布,如果你同我一樣喜歡研究 Android 源碼,一起來學習,期待與你一起成長
算法
由于 LeetCode 的題庫龐大,每個分類都能篩選出數百道題,由于每個人的精力有限,不可能刷完所有題目,因此我按照經典類型題目去分類、和題目的難易程度去排序
- 數據結構: 數組、棧、隊列、字符串、鏈表、樹……
- 算法: 查找算法、搜索算法、位運算、排序、數學、……
每道題目都會用 Java 和 kotlin 去實現,并且每道題目都有解題思路,如果你同我一樣喜歡算法、LeetCode,可以關注我 GitHub 上的 LeetCode 題解:Leetcode-Solutions-with-Java-And-Kotlin,一起來學習,期待與你一起成長
Android 10 源碼系列
正在寫一系列的 Android 10 源碼分析的文章,了解系統源碼,不僅有助于分析問題,在面試過程中,對我們也是非常有幫助的,如果你同我一樣喜歡研究 Android 源碼,可以關注我 GitHub 上的 Android10-Source-Analysis,文章都會同步到這個倉庫
- 0xA01 Android 10 源碼分析:APK 是如何生成的
- 0xA02 Android 10 源碼分析:APK 的安裝流程
- 0xA03 Android 10 源碼分析:APK 加載流程之資源加載
- 0xA04 Android 10 源碼分析:APK 加載流程之資源加載(二)
- 0xA05 Android 10 源碼分析:Dialog 加載繪制流程以及在 Kotlin、DataBinding 中的使用
- 0xA06 Android 10 源碼分析:WindowManager 視圖綁定以及體系結構
- 0xA07 Android 10 源碼分析:Window 的類型 以及 三維視圖層級分析
- 更多
Android 應用系列
精選譯文
- [譯][Google工程師] 剛剛發布了 Fragment 的新特性 “Fragment 間傳遞數據的新方式” 以及源碼分析
- [譯][Google工程師] 詳解 FragmentFactory 如何優雅使用 Koin 以及部分源碼分析
- [譯][2.4K Start] 放棄 Dagger 擁抱 Koin
- [譯][5k+] Kotlin 的性能優化那些事
- [譯] 解密 RxJava 的異常處理機制
- [譯][1.4K+ Star] Kotlin 新秀 Coil VS Glide and Picasso