Android 從原始碼看懂視窗繪製機制
前言
Android開發最息息相關的就是介面,平時開發總會使用各種佈局和檢視來組合成我們的螢幕效果,在Android的知識體系中,View扮演著很重要的角色,簡單來理解,View是Android在視覺上的呈現。那Android是如何將這些View一步步繪製到螢幕上的呢,這就涉及到本文所要講的視窗繪製工作原理。
先上一張結構圖,帶著這張結構圖去分析原始碼:

Activity結構圖
原始碼分析
我們最經常接觸的往往是View這一層,在它之上有Activity,Activity一般都要在onCreate中呼叫setContentView,那就從setContentView進去看究竟做了啥:
public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }
可以看到實際上是呼叫了getWindow得到的物件再去setContentView,所以其實在View和Activity中間還隔著一層Window,它是Activity與View之間的橋樑,是一個抽象類,PhoneWindow是它的唯一實現子類,我們在Activity中所使用的View其實都是通過PhoneWindow來呈現的,它就類似於View的一個載體,就像PS中的畫板,而View就是具體要繪製的內容。
那麼PhoneWindow裡面又是什麼構成呢?繼續順著剛才setContentView看進去:
@Override public void setContentView(int layoutResID) { // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window // decor, when theme attributes and the like are crystalized. Do not check the feature // before this happens. if (mContentParent == null) { installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { //忽略程式碼 } else { mLayoutInflater.inflate(layoutResID, mContentParent); } //忽略程式碼 }
可以看到先是installDecor(),installDecor原始碼如下:
private void installDecor() { if (mDecor == null) { //new一個DecorView mDecor = generateDecor(); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } if (mContentParent == null) { mContentParent = generateLayout(mDecor); } //忽略程式碼 }
可以看到這裡首先建立了一個DecorView,然後呼叫generateLayout,詳細看下generateLayout:
protected ViewGroup generateLayout(DecorView decor) { //忽略程式碼 int layoutResource; ... 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) { TypedValue res = new TypedValue(); getContext().getTheme().resolveAttribute( R.attr.dialogTitleIconsDecorLayout, res, true); layoutResource = res.resourceId; } else { layoutResource = R.layout.screen_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) { TypedValue res = new TypedValue(); getContext().getTheme().resolveAttribute( R.attr.dialogCustomTitleDecorLayout, res, true); layoutResource = res.resourceId; } else { layoutResource = R.layout.screen_custom_title; } } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) { if (mIsFloating) { TypedValue res = new TypedValue(); getContext().getTheme().resolveAttribute( R.attr.dialogTitleDecorLayout, res, true); layoutResource = res.resourceId; } 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; } } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) { layoutResource = R.layout.screen_simple_overlay_action_mode; } else { layoutResource = R.layout.screen_simple; } mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); if (contentParent == null) { throw new RuntimeException("Window couldn't find content container view"); } //忽略程式碼 return contentParent; }
可以看到先是根據feature型別來為layoutResource賦值,隨便選擇一個layout檔案開啟如下:
<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>
這個佈局檔案裡最外層是一個LinearLayout, 裡面還包含著一個id為R.id.content的FrameLayout ,我們繼續回到程式碼中,剛才賦值了layoutResource後,接著呼叫了DecorView的 onResourcesLoaded
,先看下里面做了啥:
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) { //...忽略程式碼 final View root = inflater.inflate(layoutResource, null); if (mDecorCaptionView != null) { if (mDecorCaptionView.getParent() == null) { addView(mDecorCaptionView, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); } mDecorCaptionView.addView(root, new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT)); } else { // Put it below the color views. addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); } mContentRoot = (ViewGroup) root; //...忽略程式碼 } @Override public void onDraw(Canvas c) { super.onDraw(c); // When we are resizing, we need the fallback background to cover the area where we have our // system bar background views as the navigation bar will be hidden during resizing. mBackgroundFallback.draw(isResizing() ? this : mContentRoot, mContentRoot, c, mWindow.mContentParent); }
可以看到這裡將我們傳進來的layoutResource資源inflate成了View,然後將其 addView
給 DecorView
自身並且在onDraw中繪製了出來,換句話說就是把剛才那個layout檔案新增到DecorView中並繪製。
那麼剛才那個layout檔案用來做什麼的呢?回到上一步,onResroucesLoaded之後,通過findViewById查詢id為 R.id.content
的View (看到沒,這裡就是我們剛才那個layout檔案中的R.id.content,所以實際上就是先將layout檔案add到DecorView中去,然後再將其content給find出來) ,然後將其返回,賦值給剛才分析過的installDecor方法中的 mContentParent
,然後現在回頭看回我們一開始的setContentView:
@Override public void setContentView(int layoutResID) { // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window // decor, when theme attributes and the like are crystalized. Do not check the feature // before this happens. if (mContentParent == null) { installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { //忽略程式碼 } else { mLayoutInflater.inflate(layoutResID, mContentParent); } //忽略程式碼 }
我們發現了mContentParent的影子!, mLayoutInflater.inflate(layoutResID, mContentParent);
,它將layoutResID的資源通過inflate新增到了我們的mContentParent(inflate的引數解讀見我另一篇文章: https://www.jianshu.com/p/3f871d95489c )
所以其實我們平時setContetntView傳進去的R.layout.xxx最終都會新增到R.id.content這個View上面,這也就是為什麼它命名為setContentView的原因了,而R.id.content又是被包裹在一層LinearLayout裡面,然後這個LinearLayout檔案又會被通過addView新增到DecorView這個FrameLayout中,原來DecorView就是承載了我們佈局檔案的一個頂級大佬。
總結
現在再回過頭看文章開頭那個結構圖,是否清晰了很多呢~這裡只是從setContentView分析了從Activity到Window再到DecorView的層級和繪製流程,接下來還有關於View的繪製再慢慢總結,不得不說原始碼裡還是有很多值得借鑑的地方,多看原始碼對自己的理解會更有幫助,不能知其然不知其所以然。
GitHub: GitHub-ZJYWidget
CSDN部落格: IT_ZJYANG
簡 書: Android小Y
在Github上建了一個集合炫酷自定義View的專案,裡面有很多實用的自定義View原始碼及demo,會長期維護,歡迎Star~ 如有不足之處或建議還望指正,相互學習,相互進步,如果覺得不錯動動小手給個Star, 謝謝~