提示:本文的源碼均取自Android 7.0(API 24)
前言
自定義View是Android進階路線上必須攻克的難題,而在這之前就應該先對View的工作原理有一個系統的理解。本系列將分爲4篇博客進行講解,本文將主要對MeasureSpec、DecorView、ViewRootImpl等基礎知識進行講解。相關內容如下:
查看源碼的方式
既然要探究View的工作原理,閱讀源碼自然是最好的手段了。其實在AndroidStudio中就可以很方便地查閱到Android系統的相關源碼,但是在實際閱讀過程中會發現很多類和方法都是被隱藏的(@hide註解
),嚴重影響學習熱情。
Github上已經有開發者上傳了去除@hide註解的源碼jar包,我們只需要下載相應版本的Android.jar文件,並且替換本地SDK目錄下相應版本的jar文件,就可以很清爽地閱讀源碼了。需要替換的路徑爲:<SDK location>/platforms/android-xx/Android.jar
Github的地址如下:
該解決方案的原文地址如下:
注意:這種方式建議只在研究時使用,否則在開發過程中可能會在無意中引用@hide標記的API。
MeasureSpec
MeasureSpec可以翻譯爲測量規格,主要用於View的測量過程,封裝着View的大小和測量模式(size和mode)。根據Android官方的註釋,MeasureSpec表達的是父容器對子View的一種佈局要求,可以簡單理解爲對子View大小的限制。不過MeasureSpec並非是由父容器獨立決定的。實際上,父容器會通過自身的MeasureSpec結合子View的LayoutParams
,共同生成子View的MeasureSpec,這一點在後續文章中會詳細講到。
關於LayoutParams的知識可以參考這篇博客:
爲了減少測量過程中創建對象的開銷,在實際使用中MeasureSpec以int(32位數)的形式存在,其高2位用於保存mode,其餘位用於保存size。系統提供的MeasureSpec類是View的靜態內部類,主要提供幾種實用的靜態方法,用於對int形式的MeasureSpec進行打包和解包,關鍵源碼如下:
/**
* A MeasureSpec encapsulates the layout requirements passed from parent to child.
* Each MeasureSpec represents a requirement for either the width or the height.
* A MeasureSpec is comprised of a size and a mode. There are three possible modes.
*/
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// 測量模式:父容器對子View的大小不作任何限制
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// 測量模式:父容器已經爲View指定了一個精確的大小
public static final int EXACTLY = 1 << MODE_SHIFT;
// 測量模式:父容器爲View指定了一個最大的大小
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* 根據傳入的size和mode創建一個MeasureSpec(打包)
*/
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
/**
* 根據傳入的MeasureSpec獲取mode(解包)
*/
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
/**
* 根據傳入的MeasureSpec獲取size(解包)
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
MeasureSpec在View的測量流程中有着很重要的作用,這裏只是簡單介紹一下,在後續的文章中會結合具體源碼進一步講解。
DecorView
回想我們在Activity中經常使用的方法setContentView
,不知道大家有沒有想過我們設置的View被添加到了哪裏。由於在Android中只有ViewGroup的派生類纔可以添加子View,那麼可以自然地想到,整個視圖樹(ViewTree)的根節點必定是一個ViewGroup,而DecorView就是ViewTree最頂級的容器。
實際上,DecorView只不過是FrameLayout的一個子類。PhoneWindow
負責將其實例化,並根據當前Activity的風格特性(theme、feature、flag)爲其添加不同的子佈局。但是無論使用什麼子佈局,子佈局中都會存在一個id爲content
的FrameLayout。我們平時在Activity中使用setContentView
方法設置的View,就會被添加到這個名爲content的FrameLayout中。這一過程發生在PhoneWindow#generateLayout
中,關鍵代碼如下:
protected ViewGroup generateLayout(DecorView decor) {
..........
// Inflate the window decor.
// ① 根據Feature使用不同的佈局資源文件
int layoutResource;
int features = getLocalFeatures();
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
} 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;
}
removeFeature(FEATURE_ACTION_BAR);
} 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;
}
removeFeature(FEATURE_ACTION_BAR);
} 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.startChanging();
// ② 在這個方法中解析佈局資源文件(mDecor即DecorView)
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
// ③ 這裏的contentParent就是指id爲content的那個FrameLayout
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
..........
mDecor.finishChanging();
return contentParent;
}
可以看到,在代碼①的位置根據不同的Feature爲layoutResource賦值,其實就是在選擇DecorView需要使用的子佈局資源文件。在代碼②的位置調用了mDecor的onResourcesLoaded
方法,這裏的mDecor指的就是DecorView。在這裏方法中會根據①中獲取的佈局資源進行解析,並將解析獲得的View添加到DecorView中。
在代碼③的位置根據ID_ANDROID_CONTENT
獲取了名爲contentParent
的ViewGroup,這其實就是setContentView
方法設置的View的父容器。在Activity中我們也可以通過android.R.id.content
獲得這個ViewGroup。
上面說了這麼多,其實就是想說明一件事,那就是DecorView是整個視圖樹的根容器。後續文章要講到測量、佈局、繪製流程,就是從DecorView開始向下傳遞的。
ViewRootImpl
ViewRootImpl開啓測量、佈局和繪製流程
ViewRootImpl實現了ViewParent
接口(定義了View的父容器應該承擔的職責),處於視圖層級的頂點,實現了View和WindowManager之間必需的協議。拋開這些複雜定義,我們要知道的是ViewRootImpl將負責開啓整個視圖樹的測量-佈局-繪製流程。這一過程體現在ViewRootImpl的performTraversals
方法中,這個方法將依次調用performMeasure
、performLayout
和performDraw
方法完成上述流程,關鍵代碼如下:
private void performTraversals() {
// mView是與ViewRootImpl綁定的DecorView
final View host = mView;
if (host == null || !mAdded)
return;
mIsInTraversal = true;
boolean newSurface = false;
WindowManager.LayoutParams lp = mWindowAttributes;
// 這裏是屏幕的寬度和高度
int desiredWindowWidth;
int desiredWindowHeight;
........
if (mFirst || windowShouldResize || insetsChanged ||
viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
........
if (!mStopped || mReportNextDraw) {
boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
(relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged ||updatedConfiguration) {
// ① 獲取測量需要的MeasureSpec
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// ② 開啓測量流程
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// ③ 測量流程結束後就可以獲得DecorView的寬高了
int width = host.getMeasuredWidth();
int height = host.getMeasuredHeight();
boolean measureAgain = false;
if (lp.horizontalWeight > 0.0f) {
width += (int) ((mWidth - width) * lp.horizontalWeight);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (lp.verticalWeight > 0.0f) {
height += (int) ((mHeight - height) * lp.verticalWeight);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (measureAgain) {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
layoutRequested = true;
}
}
} else {
maybeHandleWindowMove(frame);
}
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
if (didLayout) {
// ④ 開啓佈局流程
performLayout(lp, mWidth, mHeight);
// 計算透明區域
........
}
........
// Remember if we must report the next draw.
if ((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
reportNextDraw();
}
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw && !newSurface) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
// ⑤ 開啓繪製流程
performDraw();
} else {
........
}
mIsInTraversal = false;
}
可以看到,首先在代碼①的位置通過getRootMeasureSpec
方法獲得了測量DecorView需要的MeasureSpec,這也是整個視圖樹(ViewTree)最初的MeasureSpec,關鍵代碼如下:
/**
* Figures out the measure spec for the root view in a window based on it's
* layout params.
*
* @param windowSize The available width or height of the window
* @param rootDimension The layout params for one dimension (width or height) of the window.
*
* @return The measure spec to use to measure the root view.
*/
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
在這個方法中將根據DecorView的佈局參數生成不同的MeasureSpec,windowSize即屏幕的寬/高,dimension則是DecorView中LayoutParams的width/height。可以看到在佈局參數爲MATCH_PARENT或設置了具體寬/高(比如20dp這種形式)的情況下,生成的MeasureSpec都是使用EXACTLY模式(精確模式),否則使用AT_MOST
模式。
隨後,在代碼②的位置開啓了測量流程,代碼④的位置開啓了佈局流程,代碼⑤的位置開啓了繪製流程。
在performMeasure中將調用DecorView的measure方法進行測量:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
try {
// 開始測量(mView就是DecorView)
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
......
}
}
在performLayout中將調用DecorView的layout方法進行佈局,並傳入測量完成後獲得的寬高:
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mInLayout = true;
final View host = mView;
if (host == null) {
return;
}
try {
// 開始佈局(host就是DecorView)
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
mInLayout = false;
} finally {
......
}
mInLayout = false;
}
在performDraw方法中將調用ViewRootImpl的draw方法,之後又會調用drawSoftware
方法,最終將調用DecorView的draw方法開啓繪製流程:
/**
* ViewRootImpl#performDraw
*/
private void performDraw() {
if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
return;
} else if (mView == null) {
return;
}
final boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
mIsDrawing = true;
try {
// 開始繪製
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
}
......
}
/**
* ViewRootImpl#draw
*/
private void draw(boolean fullRedrawNeeded) {
......
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
/**
* ViewRootImpl#drawSoftware
*/
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
// Draw with software renderer.
final Canvas canvas;
......
try {
canvas.translate(-xoff, -yoff);
if (mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
attachInfo.mSetIgnoreDirtyState = false;
// 調用DecorView的draw方法開始繪製
mView.draw(canvas);
drawAccessibilityFocusedDrawableIfNeeded(canvas);
} finally {
......
}
return true;
}
ViewRootImpl的生成過程
上面講了ViewRootImpl的作用,這裏再提一下ViewRootImpl的生成過程:
首先是ActivityThread
類的handleResumeActivity
方法,在這裏會調用WindowManager
的addView方法添加DecorView,關鍵代碼如下:
ActivityThread#handleResumeActivity
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
ActivityClientRecord r = mActivities.get(token);
r = performResumeActivity(token, clearHide, reason);
.......
if (r != null) {
final Activity a = r.activity;
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
// 調用WindowManager的addView方法添加DecorView
wm.addView(decor, l);
} else {
.......
}
}
}
}
.......
}
上述代碼中的wm是一個WindowManagerImpl
對象(實現WindowManager),它的addView方法如下:
WindowManagerImpl#addView
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
mGlobal是一個WindowManagerGlobal
對象,這裏調用了它的addView方法,關鍵代碼如下:
WindowManagerGlobal#addView
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
.......
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
.......
// ① 實例化一個ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
// ② 建立ViewRootImpl和DecorView的聯繫(view就是DecorView)
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
.......
}
}
}
在代碼①的位置實例化了一個ViewRootImpl對象,然後在代碼②的位置通過setView
方法建立了ViewRootImpl和DecorView的聯繫,這裏的view其實就是DecorView。
小結
本文簡單講解了在學習View的工作原理中需要知道的基礎知識。如果看完後覺得不是很懂也沒有關係,現在先對它們有個大致印象,然後結合後續的文章一起理解,效果可能會更好。
參考資料
https://blog.csdn.net/a553181867/article/details/51477040
https://blog.csdn.net/singwhatiwanna/article/details/50775201