在我的系列文章上一篇:App 竟然是這樣跑起來的 —— Android App/Activity 啟動流程分析中已經分析了一個 App 從點選它的圖示到 Activity 的 onCreate()、onStart() 和 onResume() 等生命週期被呼叫的整個流程。我們都知道,普通 App 螢幕上顯示的內容都是由一個個自己設計的介面被系統載入而來的,而這些介面中的元素又是怎麼被渲染出來的呢?本文將繼續基於 Android Nougat 從原始碼的角度來進一步分析整個過程。
在開始之前,回顧一下上一篇文章中分析的從 ActivityThread 到 Activity 過程的時序圖:
步驟一:初始化 PhoneWindow 和 WindowManager
如上圖所示,在 Activity 的 onCreate()、onStart() 和 onResume() 等生命週期被呼叫之前,它的 attach() 方法將會先被呼叫,因此,我們將 attach() 方法作為這篇文章主線的開頭:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window) {
attachBaseContext(context);
...
// mWindow 是一個 PhoneWindow 物件例項
mWindow = new PhoneWindow(this, window);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
...
// 呼叫 Window 的 setWindowManager 方法
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if (mParent != null) {
mWindow.setContainer(mParent.getWindow());
}
// 從 Window 中獲取 WindowManager
mWindowManager = mWindow.getWindowManager();
mCurrentConfig = config;
}
複製程式碼
mWindow 是一個 Window 型別的變數,在 attach() 方法中,建立了一個 PhoneWindow 物件例項並賦值給了 mWindow,PhoneWindow 直接繼承自 Window 類。然後呼叫了 Window 的 setWindowManager() 方法:
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
...
// mWindowManager 就是 WindowManagerImpl 物件的例項
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
複製程式碼
因此,Acitvity 中的 mWindow 變數就是 PhoneWindow 類的例項,而 mWindowManager 是 WindowManagerImpl 類的例項,attach() 方法的主要工作就是初始化這兩個變數。
步驟二:初始化 DecorView
跟蹤程式碼執行過程
接下來到了 onCreate 方法,我們都知道,如果想要讓自己設計的 layout 佈局檔案或者 View 顯示在 Activity 中,必須要在 Activity 的 onCreate() 方法中應該呼叫 setContentView() 方法將我們的佈局 id 或者 View 傳遞過去,檢視其中一個 setContentView() 方法:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
複製程式碼
繼續檢視 PhoneWindow 類的 setContentView() 方法:
public void setContentView(int layoutResID) {
if (mContentParent == null) {// 是否首次呼叫
// 初始化 Decor
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {// 轉場動畫,預設 false
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
...
} else {
// 解析佈局檔案
mLayoutInflater.inflate(layoutResID, mContentParent);
}
...
}
複製程式碼
如果是首次呼叫這個方法,則 mContentParent 為 null,否則如果沒有轉場動畫的話就移除 mContentParent 的全部子 View,繼續跟蹤 installDecor() 方法:
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
// 生成 DecorView 物件
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
// 呼叫 generateLayout 方法
mContentParent = generateLayout(mDecor);
...
}
}
}
複製程式碼
當 mDecor 為 null 的時候會呼叫 generateDecor() 方法建立一個 DecorView 類的例項,DecorView 繼承自 FrameLayout。接下來判斷 mContentParent 是否為 null(前面已經提到過,首次載入的時候就是 null),如果是則呼叫 generateLayout() 方法,這個方法就會建立 mContentParent 物件,跟蹤進去:
protected ViewGroup generateLayout(DecorView decor) {
TypedArray a = getWindowStyle();
...
// 通過 WindowStyle 中設定的各種屬性對 Window 進行各種初始化操作
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
& (~getForcedWindowFlags());
if (mIsFloating) {
setLayout(WRAP_CONTENT, WRAP_CONTENT);
setFlags(0, flagsToUpdate);
} else {
setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
}
....
int layoutResource;
int features = getLocalFeatures();
// 根據設定好的 features 值獲取相應的佈局檔案並賦值給 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;
}
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();
// 呼叫 onResourcesLoaded 方法
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
// 在 layoutResource 中根據 id:com.android.internal.R.id.content 獲取一個 ViewGroup 並賦值給 contentParent 物件
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
...
mDecor.finishChanging();
// 返回 contentParent
return contentParent;
}
複製程式碼
DecorView 的 onResourcesLoaded() 方法:
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
...
mDecorCaptionView = createDecorCaptionView(inflater);
// 解析 layoutResource 檔案
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
...
} else {
// 作為根佈局新增到 mDecor 中
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}
複製程式碼
可以看到,這個方法主要分為四個部分:
- 首先會通過 com.android.internal.R.styleable.Window 中設定的各種屬性對 Window 進行各 requestFeature 或者 setFlags 等操作。
- 接下來是根據設定好的 features 值選擇不同的視窗修飾佈局檔案,得到 layoutResource 值。因此在自己的 Activity 中設定全屏的 requestFeature() 等方法也需要在 setContentView 之前呼叫,才能夠根據你的設定來選擇不同的根佈局。
- 將 layoutResource 值傳給 DecorView 的 onResourcesLoaded() 方法,通過 LayoutInflater 把佈局轉化成 View 作為根檢視並將其新增到 mDecor。
- 在 mDecor 中查詢 id 為 com.android.internal.R.id.content 的 ViewGroup 並作為返回值返回,這個 ViewGroup 一般為 FrameLayout。
關於第四點,可能有人會有疑問,為什麼根據 id 為 com.android.internal.R.id.content 就一定能找到對應的 ViewGroup?答案就在前面我們分析過的 generateLayout() 方法中,這裡會根據設定好的 features 值獲取相應的佈局檔案並賦值給 layoutResource,而這所有的佈局檔案中都包括了一個 id 為 content 的 FrameLayout,除此之外有些佈局檔案中還可能有 ActiionBar 和 Title 等,這些佈局檔案存放於該目錄下。以 R.layout.screen_simple 為例,它的內容如下:
<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>
複製程式碼
回到 PhoneWindow 的 setContentView() 方法,執行完 installDecor() 之後,mDecor 被初始化了,同時 mContentParent 也被賦了值, 回到 setContentView() 方法,最後一個重要步驟就是通過 mLayoutInflater.inflater 將我們的 layout 佈局檔案壓入 mDecor 中 id 為 content 的 FrameLayout 中。
public void setContentView(int layoutResID) {
if (mContentParent == null) {// 是否首次呼叫
// 初始化 Decor
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {// 轉場動畫,預設 false
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
...
} else {
// 解析佈局檔案
mLayoutInflater.inflate(layoutResID, mContentParent);
}
...
}
複製程式碼
小結
至此,setContentView() 方法的流程就走完了,總體來看分為三個步驟:
- 初始化 mDecor,它是 DecorView 類的例項,DecorView 繼承自 FrameLayout;
- 根據 theme 中的屬性值,選擇相應的佈局檔案並通過 infalter.inflater() 方法將它載入出來並 add 到 mDecor 中;這些佈局檔案都有一個 id 為 content 的 FrameLayout;
- 在 Activity 的 setContentView() 方法中設定的 layout 佈局檔案,會通過 mLayoutInflater.inflater() 壓入 mDecor 中 id 為 content 的 FrameLayout 中。
這個過程的時序圖如下:
Activity、PhoneWindow、DecorView 和 ContentView 的關係如下圖所示:
但是,此時我們的佈局還沒有顯示出來,接著往下看。
步驟三:ViewRootImpl 的建立和關聯 DecorView 以及繪製前的準備
跟蹤程式碼執行過程
在開篇的時序圖中我們可以看到,ActivityThread 在 handleLaunchActivity() 方法中間接呼叫了 Activity 的 attach() 和 onCreate():
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
WindowManagerGlobal.initialize();
// 執行 performLaunchActivity 方法
Activity a = performLaunchActivity(r, customIntent);
if (a != null) {
r.createdConfig = new Configuration(mConfiguration);
Bundle oldState = r.state;
// 執行 handleResumeActivity 方法,最終呼叫 onStart 和 onResume 方法
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed);
if (!r.activity.mFinished && r.startsNotResumed) {
r.activity.mCalled = false;
mInstrumentation.callActivityOnPause(r.activity);
r.paused = true;
}
} else {
// 停止該 Activity
ActivityManagerNative.getDefault()
.finishActivity(r.token, Activity.RESULT_CANCELED, null, false);
}
}
複製程式碼
接著會呼叫 handleResumeActivity() 方法:
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
ActivityClientRecord r = mActivities.get(token);
...
// 最終會呼叫 onStart() 和 onResume() 等方法
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();
// 獲取 DecorView
View decor = r.window.getDecorView();
// 將 DecorView 設定成不可見
decor.setVisibility(View.INVISIBLE);
// 獲取 ViewManager,這裡是 WindowManagerImpl 例項
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
...
if (a.mVisibleFromClient && !a.mWindowAdded) {
// 標記設定為 true
a.mWindowAdded = true;
// 呼叫 WindowManagerImpl 的 addView 方法
wm.addView(decor, l);
}
} else if (!willBeVisible) {
...
}
...
if (!r.activity.mFinished && willBeVisible
&& r.activity.mDecor != null && !r.hideForNow) {
...
if (r.activity.mVisibleFromClient) {
// 呼叫 makeVisible 方法將 DecorView 設定為可見
r.activity.makeVisible();
}
}
...
} else {
try {
// 在此過程出現異常,則直接殺死 Activity
ActivityManagerNative.getDefault()
.finishActivity(token, Activity.RESULT_CANCELED, null,
Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
}
複製程式碼
執行完 performResumeActivity() 方法之後,接著會取出 ActivityClientRecord 中的 Activity 物件,並得到之前在 setContentView 流程中初始化好的 DecorView 物件,然後會將它作為引數傳入 ViewManager 型別的物件 wm 的 addView 方法,ViewManager 是一個介面,那麼它是由誰來實現的呢?這裡先打個岔,回頭看一下 Activity 的 attach() 方法:
final void attach(...) {
...
// mWindow 是一個 PhoneWindow 物件
mWindow = new PhoneWindow(this, window);
...
// 呼叫 Window 的 setWindowManager 方法
mWindow.setWindowManager(...);
...
}
複製程式碼
跟蹤 Window 的 setWindowManager() 方法:
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
...
// mWindowManager 就是 WindowManagerImpl 物件的例項
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
複製程式碼
因此,回到主線,Activity 的 getWindowManager() 獲取到的就是 WindowManagerImpl 物件的例項,再看它的 addView() 方法:
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
// mGlobal 是 WindowManagerGlobal 物件的例項
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
複製程式碼
mGlobal 是 WindowManagerGlobal 物件的例項,檢視它的 addView() 方法:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
...
root = new ViewRootImpl(view.getContext(), display);
...
mRoots.add(root);
...
}
try {
// 將傳過來的 DecorView 新增到 ViewRootImpl 中
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
...
}
}
複製程式碼
DecorView 新增到 ViewRootImpl 之後,便和 ViewRootImpl 建立了聯絡,再看 ViewRootImpl 的 setView() 方法:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
...
requestLayout();
...
view.assignParent(this);
...
}
}
}
複製程式碼
跟蹤 ViewRootImpl 的 requestLayout() 方法:
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
複製程式碼
跟蹤 ViewRootImpl 的 scheduleTraversals() 方法:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 設定同步障礙,暫停處理後面的同步訊息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 在下一幀到來的時候執行 mTraversalRunnable
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
}
複製程式碼
首先,設定同步障礙,暫停處理後面的同步訊息,然後利用 Choreographer 類在下一繪製幀來臨的時候執行 mTraversalRunnable 物件(關於 Choreographer 原理,可檢視Android系統Choreographer機制實現過程)。mTraversalRunnable 是一個 Runnable 物件:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
複製程式碼
run() 方法裡面只有一句程式碼,doTraversal() 方法如下:
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 移除同步障礙
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
...
// 正式進入 View 繪製流程
performTraversals();
...
}
}
複製程式碼
移除了同步障礙之後,所有繪製之前的準備工作已經執行完畢,接下來會呼叫performTraversals() 方法正式進入 View 的繪製流程。
這裡不要忘了,handleResumeActivity() 方法最後還有一句程式碼尚未執行:
r.activity.makeVisible();
別急,主線流程還沒走完呢。
小結
這一步驟主要的工作內容是:
- 初始化 ViewRootImpl;
- 呼叫 ViewRootImpl 的 setView() 方法,將 DecorView 物件傳遞過去;
- 呼叫 ViewRootImpl 的 requestLayout() 方法,作出繪製之前的最後準備。
這個過程的時序圖如下:
步驟四:正式繪製 View
接上一步驟的內容,繪製前的準備工作完成後,ViewRootImpl 的 performTraversals() 方法將會被呼叫,這個方法內容相當多,忽略條件判斷,精簡過後如下:
private void performTraversals() {
...
WindowManager.LayoutParams lp = mWindowAttributes;
...
// 獲取 DecorView 寬和高的 MeasureSpec
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
// 執行 Measure 流程
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
// 執行 Layout 流程
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
// 執行 Draw 流程
performLayout();
...
}
複製程式碼
首先,根據 getRootMeasureSpec() 方法獲取到 childWidthMeasureSpec 和 childHeightMeasureSpec 的值,用於 DecorView 的繪製。因為 DecorView 是所有子元素的根元素,子元素的佈局層層巢狀,因此會接著從 DecorView 開始進行一層層地對所有子元素進行測量、佈局和繪製,分別對應 performMeasure()、performLayout() 和 performLayout() 方法,整個過程的示意圖如下:
理解 MeasureSpec
MeasureSpec 是 View 的一個內部類,簡單來說就是一個 32 位的 int 值,採用它的高 2 位表示三種 SpecMode(測量模式),低 30 位用來表示 SpecSize(某種測量模式下的規格大小)。採用這種表示方法是為了避免建立過多的物件以減少記憶體分配,MeasureSpec 的定義如下:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// 不限定測量模式:父容器不對 View 作任何限制,View 想要多大給多大,
// 這種模式通常用於系統內部。
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// 精確為 SpecSize:父容器已經確定 View 需要的大小,就是 SpecSize,
// 對應佈局引數是 match_parent 或具體數值時的情況
public static final int EXACTLY = 1 << MODE_SHIFT;
// 最大隻能是 SpecSize:父容器規定 View 最大隻能是 SpecSize,
// 對應佈局引數是 wrap_content 時的情況
public static final int AT_MOST = 2 << MODE_SHIFT;
// 根據 SpecMode 和 SpecSize 建立一個 MeasureSpec
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
// 獲取 SpecMode
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
// 獲取 SpecSize
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
// 調整 MeasureSpec
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
if (mode == UNSPECIFIED) {
return make MeasureSpec(0, UNSPECIFIED);
}
int size = getSize(measureSpec) + delta;
if (size < 0) {
size = 0;
}
return makeMeasureSpec(size, mode);
}
}
複製程式碼
接著看 getRootMeasureSpec() 方法,傳入的第一個引數是整個螢幕的寬或者高,第二個引數是 Window 的 LayoutParams:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
複製程式碼
從程式碼邏輯來看,DecorView 的 MeasureSpec 的產生遵循如下規律:
- LayoutParams = MATCH_PARENT:精確模式,大小就是螢幕的寬或者高;
- LayoutParams = WRAP_CONTENT:最大不能超過螢幕的寬或者高;
- 固定大小:精確模式,大小為 LayoutParams 中指定的值。
但是對於普通的 View 來說,View 的 measure() 方法是由父容器 ViewGroup 呼叫的,看一下 ViewGroup 的 measureChildWithMargins() 方法:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
// 獲取子元素的佈局引數
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 產生子元素的 MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製程式碼
可以看出,在呼叫子元素 的 measure() 方法之前,先要呼叫 getChildMeasureSpec() 方法產生子元素的 MeasureSpec,子元素的產生除了跟父容器的 MeasureSpec 和子元素本身的 LayoutParams 有關之外,還與子元素的 Margin 和父容器的 Padding 值以及父容器當前佔用空間有關,具體的過程可以看 getChildMeasureSpec() 方法:
public static int getChildMeasureSpec(int spec, int padding, int childDimesion) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// 子元素最大可用空間為父容器的尺寸減去父容器中已被佔用的空間的大小
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (sepcMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimesion == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size....
// find out how big it should be
resultSize = 0;
resultMode == MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製程式碼
整個過程稍微有點複雜,可以參考以下表格:
Measure 流程分析
回到 performTraversals() 方法中,獲取到 DecorView 的 MeasureSpec 後接著會呼叫 performMeasure() 方法:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
...
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
複製程式碼
mView 就是之前通過 setView() 方法傳遞過來的 DecorView 例項,它繼承自 FrameLayout,而 FrameLayout 又是一個 ViewGroup 同時繼承自 View。View 的 measure() 方法是 final 型別的,不允許子類去重寫,因此這裡呼叫的實際上是 View 的 measure 方法:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
複製程式碼
View 的 onMeasure() 實現:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
複製程式碼
可以看到,View 預設的 onMeasure() 方法首先會呼叫 getDefaultSize() 獲取寬和高的預設值,然後再呼叫 setMeasuredDimension() 將獲取的值進行設定,檢視 getDefaultSize() 的程式碼:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
複製程式碼
無論是 AT_MOST 還是 EXACTLY,最終返回的都是 measureSpec 中的 specSize,這個 specSize 就是測量後的最終結果。至於 UNSPECIFIED 的情況,則會返回一個建議的最小值,這個值和子元素設定的最小值它的背景大小有關。
從 onMeasure() 的預設實現可以看出,如果我們自定義一個直接繼承自 View 的控制元件如果不重寫 onMeasure() 方法,在使用這個控制元件並把 layout_width 或 layout_height 設定成 wrap_content 的時候,效果將會和 match_parent 一樣!因為佈局中使用 wrap_content 的時候,根據上面 getChildMeasureSpec() 方法總結出來的表格可以知道,此時的 specMode 是 AT_MOST,specSize 是 parentSize,而 parentSize 是父容器當前剩餘的空間大小,此時 getDefaultSize() 就會返回 specSize,因此子元素的寬或高就被設定成了等於當前父容器剩餘空間的大小了,這顯然不符合我們的預期,如何解決這個問題呢?一個通用的方案就是像如下方式重寫 onMeasure() 方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 預設的寬/高
int mWidth = default_width;
int mHeight = default_height;
// 當佈局引數設定為 wrap_content 時,使用預設值
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(mWidth, mHeight);
// 寬 / 高任意一個佈局引數為 wrap_content 時,都使用預設值
} else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(mWidth, heightSize);
} else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(widthSize, mHeight);
}
}
複製程式碼
因為 DecorView 繼承自 FrameLayout,它是一個 ViewGroup,ViewGroup 是一個抽象類,它並沒有定義一個具體的測量過程,預設使用 View 的 onMeasure() 進行測量。它的測量過程需要各個子類通過重寫 onMeasure() 方法去實現,因為不同的子類具有不同的佈局特性,因此需要不一樣的測量邏輯,DecorView 也自然重寫了 onMeasure() 方法來實現自己的測量邏輯,簡略後方法內容如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
final int widthMode = getMode(widthMeasureSpec);
final int heightMode = getMode(heightMeasureSpec);
...
if (widthMode == AT_MOST) {
...
}
...
if (heightMode == AT_MOST) {
...
}
...
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
複製程式碼
最終會呼叫父類 FrameLayout 的 onMeasure() 方法,簡略後方法內容如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
...
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
...
}
}
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
...
}
複製程式碼
可以看到,這裡會遍歷它的每一個子元素,並呼叫 measureChildWithMargins() 方法,這個方法其實前面已經出現過,它的作用是計算出子元素的 MeasureSpec 後呼叫子元素本身的 measure() 方法:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
// 獲取子元素的佈局引數
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 產生子元素的 MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製程式碼
此時實際上呼叫的也是 View 的 measure() 方法,從上面的內容可以知道,子元素的 onMeasure() 方法又會被呼叫,這樣便實現了層層遞迴地呼叫到了每個子元素的 onMeasure() 方法進行測量。
Layout 流程分析
再次回到 performTraversals() 方法,執行完 performMeasure() 遍歷測量所有子元素之後,接著會呼叫 performLayout() 方法:
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
...
// 呼叫 DecorView 的 layout() 方法
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}
複製程式碼
這裡的 getMeasuredWidth() 和 getMeasuredHeight() 就是 DecorView 在前面 Measure 流程中計算得到的測量值,它們都被作為引數傳入 layout() 方法中,這裡呼叫的是 View 的 layout() 方法:
public void layout(int l, int t, int r, int b) {
...
// View 狀態是否發生了變化
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 如果 View 狀態有變化,則重新佈局
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
...
}
...
}
複製程式碼
setOpticalFrame() 內部也直接呼叫了 setFrame() 方法,檢視 setFrame() 方法的實現:
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
...
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);
// 變數初始化
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
// 更新用於渲染的顯示列表
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
...
if (sizeChanged) {
// 如果 View 大小發生變化,則會在裡面回撥 onSizeChanged 方法
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
...
}
// 返回是否發生變化
return changed;
}
複製程式碼
setFrame() 方法的主要作用有以下幾點:
- 判斷 View 位置是否發生變化,並根據變化情況進行相應的處理;
- 初始化 mLeft、mBottom、mRight 和 mTop 變數,首次呼叫這個方法的時候返回值為 true;
- 呼叫RenderNode中原生方法更新用於渲染的顯示列表。
回到 layout() 方法,根據 setFrame() 方法返回的狀態判斷是否需要呼叫 onLayout() 進行重新佈局,檢視 onLayout() 方法:
/**
* Assign a size and position to a view and all of its
* descendants
*
* <p>This is the second phase of the layout mechanism.
* (The first is measuring). In this phase, each parent calls
* layout on all of its children to position them.
* This is typically done using the child measurements
* that were stored in the measure pass().</p>
*
* <p>Derived classes should not override this method.
* Derived classes with children should override
* onLayout. In that method, they should
* call layout on each of their children.</p>
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
複製程式碼
onLayout() 方法是一個空方法,從註釋最後一段內容可以瞭解到,單一的 View 並不需要重寫這個方法,當 View 的子類具有子元素(即 ViewGroup)的時候,應該重寫這個方法並呼叫每個子元素的 layout() 方法,因此作為一個 ViewGroup,我們檢視 DecorView 的 onLayout() 方法:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
...
}
複製程式碼
這裡的主要邏輯是呼叫父類的 onLayout() 方法,繼續跟蹤 FrameLayout 的 onLayout() 方法:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
// 呼叫每個子元素的 layout 方法
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
複製程式碼
在 layoutChildren() 方法中,根據自己的佈局邏輯,計算出每個子元素的 left、top、right 和 bottom 值,並呼叫它們的 layout() 方法。
Layout 流程的作用是 ViewGroup 用來確定它的子元素的位置, 當 ViewGroup 的位置被確定後,在它的 onLayout() 方法中就會遍歷呼叫所有子元素的 layout() 方法,子元素的 layout() 方法被呼叫的時候它的 onLayout() 方法又會被呼叫,這樣就實現了層層遞迴。
Draw 流程分析
最後,又一次回到主線中的 performTraversals() 方法,此時,經過 Measure 流程確定了每個 View 的大小並且經過 Layout 流程確定了每個 View 的擺放位置,下面將進入下一個流程確定每個 View 的具體繪製細節。檢視 performDraw() 方法內容:
private void performDraw() {
...
final boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
mIsDrawing = true;
try {
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
}
...
}
複製程式碼
跟蹤 draw() 方法:
private void draw(boolean fullRedrawNeeded) {
...
// “髒”區域,即需要重繪的區域
final Rect dirty = mDirty;
...
if (fullRedrawNeeded) {
mAttachInfo.mIgnoreDirtyState = true;
dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}
...
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
...
// 呼叫 drawSoftware 方法
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
}
...
}
複製程式碼
檢視 drawSoftware() 方法實現:
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
// Draw with software renderer.
final Canvas canvas;
try {
final int left = dirty.left;
final int top = dirty.top;
final int right = dirty.right;
final int bottom = dirty.bottom;
// 鎖定canvas區域,由 dirty 區域決定
canvas = mSurface.lockCanvas(dirty);
...
canvas.setDensity(mDensity);
} catch (Surface.OutOfResourcesException e) {
handleOutOfResourcesException(e);
return false;
} catch (IllegalArgumentException e) {
mLayoutRequested = true; // ask wm for a new surface next time.
return false;
}
try {
...
try {
...
// 呼叫 DecorView 的 draw 方法
mView.draw(canvas);
...
} finally {
...
}
} finally {
mLayoutRequested = true; // ask wm for a new surface next time.
//noinspection ReturnInsideFinallyBlock
return false;
}
return true;
}
複製程式碼
繼續檢視 DecorView 的 draw() 方法:
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (mMenuBackground != null) {
mMenuBackground.draw(canvas);
}
}
複製程式碼
主要是呼叫了父類的 draw 方法,FrameLayout 和 ViewGroup 都沒有重寫 draw() 方法,所以直接看 View 的 draw() 方法:
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
/*
* Here we do the full fledged routine...
* (this is an uncommon case where speed matters less,
* this is why we repeat some of the tests that have been
* done above)
*/
boolean drawTop = false;
boolean drawBottom = false;
boolean drawLeft = false;
boolean drawRight = false;
float topFadeStrength = 0.0f;
float bottomFadeStrength = 0.0f;
float leftFadeStrength = 0.0f;
float rightFadeStrength = 0.0f;
// Step 2, save the canvas' layers
int paddingLeft = mPaddingLeft;
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired);
if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}
final ScrollabilityCache scrollabilityCache = mScrollCache;
final float fadeHeight = scrollabilityCache.fadingEdgeLength;
int length = (int) fadeHeight;
// clip the fade length if top and bottom fades overlap
// overlapping fades produce odd-looking artifacts
if (verticalEdges && (top + length > bottom - length)) {
length = (bottom - top) / 2;
}
// also clip horizontal fades if necessary
if (horizontalEdges && (left + length > right - length)) {
length = (right - left) / 2;
}
if (verticalEdges) {
topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
drawTop = topFadeStrength * fadeHeight > 1.0f;
bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
}
if (horizontalEdges) {
leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
drawLeft = leftFadeStrength * fadeHeight > 1.0f;
rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
drawRight = rightFadeStrength * fadeHeight > 1.0f;
}
saveCount = canvas.getSaveCount();
int solidColor = getSolidColor();
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
if (drawBottom) {
matrix.setScale(1, fadeHeight * bottomFadeStrength);
matrix.postRotate(180);
matrix.postTranslate(left, bottom);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, bottom - length, right, bottom, p);
}
if (drawLeft) {
matrix.setScale(1, fadeHeight * leftFadeStrength);
matrix.postRotate(-90);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, left + length, bottom, p);
}
if (drawRight) {
matrix.setScale(1, fadeHeight * rightFadeStrength);
matrix.postRotate(90);
matrix.postTranslate(right, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(right - length, top, right, bottom, p);
}
canvas.restoreToCount(saveCount);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}
複製程式碼
從程式碼註釋中可以看到,draw() 過程分為六個步驟,分別是:
- 繪製 View 的背景;
- 儲存當前 canvas 的圖層;
- 繪製 View 的內容;
- 如果有子元素,則呼叫子元素的 draw() 方法;
- 繪製淡入淡出效果並恢復圖層;
- 繪製 View 的裝飾(滾動條等)。
其中第二步和第五步一般情況下不會用到,我們繼續看第三步,看看它是如何分配子元素的繪製的:
protected void dispatchDraw(Canvas canvas) {
}
複製程式碼
顯然,單一的 View 並沒有子元素,因此,看看 ViewGroup 是怎麼實現這個過程的:
@Override
protected void dispatchDraw(Canvas canvas) {
...
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
...
}
...
}
...
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
複製程式碼
在 ViewGroup 的 dispatchDraw() 中,遍歷每個子元素並呼叫了它們的 draw() 方法,由此實現了層層遞迴呼叫,最終完成繪製。
小結
縱觀整個 Measure、Layout 和 Draw 過程,使用流程圖表示如下:
步驟五:顯示 View
不知道你還記不記得,上一步驟中執行的 View 的 Measure、Layout 和 Draw 流程都是前面 handleResumeActivity() 中的 wm.addView() 方法為源頭的,回顧 handleResumeActivity() 方法:
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
ActivityClientRecord r = mActivities.get(token);
...
// 最終會呼叫 onStart() 和 onResume() 等方法
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();
// 獲取 DecorView
View decor = r.window.getDecorView();
// 將 DecorView 設定成不可見
decor.setVisibility(View.INVISIBLE);
// 獲取 ViewManager,這裡是 WindowManagerImpl 例項
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
...
if (a.mVisibleFromClient && !a.mWindowAdded) {
// 標記設定為 true
a.mWindowAdded = true;
// 呼叫 WindowManagerImpl 的 addView 方法
wm.addView(decor, l);
}
} else if (!willBeVisible) {
...
}
...
if (!r.activity.mFinished && willBeVisible
&& r.activity.mDecor != null && !r.hideForNow) {
...
if (r.activity.mVisibleFromClient) {
// 呼叫 makeVisible 方法將 DecorView 設定為可見
r.activity.makeVisible();
}
}
...
} else {
try {
// 在此過程出現異常,則直接殺死 Activity
ActivityManagerNative.getDefault()
.finishActivity(token, Activity.RESULT_CANCELED, null,
Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
}
複製程式碼
可以看到在呼叫 wm.addView() 方法之前,DecorView 是處於不可見的狀態的,因此,即使經過了 Measure、Layout 和 Draw 流程,我們的 View 仍然沒有顯示在螢幕上,看看 Activity 的 makeVisible() 方法:
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
複製程式碼
執行 DecorView 的 setVisibility() 之後,我們的 View 才正式出現在螢幕上!
總結
- Window 是一個抽象類,提供了各種視窗操作的方法;
- PhoneWindow 是 Window 的唯一實現類,每個 Acitvity 中都會有一個 PhoneWindw 例項;
- DecorView —— 頂級檢視,它繼承自 FrameLayout,setContentView() 時,PhoneWindow 會建立 DecorView 並與 DecorView 建立關聯;
- PhoneWindow 會根據 Theme 和 Feature 等將對應的佈局檔案將佈局解析並新增到 DecorView 中,這些佈局中都包含了一個 id 為 content 的 FrameLayout;
- setContentView() 方法中設定的 layout 佈局檔案會被 PhoneWindow 解析並壓入 DecorView 中 id 為 content 的 FrameLayout 中;
- View 的正式繪製是從 ViewRootImpl 的 performTraversals() 方法開始的;
- 單一 View 一般需要重寫 onMeasure() 方法根據佈局引數和父 View 的測量規格計算自己的寬高並儲存;
- ViewGroup 需要重寫 onMeasure() 方法計算所有子元素的尺寸然後計算自己的尺寸並儲存;
- 單一 View 一般不需要重寫 onLayout() 方法;
- ViewGroup 需要重寫 onLayot() 方法根據測量的值確定所有子元素的位置;
- 單一 View 需要重寫 onDraw() 方法繪製自身;
- ViewGroup 需要重寫 onDraw() 方法繪製自身以及遍歷子元素對它們進行繪製。
- 在 Activity 的 onResume() 生命週期被呼叫後,ActivityThread 才會呼叫 activity.makeVisible() 讓 DecorView 可見。
系列文章
按下電源鍵後竟然發生了這一幕 —— Android 系統啟動流程分析
App 竟然是這樣跑起來的 —— Android App/Activity 啟動流程分析
螢幕上內容究竟是怎樣畫出來的 —— Android View 工作原理詳解(本文)
參考文章
(3)自定義View Layout過程 - 最易懂的自定義View原理系列
如果你對文章內容有疑問或者有不同的意見,歡迎留言,我們一同探討。