改文章為轉(zhuǎn)載來源:
作者:fishforest
鏈接:http://www.reibang.com/p/6e45f42da304
來源:簡書
著作權(quán)歸作者所有序厉。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)疏遏,非商業(yè)轉(zhuǎn)載請注明出處锉屈。
前言
之前系統(tǒng)地分析了View Measure 過程:
Android 自定義View之Measure過程
我們知道父布局根據(jù)自身和子布局的要求給子布局生成測量模式和測量尺寸瓤帚,并封裝在MeasureSpec 對象里觉痛,最終傳遞給子布局讓它最后確定自身的尺寸君旦。
很自然就會想到贡蓖,既然子布局是從父布局拿的測量結(jié)果甩牺,父布局又從它的父布局拿測量結(jié)果蘑志,最終到ViewTree的頂點根View是誰測量的呢?
循著這個問題贬派,從源碼角度一探究竟急但。
系列文章:
Window/WindowManager 不可不知之事
Android Window 如何確定大小/onMeasure()多次執(zhí)行原因
通過本篇文章,你將了解到:
1搞乏、Window 尺寸測量
2波桩、根View 尺寸測量
3、Window请敦、ViewRootImpl镐躲、View 三者關(guān)系
1、Window 尺寸測量
一個小Demo
通過WindowManager.addView(xx)展示一個懸浮窗:
private void showView() {
//獲取WindowManager實例
wm = (WindowManager) App.getApplication().getSystemService(Context.WINDOW_SERVICE);
//設(shè)置LayoutParams屬性
layoutParams = new WindowManager.LayoutParams();
//寬高尺寸
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
layoutParams.format = PixelFormat.TRANSPARENT;
//設(shè)置背景陰暗
layoutParams.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
layoutParams.dimAmount = 0.6f;
//Window類型
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}
//構(gòu)造TextView
TextView myView = new TextView(this);
myView.setText("hello window");
//設(shè)置背景為紅色
myView.setBackgroundResource(R.color.colorRed);
FrameLayout.LayoutParams myParam = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 400);
myParam.gravity = Gravity.CENTER;
myView.setLayoutParams(myParam);
//myFrameLayout 作為rootView
FrameLayout myFrameLayout = new FrameLayout(this);
//設(shè)置背景為綠色
myFrameLayout.setBackgroundColor(Color.GREEN);
myFrameLayout.addView(myView);
//添加到window
wm.addView(myFrameLayout, layoutParams);
}
上述代碼簡單概述如下:
1侍筛、構(gòu)造TextView萤皂,設(shè)置其背景為紅色
2、構(gòu)造FrameLayout匣椰,設(shè)置其背景為綠色
3裆熙、將TextView作為子View添加到FrameLayout
4、將FrameLayout作為RootView(根View)添加到Window里
懸浮窗展示完整Demo請移步:Window/WindowManager 不可不知之事
注意到
wm.addView(myFrameLayout, layoutParams);
layoutParams 里重點關(guān)注寬、高字段的值入录,我們知道這是給Window的尺寸約束齐媒,以寬為例,設(shè)置不同的值纷跛,看看其效果:
1喻括、wrap_content
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
可以看出:RootView(FrameLayout) 包裹著TextView,兩者寬度一致贫奠。
2唬血、match_parent
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
可以看出:RootView(FrameLayout) 寬充滿屏幕
3、設(shè)置具體的值
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
layoutParams.width = 800;
可以看出:RootView(FrameLayout) 寬沒充滿屏幕唤崭,屏幕寬1080px拷恨。
結(jié)合上述三張圖,我們有理由相信谢肾,wm.addView(myFrameLayout, layoutParams) 里的layoutParams 是用來約束myFrameLayout(RootView)腕侄,那么Window尺寸是怎么來的呢?
Window 尺寸的確定
從wm.addView(xx)開始分析芦疏,WindowManager 是個接口冕杠,其實現(xiàn)類是:WindowManagerImpl。
#WindowManagerImpl.java
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
//賦值token酸茴,在啟動Dialog/PopupDialog 會判斷該值
applyDefaultToken(params);
//mGlobal 為單例分预,管理所有的ViewRootImpl、RootView
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
接著看WindowManagerGlobal 的處理:
#WindowManagerGlobal.java
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) {
...
//構(gòu)造ViewRootImpl 對象
root = new ViewRootImpl(view.getContext(), display);
//view 作為RootView
//將傳進(jìn)來的wparams作為該RootView的LayoutParams
view.setLayoutParams(wparams);
//記錄對象
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
//ViewRootImpl 關(guān)聯(lián)RootView
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
...
}
}
}
由上可知薪捍,在wm.addView(xx)里傳遞進(jìn)來的LayoutParams設(shè)置給了RootView笼痹。
繼續(xù)來看ViewRootImpl.setView(xx)過程。
#ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
...
//將LayoutParams記錄到成員變量 mWindowAttributes 里
//該變量用來描述Window屬性
mWindowAttributes.copyFrom(attrs);
...
//開啟View layout 三大流程
requestLayout();
...
try {
...
//IPC 通信酪穿,告訴WindowManagerService 要創(chuàng)建Window
//將mWindowAttributes 傳入
//返回mTmpFrame 表示該Window可以展示的最大尺寸
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
//將返回的值記錄到成員變量 mWinFrame 里
setFrame(mTmpFrame);
} catch (RemoteException e) {
...
} finally {
if (restore) {
attrs.restore();
}
}
...
}
}
}
上面這段重點關(guān)注2個方面:
1凳干、傳入的LayoutParams記錄到成員變量mWindowAttributes,最后用來約束Window被济。
2救赐、添加Window時返回Window的最大尺寸,最終記錄在成員變量:mWinFrame里溉潭。
綜上所述净响,我們發(fā)現(xiàn):
wm.addView(myFrameLayout, layoutParams) 里的layoutParams不僅約束了RootView少欺,也約束了Window喳瓣。
2、根View 尺寸測量
既然知道了RootView 的layoutParams赞别,依據(jù)我們之前分析過的ViewTree的測量過程:Android 自定義View之Measure過程
可知還需要為RootView生成MeasureSpec對象畏陕。
在setView(xx)過程中調(diào)用了requestLayout 注冊了回調(diào),當(dāng)屏幕刷新信號到來之時執(zhí)行performTraversals()開啟三大流程仿滔。
#ViewRootImpl.java
private void performTraversals() {
...
//之前記錄的Window LayoutParams
WindowManager.LayoutParams lp = mWindowAttributes;
//Window需要的大小
int desiredWindowWidth;
int desiredWindowHeight;
...
Rect frame = mWinFrame;
if (mFirst) {
...
if (shouldUseDisplaySize(lp)) {
...
} else {
//mWinFrame即是之前添加Window時返回的Window最大尺寸
desiredWindowWidth = mWinFrame.width();
desiredWindowHeight = mWinFrame.height();
}
...
} else {
...
}
...
if (layoutRequested) {
...
//從方法名看應(yīng)該是測量ViewTree -----------(1)
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
...
if (mFirst || windowShouldResize || insetsChanged ||
viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
...
try {
...
//重新確定Window尺寸 --------(2)
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
...
} catch (RemoteException e) {
}
...
if (!mStopped || mReportNextDraw) {
...
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
updatedConfiguration) {
...
//再次測量ViewTree -------- (3)
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
}
} else {
...
}
...
if (didLayout) {
//對ViewTree 進(jìn)行Layout ---------- (4)
performLayout(lp, mWidth, mHeight);
...
}
...
if (!cancelDraw) {
...
//開始ViewTree Draw過程 ------- (5)
performDraw();
} else {
...
}
}
來看看標(biāo)注的重點:
(1)
measureHierarchy(xx)
#ViewRootImpl.java
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
boolean windowSizeMayChange = false;
...
//標(biāo)記是否測量成功
boolean goodMeasure = false;
//寬度為wrap_content時
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
final DisplayMetrics packageMetrics = res.getDisplayMetrics();
res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
int baseSize = 0;
if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
baseSize = (int)mTmpValue.getDimension(packageMetrics);
}
//baseSize 為預(yù)置的寬度
//desiredWindowWidth 想要的寬度是否大于預(yù)置寬度
if (baseSize != 0 && desiredWindowWidth > baseSize) {
//以baseSize 作為寬度傳入
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//測量----------------- 第一次
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//如果ViewTree的子布局需要的寬度大于父布局能給的寬度惠毁,則該標(biāo)記被設(shè)置
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
//該標(biāo)記沒被設(shè)置犹芹,說明父布局給的尺寸夠用,測量完成
goodMeasure = true;
} else {
//父布局不能滿足子布局的需求鞠绰,嘗試擴(kuò)大寬度
//desiredWindowWidth > baseSize腰埂,因此新計算的baseSize要大于原先的baseSize
baseSize = (baseSize+desiredWindowWidth)/2;
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
//拿到后繼續(xù)測量----------------- 第二次
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//繼續(xù)檢測是否滿足
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
goodMeasure = true;
}
}
}
}
//沒測量好,繼續(xù)測量
if (!goodMeasure) {
//可以看出是為RootView 生成MeasureSpec
//傳入的參數(shù):能給RootView分配的最大尺寸值以及RootView本身想要的尺寸(記錄在LayoutParams里)
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//既然MeasureSpec 有了蜈膨,那么就可以測量RootView了
//該過程就是測量整個ViewTree----------------- 第三次
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
//Window尺寸變化了屿笼,用于后續(xù)判斷執(zhí)行performMeasure(xx)
windowSizeMayChange = true;
}
}
...
return windowSizeMayChange;
}
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
//RootView 希望填充Window,則滿足它翁巍,此時它尺寸是確切值
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
//RootView 希望根據(jù)自身內(nèi)容來確定尺寸驴一,則設(shè)置為AT_MOST 模式
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
//RootView 希望直接指定尺寸值,則滿足它灶壶,此時它尺寸是確切值
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
以上代碼主要做了兩件事:
1肝断、結(jié)合Window尺寸,確定RootView 的測量模式和預(yù)估測量值(MeasureSpec)
2驰凛、根據(jù)第一步的結(jié)果胸懈,發(fā)起對ViewTree的測量(從RootView開始)
(2)
經(jīng)過對ViewTree的測量后,RootView的測量值已經(jīng)確定了恰响。
來看看relayoutWindow(xx):
#ViewRootImpl.java
private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
boolean insetsPending) throws RemoteException {
...
//重新設(shè)置Window大小
//傳入的尺寸值為RootView的尺寸值
//返回Window尺寸值存放在 mTmpFrame里
int relayoutResult = mWindowSession.relayout(mWindow, mSeq, params,
(int) (mView.getMeasuredWidth() * appScale + 0.5f),
(int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility,
insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
mTmpFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout,
mPendingMergedConfiguration, mSurfaceControl, mTempInsets);
//關(guān)聯(lián)Window和surface
if (mSurfaceControl.isValid()) {
mSurface.copyFrom(mSurfaceControl);
} else {
destroySurface();
}
//記錄Window 尺寸
setFrame(mTmpFrame);
return relayoutResult;
}
我們發(fā)現(xiàn):
- Window的尺寸是依賴于RootView的測量尺寸箫荡,并且一般來說appScale=1,也就是說Window尺寸就是RootView的尺寸渔隶。
- 此處也即是解釋了之前的Demo現(xiàn)象。
而(1)步驟的測量ViewTree是為了確定RootView的尺寸從而在此步驟確定Window尺寸间唉。
(3)(4)(5)
這里分析經(jīng)典問題:onMeasure() 為什么會執(zhí)行多次绞灼?
這三部分即是我們熟知的View 的三大過程,此處值得注意的是:
步驟(1)
在(1)步驟里的measureHierarchy(xx)呈野,我們標(biāo)注了三次測量低矮。
1、第一次:先用預(yù)置寬度測量ViewTree被冒,得到測量結(jié)果
2军掂、發(fā)現(xiàn)第一次測量結(jié)果不滿足,因為存在需要寬度比預(yù)置寬度大的子布局昨悼,于是給子布局更大的寬度蝗锥,再進(jìn)行第二次測量
3、發(fā)現(xiàn)第二次測量結(jié)果仍然不滿足率触,于是用Window能夠拿到的最大寬度再進(jìn)行測量
可以看出measureHierarchy(xx)里至少執(zhí)行一次performMeasure()终议,最多能執(zhí)行三次。而調(diào)用performMeasure()方法,該方法最終會調(diào)用到各個View的onMeasure()穴张。
每次performMeasure() 一定能夠觸發(fā)執(zhí)行onMeasure()嗎细燎?會。
原因:
再次簡單回顧一下measure(xx)代碼:
public final void measure(xx) {
...
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
//兩個條件滿足其中一個
//1皂甘、需要強(qiáng)制layout
//2玻驻、尺寸發(fā)生改變
if (forceLayout || needsLayout) {
...
onMeasure();
...
}
...
}
從上面可知,能執(zhí)行onMeasure()偿枕,說明上述條件滿足了击狮。
needsLayout肯定是不滿足的了,因為View尺寸沒變過益老。
那么只能是forceLayout=true了彪蓬。在第一次測量ViewTree的時候,僅僅只是走了Measure過程捺萌,并未走Layout過程档冬。而我們知道PFLAG_FORCE_LAYOUT 標(biāo)記是在Layout結(jié)束后清空的,因此此處PFLAG_FORCE_LAYOUT 標(biāo)記并沒有清空桃纯,當(dāng)然needsLayout=true滿足條件酷誓。
詳細(xì)的Measure/Layout/Draw 系列請移步:
步驟(3)
而在(3)步驟又再次測量了ViewTree,此時View/ViewGroup onMeasure() 再次執(zhí)行态坦。
結(jié)合步驟(1)盐数、步驟(3)總結(jié)一下:
在步驟(1)里至少執(zhí)行了一次測量,最多執(zhí)行三次
而在步驟(3)里也會執(zhí)行一次測量
當(dāng)requestLayout的時候伞梯,步驟(1)一定會執(zhí)行玫氢,也就是說保底執(zhí)行一次performMeasure(xx)->onMeasure()。
而步驟(3)在第一次展示View的時候會執(zhí)行或者窗口尺寸發(fā)生變化會執(zhí)行谜诫。
于是我們得出如下結(jié)論:
1漾峡、第一次展示View的時候,步驟(1) 喻旷、(3)一定會執(zhí)行生逸,因此onMeasure()至少執(zhí)行兩次
2、后續(xù)通過requestLayout()觸發(fā)時且预,不一定執(zhí)行步驟(3)槽袄,因此此時onMeasure()可能只會執(zhí)行一次
這就是onMeasure() 為什么會執(zhí)行多次的原因
什么時候步驟(1)會執(zhí)行三次測量?
一般來說锋谐,步驟(1)里通常只會走第三次測量遍尺,第一次、第二次不會走怀估,因為對于DecorView作為RootView來說狮鸭,lp.width == ViewGroup.LayoutParams.MATCH_PARENT。不滿足走第一次多搀、第二次的條件歧蕉。所以通常只會看到onMeasure()執(zhí)行兩次,而不是多次康铭。
當(dāng)然我們可以滿足它條件惯退,讓onMeasure()執(zhí)行5次。
#展示懸浮窗
private void testMeasure() {
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
//一定要是WRAP_CONTENT
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
//TransView為自定義View
final TransView transView = new TransView(this);
windowManager.addView(transView, layoutParams);
}
#重寫TransView onMeasure(xx)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//該View 需要寬度大于屏幕寬度
int width = resolveSizeAndState(10000, widthMeasureSpec, 0);
setMeasuredDimension(width, View.MeasureSpec.getSize(heightMeasureSpec));
}
TransView 為自定義View从藤,TransView 作為RootView催跪。
以上代碼主要做了兩件事:
1、約束Window的寬為WRAP_CONTENT
2夷野、讓其子View(TransView)申請大于屏幕寬度的寬度
想要驗證執(zhí)行多少次懊蒸,在TransView onMeasure(xx)打印即可。
另外悯搔,除了上述原因?qū)е耾nMeasure()執(zhí)行多次外骑丸,onMeasure()有可能執(zhí)行更多次,比如FrameLayout在測量子布局的時候妒貌,在某些條件下會再次觸發(fā)child.measure()過程通危,此時算起來子布局的onMeasure()執(zhí)行次數(shù)可能就會更多了,有興趣可以看看FrameLayout->onLayout(xx)灌曙。
onMeasure()為什么要執(zhí)行兩次
我們知道了會執(zhí)行兩次的原因菊碟,為什么這么設(shè)計呢?
不考慮特殊情況在刺,View在第一次展示的時候會執(zhí)行兩次onMeasure(xx)逆害。
前面提到過只要執(zhí)行了requestLayout(),步驟(1)一定會執(zhí)行蚣驼。
步驟(1)執(zhí)行的目的是為了獲取RootView的測量值忍燥,而RootView的測量值會在relayoutWindow(xx)用來重新確定Window的寬高,而步驟(3)的執(zhí)行在relayoutWindow(xx)之后隙姿,因此步驟(1)就有執(zhí)行的必要了梅垄。
至此,我們知道了RootView與Window的尺寸是如何確定了输玷。
3队丝、Window、ViewRootImpl欲鹏、View 三者關(guān)系
上邊涉及到了Window机久、RootView,所用的方法基本都是ViewRootImpl里提供的赔嚎。那么三者到底是個什么關(guān)系呢膘盖?
RootView需要添加到Window里才能展示胧弛,但是Window并不是直接管理RootView,而是通過ViewRootImpl進(jìn)行管理侠畔。
ViewRootImpl可以看做是Window结缚、RootView的中間者,負(fù)責(zé)協(xié)調(diào)這兩者软棺。
那么Window與RootView又是什么關(guān)系的红竭?
你可能已經(jīng)發(fā)現(xiàn)了,addToDisplay(xx)并沒有傳入RootView喘落,那么RootView是如何添加到Window里呢茵宪?
實際上,這里的添加說法比較擬人化瘦棋。
在relayoutWindow(xx)里稀火,傳入了mSurfaceControl,返回后就與Surface mSurface 建立了聯(lián)系赌朋。也就是說底層的Surface與Java 層的Surface關(guān)聯(lián)起來了憾股。而通過Surface,能夠拿到Canvas箕慧。而每個View的繪制都需要關(guān)聯(lián)Canvas服球,以此類推,View就與Surface關(guān)聯(lián)了颠焦,那么在View上進(jìn)行的繪制都會反饋到Surface上斩熊。這也就是說View添加到了Window。
本文基于Android 10.0
作者:fishforest
鏈接:http://www.reibang.com/p/6e45f42da304
來源:簡書
著作權(quán)歸作者所有伐庭。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)粉渠,非商業(yè)轉(zhuǎn)載請注明出處。