Android系統原始碼分析–View繪製流程之-setContentView

墨香發表於2018-11-18

上一篇分析了四大元件之ContentProvider,這也是四大元件最後一個。因此,從這篇開始我們分析新的篇章–View繪製流程,View繪製流程在Android開發中佔有非常重要的位置,只要有檢視的顯示,都離不開View的繪製,所以瞭解View繪製原理對於應用開發以及系統的學習至關重要。由於View繪製流程比較複雜,並且涉及的知識非常多,所以後面我會按照下面幾方面來介紹View的繪製流程。每篇不是很長,但是儘量的詳細,讓每個人都看懂。

  • Android系統原始碼分析–View繪製流程之-setContentView
  • Android系統原始碼分析–View繪製流程之-inflate
  • Android系統原始碼分析–View繪製流程之-onMeasure
  • Android系統原始碼分析–View繪製流程之-onLayout
  • Android系統原始碼分析–View繪製流程之-onDraw
  • Android系統原始碼分析–View繪製流程之-硬體加速
  • Android系統原始碼分析–View繪製流程之-addView
  • Android系統原始碼分析–View繪製流程之-彈性效果

所以這篇我們先分析View繪製流程的setContentView方法,按照慣例,先貼一下流程圖:

Android系統原始碼分析–View繪製流程之-setContentView

1.PhoneWindow.setContentView

呼叫setContentView最開始的地方是在我們繼承Activity的子類中的onCreate方法中,這個方法其實是呼叫的Activity中的setContentView方法:

    public void setContentView(@LayoutRes int layoutResID) { 
// getWindow獲取的是PhoneWindow,所以這裡是呼叫的PhoneWindow的setContentView方法 getWindow().setContentView(layoutResID);
initWindowDecorActionBar();

}複製程式碼

其實這個getWindow獲取的是繼承Window的PhoneWindow,所以這裡getWindow.setContentView是呼叫的PhoneWindow.setContentView方法,具體的自己可以看看程式碼哪裡賦值的就知道了。另外這個方法還有兩個類似的方法:

    public void setContentView(View view) { 
getWindow().setContentView(view);
initWindowDecorActionBar();

} public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
initWindowDecorActionBar();

}複製程式碼

這三個方法差不多,只不過下面的兩個直接傳遞了view物件,而第一個是傳遞了view的id。我們接著看PhoneWindow.setContentView方法。

    public void setContentView(int layoutResID) { 
// 根據layout的id載入一個佈局,然後通過findViewById(R.id.content)載入出佈局中id為content // 的FrameLayout賦值給mContentParent,並且將該view新增到mDecor(DecorView)中 if (mContentParent == null) {// 第一次是空 installDecor();

} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 沒有過度效果,並且不是第一次setContentView,那麼要先移除盛放setContentView傳遞進來 // 的View的父容器中的所有子view mContentParent.removeAllViews();

} // 視窗是否需要過度顯示 if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
...
} else {// 不需要過度,載入id為layoutResID的檢視並且新增到mContentParent中 mLayoutInflater.inflate(layoutResID, mContentParent);

} // 繪製檢視 mContentParent.requestApplyInsets();
... mContentParentExplicitlySet = true;

}複製程式碼

上面註釋很詳細,但是還是需要解釋一下mContentParent,這個mContentParent是一個FrameLayout,這裡的Content是指你setContentView傳遞進來的id指向的檢視,所以mContentParent也就是指放置傳遞進來的檢視的父檢視。看下面的圖:

Android系統原始碼分析–View繪製流程之-setContentView

上面的ActionBarContextView是標題,不過有些設定是不會顯示整個標題的,所以這裡只是一種情況,下面的id為content的FrameLayout就是這個mContentParent,你通過setContentView方法傳遞的檢視會放到這個id為content的FrameLayout上面,這樣你的Activity就顯示了你寫的佈局檢視了,這裡先解釋一下,我們下面看看是不是真的這樣。由於第一次建立Activity時mContentParent是空的,所以會走PhoneWindow.installDecor方法。

2.PhoneWindow.installDecor

    private void installDecor() { 
mForceDecorInstall = false;
// 繼承FrameLayout,是視窗頂級檢視,也就是Activity顯示View的根View,包含一個TitleView和一個ContentView if (mDecor == null) {// 首次為空 // 建立DecorView(FrameLayout) mDecor = generateDecor(-1);
...
} else {
mDecor.setWindow(this);

} if (mContentParent == null) {// 第一次setContentView時為空 // 這個mContentParent就是後面從系統的frameworks\base\core\res\res\layout\目錄下載入出來 // 的layout佈局(這個Layout佈局載入完成後會新增到mDecor(DecorView)中)中的一個id為content的 // FrameLayout控制元件,這個FrameLayout控制元件用來盛放setContentView傳遞進來的View mContentParent = generateLayout(mDecor);
... // 判斷是否存在id為decor_content_parent的view(我只看到screen_action_bar.xml這個裡面有這個id) final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById( R.id.decor_content_parent);
if (decorContentParent != null) {
... if (mDecorContentParent.getTitle() == null) {
// 設定標題 mDecorContentParent.setWindowTitle(mTitle);

} ...
} else {
// 標題檢視 mTitleView = (TextView) findViewById(R.id.title);
// 有的佈局中是沒有id為title的控制元件的,也就是不顯示標題 if (mTitleView != null) {
// 判斷是否有不顯示標題的特性 if ((getLocalFeatures() &
(1 <
<
FEATURE_NO_TITLE)) != 0) {
final View titleContainer = findViewById(R.id.title_container);
if (titleContainer != null) {
titleContainer.setVisibility(View.GONE);

} else {
mTitleView.setVisibility(View.GONE);

} mContentParent.setForeground(null);

} else {// 顯示標題 mTitleView.setText(mTitle);

}
}
} // 背景 if (mDecor.getBackground() == null &
&
mBackgroundFallbackResource != 0) {
mDecor.setBackgroundFallback(mBackgroundFallbackResource);

} // 過度效果 ...
}
}複製程式碼

這裡出現了一個mDecor,這個mDecor是DecorView,繼承FrameLayout,是視窗頂級檢視,也就是Activity顯示View的根View,包含一個TitleView和一個ContentView,也就是上面圖形中的最外層藍色的邊框所指代的檢視,當然,這裡第一載入時也是空的,那麼會呼叫generateDecor函式來建立mDecor,然後通過generateLayout方法建立mContentParent檢視,建立完成後會設定標題,設定標題的就不分析了,比較簡單,下面先看建立mDecor的方法。

3.PhoneWindow.generateDecor

    protected DecorView generateDecor(int featureId) { 
... // activity. Context context;
if (mUseDecorContext) {// 從Activity的setContentView方法呼叫則為true Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {// 系統程式時沒有Application的context,所以就用現有的context context = getContext();

} else {// 應用會有application的Context ...
}
} else {
context = getContext();

} return new DecorView(context, featureId, this, getAttributes());

}複製程式碼

這裡判斷了一個applicationContext是否存在,主要是區分這個是系統呼叫還是應用,系統是沒有applicationContext的,最後通過new關鍵字建立物件DecorView,這裡就獲取到了DecorView。

5.PhoneWindow.generateLayout

    protected ViewGroup generateLayout(DecorView decor) { 
... // 根據Window的屬性呼叫相應的requestFeature ... // 獲取Window的各種屬性來設定flag和引數 ... // 根據之前的flag和feature來載入一個layout資源到DecorView中,並把可以作為容器的View返回 // 這個layout佈局檔案在frameworks\base\core\res\res\layout\目錄下 int layoutResource;
int features = getLocalFeatures();
if ((features &
(1 <
<
FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;

} else if ((features &
((1 <
<
FEATURE_LEFT_ICON) | (1 <
<
FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
...
} else {
layoutResource = R.layout.screen_title_icons;

} removeFeature(FEATURE_ACTION_BAR);
// System.out.println("Title Icons!");

} else if ((features &
((1 <
<
FEATURE_PROGRESS) | (1 <
<
FEATURE_INDETERMINATE_PROGRESS))) != 0 &
&
(features &
(1 <
<
FEATURE_ACTION_BAR)) == 0) {
layoutResource = R.layout.screen_progress;

} else if ((features &
(1 <
<
FEATURE_CUSTOM_TITLE)) != 0) {
if (mIsFloating) {
...
} else {
layoutResource = R.layout.screen_custom_title;

} ...
} else if ((features &
(1 <
<
FEATURE_NO_TITLE)) == 0) {
if (mIsFloating) {
...
} else if ((features &
(1 <
<
FEATURE_ACTION_BAR)) != 0) {
layoutResource = a.getResourceId( R.styleable.Window_windowActionBarFullscreenDecorLayout, R.layout.screen_action_bar);

} else {
layoutResource = R.layout.screen_title;

} // System.out.println("Title!");

} else if ((features &
(1 <
<
FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;

} else {
layoutResource = R.layout.screen_simple;

} mDecor.startChanging();
// 根據layoutResource(佈局id)載入系統中佈局檔案(Layout)並新增到DecorView中 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
// contentParent是用來新增Activity中佈局的父佈局(FrameLayout),並帶有相關主題樣式,就是上面 // 提到的id為content的FrameLayout,返回後會賦值給PhoneWindow中的mContentParent ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");

} ... // 設定mDecor背景之類 ... mDecor.finishChanging();
return contentParent;

}複製程式碼

前面一大段if-else語句是根據屬性值獲取系統中的layout的id,主要有下面幾種:

  R.layout.screen_swipe_dismiss  R.layout.screen_title_icons  R.layout.screen_progress  R.layout.screen_custom_title  R.layout.screen_title  R.layout.screen_simple_overlay_action_mode  R.layout.screen_simple複製程式碼

這些佈局檔案都在系統frameworks\base\core\res\res\layout\目錄下,我們看其中一個screen_simple.xml佈局程式碼:

<
LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:orientation="vertical">
<
ViewStub android:id="@+id/action_mode_bar_stub" android:inflatedId="@+id/action_mode_bar" android:layout="@layout/action_mode_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="?attr/actionBarTheme" />
<
FrameLayout android:id="@android:id/content" android:layout_width="match_parent" android:layout_height="match_parent" android:foregroundInsidePadding="false" android:foregroundGravity="fill_horizontal|top" android:foreground="?android:attr/windowContentOverlay" />
<
/LinearLayout>
複製程式碼

其中我們需要獲取的contentParent就是xml佈局中id為content的FrameLayout,為什麼是這個,我們通過上面程式碼分析,上面我們看到了mDecor.onResourcesLoaded方法,這裡的第二個引數layoutResource就是上面的xml佈局,所以這裡就是載入這個佈局的我們看看是不是

6.DecorView.onResourcesLoaded

    void onResourcesLoaded(LayoutInflater inflater, int layoutResource) { 
... // DecorView中的標題檢視,可能是空,也就是沒有標題 mDecorCaptionView = createDecorCaptionView(inflater);
// 載入Layout作為根佈局(frameworks\base\core\res\res\layout\目錄下layout佈局檔案) // 這裡獲取到的root是沒有寬高的 final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {// 有標題 // 這裡可以看到mDecorCaptionView不為空時,將mDecorCaptionView新增到DecorView,然後再將 // Layout新增到mDecorCaptionView if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));

} mDecorCaptionView.addView(root, new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));

} else {// 沒有標題 // 如果mDecorCaptionView為空,則直接將跟佈局Layout新增到DecorView // Put it below the color views. addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));

} mContentRoot = (ViewGroup) root;
initializeElevation();

}複製程式碼

這裡首先建立了標題檢視,然後通過LayoutInflater.inflate載入了id為layoutResource的佈局檔案並賦值給root引用,最終返回的也是這個root,所以上面方法5中載入了這個佈局檔案,載入完成後,如果標題檢視檔案存在,則將root新增到標題檢視中,再將標題檢視新增到DecorView上,如果沒有標題檢視,則直接將root佈局新增到DecorView上面,寬高是MATCH_PARENT。再回到5中,在呼叫完mDecor.onResourcesLoaded方法後通過id為ID_ANDROID_CONTENT獲取了一個ViewGroup,那麼這個ID_ANDROID_CONTENT是什麼,通過查詢我們發現是:

public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
複製程式碼

這裡就可以知道獲取的contentParent就是上面xml佈局中的id為content的FrameLayout佈局,所以到這裡整體結構基本明白了。

另外上面呼叫了一個createDecorCaptionView方法並且傳入了LayoutInflater,那麼看看這個方法做了哪些操作。

7.DecorView.createDecorCaptionView

    private DecorCaptionView createDecorCaptionView(LayoutInflater inflater) { 
... if (!mWindow.isFloating() &
&
isApplication &
&
StackId.hasWindowDecor(mStackId)) {
if (decorCaptionView == null) {
decorCaptionView = inflateDecorCaptionView(inflater);

} decorCaptionView.setPhoneWindow(mWindow, true /*showDecor*/);

} else {
decorCaptionView = null;

} ... return decorCaptionView;

}複製程式碼

這裡其實就一個方法需要再看看那就是inflateDecorCaptionView方法。

8.DecorView.inflateDecorCaptionView

    private DecorCaptionView inflateDecorCaptionView(LayoutInflater inflater) { 
final Context context = getContext();
inflater = inflater.from(context);
// 從frameworks\base\core\res\res\layout\中載入decor_caption.xml佈局 final DecorCaptionView view = (DecorCaptionView) inflater.inflate(R.layout.decor_caption, null);
... return view;

}複製程式碼

這裡其實就是通過LayoutInflater.inflate方法載入frameworks\base\core\res\res\layout\下的decor_caption.xml佈局,這個LayoutInflater.inflate由於比較重要,所以我們放到下一章單獨講解。

10.LayoutInflater.inflate

我們在第二步初始化完DecorView和mContentParent檢視後開始呼叫mLayoutInflater.inflate(layoutResID, mContentParent)方法,載入我們setContentView方法傳遞進來的檢視,也就是我們自己寫的Activity佈局,之前的都是系統的佈局。我們知道mContentParent是放置我們自己寫的Activity檢視的容器,所以後面就簡單了。

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) { 
return inflate(resource, root, root != null);

}複製程式碼

上面我們說了這個方法具體分析我們下一章單獨分析。所以我們接著前面分析。

11.View.requestApplyInsets

    public void requestApplyInsets() { 
requestFitSystemWindows();

}複製程式碼

12.View.requestFitSystemWindows

    public void requestFitSystemWindows() { 
if (mParent != null) {
mParent.requestFitSystemWindows();

}
}複製程式碼

這裡的mParent是ViewParent的具體實現ViewRootImpl,所以呼叫的是ViewRootImpl裡的requestFitSystemWindows方法。

13.ViewRootImpl.requestFitSystemWindows

    public void requestFitSystemWindows() { 
checkThread();
mApplyInsetsRequested = true;
scheduleTraversals();
// 繪製檢視
}複製程式碼

checkThread這個是檢測執行緒的方法,也就是檢測當前執行緒是不是主執行緒,也就是setContentView方法要在UI執行緒呼叫。然後呼叫scheduleTraversals方法開始繪製檢視。

15.ViewRootImpl.scheduleTraversals

    void scheduleTraversals() { 
// 當mTraversalScheduled為false,也就是沒有重繪請求或者沒有未執行完的重繪時才開始重繪 if (!mTraversalScheduled) {
// 一旦開始重回此處設定為True,當執行完畢後呼叫unscheduleTraversals函式, // 重新設定為false,避免同時存在多次繪製 mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 將訊息放入訊息處理器中,最終呼叫doTraversal方法 mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
}複製程式碼

mTraversalScheduled只有呼叫這個方法後才設定為true,所以在開始呼叫這個方法的時候是false,後面會將mTraversalRunnable放到訊息處理器中,這個mTraversalRunnable是一個實現了Runnable介面的物件,所以從這裡呼叫了TraversalRunnable中的run方法。

16.TraversalRunnable.run

        public void run() { 
doTraversal();

}複製程式碼

這裡很簡單就是呼叫了doTraversal方法。

17.ViewRootImpl.doTraversal

    void doTraversal() { 
if (mTraversalScheduled) {
mTraversalScheduled = false;
... // 執行View繪製流程 performTraversals();
...
}
}複製程式碼

這裡主要是呼叫performTraversals方法,開始View的真正繪製。

18.ViewRootImpl.performTraversals

    private void performTraversals() { 
... // 這裡是需要測量的條件:第一次載入View,需要調整視窗大小,需要適應系統視窗,檢視顯示狀態改變, // 檢視佈局引數不為空,強制視窗重新佈局。首先要滿足這個幾個條件才可能執行測量 if (mFirst || windowShouldResize || insetsChanged || viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
... // 視窗沒有停止,或者通知需要繪製 if (!mStopped || mReportNextDraw) {
... if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight() || contentInsetsChanged || updatedConfiguration) {
... // 1.第一步:測量 // Ask host how big it wants to be performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
}
} else {
...
} ... if (didLayout) {// 執行佈局 // 2.第二步:佈局 performLayout(lp, mWidth, mHeight);
...
} ... // 如果沒有取消繪製,並且不是新的Surface,那麼執行繪製 if (!cancelDraw &
&
!newSurface) {
... // 3.第三步:繪製 performDraw();

} else {// 如果取消了繪製或者是新的Surface,那麼要重新測量、佈局和繪製 ...
} mIsInTraversal = false;

}複製程式碼

這裡開始進入測量,佈局,繪製的過程,裡面通過各個條件來判斷需要執行哪一步或者哪幾部,因為這一段主要是設計測量、佈局、繪製,所以這章就不分析了,這個方法放到《Android系統原始碼分析–View繪製流程之-onMeasure》一章講解。

我們下一章開始分析《Android系統原始碼分析–View繪製流程之-inflate》。

參考文章:

程式碼地址:

直接拉取匯入開發工具(Intellij idea或者Android studio)

由於coding與騰訊雲合作,改變很多,所以後續程式碼切換到Gitlab。

gitlab.com/yuchuangu85…

注:

首發地址:www.codemx.cn

Android開發群:192508518

微信公眾賬號:Code-MX

Android系統原始碼分析–View繪製流程之-setContentView

注:本文原創,轉載請註明出處,多謝。

來源:https://juejin.im/post/5bf16ff5f265da6141712acc

相關文章