寫在最前:本文涉及到源碼的部分,查看的是 Android 8.1.0_r33 的源碼,部分與原文中代碼有出入。
附上查看源碼的網(wǎng)址:http://androidxref.com/
學(xué)習(xí)內(nèi)容:
- Window 和 WindowManager
- Window 的內(nèi)部工作原理
- Window 的添加、更新和刪除
- Actvitiy、Dialog 等類型的 Window 對象的創(chuàng)建過程
原文開篇部分:
- Window 是一個抽象類,具體實(shí)現(xiàn)是 PhoneWindow
- WindowManager 是外界訪問 Window 的入口,Window 的實(shí)現(xiàn)位于 WindowManagerService 中,WindowManager 和 WindowManagerService 的交互是一個 IPC 過程
- Window 實(shí)際是 View 的管理者,視圖都是通過 Window 呈現(xiàn)的。
1.Window 和 WindowManger
通過 WindowManager 添加一個 Window
mFloatingButton = new Button(this);
mFloatintButton.setText("button");
mLayoutParams = new WindowManager.LayoutParams(LayoutParams.RTAP_CONTENT,LayoutParams.WRAP_CONTENT,0,0,PixelFormat.TRANSPARENT);
mLayoutParams.flags = KatiytOarams.FLAG_NOT_TOUCH_MODAL | LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_SHOW_WHEN_LOCKED;
mLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
mLayoutParams.x = 100;
mLayoutParams.y = 300;
mWindowManager.addView(mFloatingButton, mLayoutParams);
通過以上代碼,可以將一個 Button 添加到坐標(biāo)為(100,300)的位置上。
下面說明 WindowManager.LayoutParams 中的 flags 和 type 這兩個參數(shù):
-
flags 參數(shù)表示 Window 的屬性,下面列出一個常用的選項(xiàng):
-
FLAG_NOT_FOCUSABLE
表示 WIndow 不需要獲取焦點(diǎn),也不需要接收各種輸入事件,最終事件會傳遞給下層的具有焦點(diǎn)的 Window
-
FLAG_NOT_FOCUSABLE
-
FLAG_NOT_TOUCH_MODAL
系統(tǒng)會將當(dāng)前 Window 區(qū)域以外的單擊事件傳遞給底層的 Window,當(dāng)前 Window 區(qū)域以內(nèi)的事件則自己處理。一般開啟,否則其他 Window 將無法收到單擊事件。
FLAG_SHOW_WHEN_LOCKED
開啟此模式可以讓 Window 顯示在鎖屏界面-
Type 參數(shù)表示 Window 的類型,Window 有三種類型:
- 應(yīng)用 Window:對應(yīng)一個 Activity;層級范圍是 1 ~ 99。
- 子 Window:不能單獨(dú)存在,需要附屬再特定的父 Window 上,比如常見的 Dialog;層級范圍是 1000 ~ 1999
- 系統(tǒng) Window:需要聲明權(quán)限才能創(chuàng)建的 Window,比如 Toast;層級范圍是 2000 ~ 2999
關(guān)于上面提到的層級范圍,此處進(jìn)行說明:Window 是分層的,每個 Window 都有對應(yīng)的 z-ordered,層級大的會覆蓋再層級小的 Window 的上面。
WindowManager 提供的功能
提供的功能很簡單,一般只有三個方法:添加 View、更新 View 和刪除 View,這三個方法定義在接口 ViewManager 中(WindowManager 繼承了 ViewManager)
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
//繼承關(guān)系
public interface WindowManager extends ViewManager {
//...
}
原文中說道,"WindowManager 操作 Window 的過程更像是在操作 Window 中的 View"。
2. Window 的內(nèi)部機(jī)制
Window 是一個抽象概念,每一個 Window都對應(yīng)著一個 View 和一個 ViewRootImpl,WIndow 和 View 通過 ViewRootImpl 建立聯(lián)系,因此 Window 以 View 的形式存在,View 才是 Window 存在的實(shí)體。
實(shí)際開發(fā)中,對 Window 的訪問必須通過 WindowManager。
2.1 Window 的添加過程
添加過程通過 WindowManager 的 addView 來實(shí)現(xiàn),具體實(shí)現(xiàn)類是 WindowManagerImpl 類。
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.updateViewLayout(view, params);
}
@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}
可以看到,實(shí)際上 WindowManagerImp 采用了橋接模式,將具體的實(shí)現(xiàn) 委托 給了 mGlobal(WindowManagerGlobal)來處理,WindowManagerGlobal 以工廠的形式向外提供自己的實(shí)例。
WindowManagerGlobal 的 addView 方法有如下幾步:
-
檢查參數(shù)是否合法,如果是子 Window 那么還需要調(diào)整一些布局參數(shù)
if (view == null) { throw new IllegalArgumentException("view must not be null"); } if (display == null) { throw new IllegalArgumentException("display must not be null"); } if (!(params instanceof WindowManager.LayoutParams)) { throw new IllegalArgumentException("Params must be WindowManager.LayoutParams"); } final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params; if (parentWindow != null) { parentWindow.adjustLayoutParamsForSubWindow(wparams); }
-
創(chuàng)建 ViewRootImpl 并將 View 添加到列表中
root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); mRoots.add(root); mParams.add(wparams);
關(guān)于上面代碼中的 mViews,mRoots 等:
//存儲所有 Window 所對應(yīng)的 View private final ArrayList<View> mViews = new ArrayList<View>(); //存儲所有 Window 所對應(yīng)的 ViewRootImpl private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>(); //存儲所有 Window 中對應(yīng)的布局參數(shù) private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>(); //存儲正在被刪除的 View 對象,或者說已經(jīng)調(diào)用 removeView 方法但是刪除操作尚未完成 Window 對象 private final ArraySet<View> mDyingViews = new ArraySet<View>();
-
通過 ViewRootImpl 來更新界面并完成 Window 的添加過程
這個步驟通過 ViewRootImpl 的 setView 方法完成:
//WindowManagerGlobal.addView() 方法內(nèi)部 root.setView(view, wparams, panelParentView); //ViewRootImpl.setView(...) 方法內(nèi)部 //... requestLayout(); //... //ViewRootImpl @Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } }
可以看到,通過 requestLayout 發(fā)起異步刷新請求,而其中的 scheduleTraversals 實(shí)際上是 View 繪制的入口。
接著會通過 WindowSession 最終來完成 Window 的添加過程。
//ViewRootImpl.setView(..)方法內(nèi)部 try { mOrigWindowType = mWindowAttributes.type; mAttachInfo.mRecomputeGlobalAttributes = true; collectViewAttributes(); res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mInputChannel); } catch (RemoteException e) { mAdded = false; mView = null; mAttachInfo.mRootView = null; mInputChannel = null; mFallbackEventHandler.setView(null); unscheduleTraversals(); setAccessibilityFocus(null, null); throw new RuntimeException("Adding window failed", e); }
上面代碼中,mWindowSession 類型是 IWindowSession,是一個 Binder 對象,真正的實(shí)現(xiàn)類是 Session,也就是說 WIndow 的添加過程是一個 IPC 調(diào)用。
在 Session 內(nèi)部會通過 WindowManagerService 實(shí)現(xiàn) Window 的添加:
@Override public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets, Rect outOutsets, InputChannel outInputChannel) { return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outContentInsets, outStableInsets, outOutsets, outInputChannel); }
如此,Window 的添加請求就交給 WindowManagerService 去處理了,在 WindowManagerService 中會為每一個應(yīng)用保留一個單獨(dú)的 Session,具體不再分析。
(原文分析到此,認(rèn)為到此添加流程已經(jīng)很清晰,再深入 WindowManagerSercice 也只是一系列代碼細(xì)節(jié))
2.2 Window 的刪除過程
Window 的刪除過程和添加過程初始階段類似,直接分析 WindowManagerGlobal 的 removeView 方法:
public void removeView(View view, boolean immediate) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
synchronized (mLock) {
int index = findViewLocked(view, true);
View curView = mRoots.get(index).getView();
removeViewLocked(index, immediate);
if (curView == view) {
return;
}
throw new IllegalStateException("Calling with view " + view + " but the ViewAncestor is attached to " + curView);
}
}
邏輯很清晰,首先通過 findViewLocked 方法查找待刪除的 View 的索引,然后調(diào)用 removeViewLocked 來做進(jìn)一步刪除:
private void removeViewLocked(int index, boolean immediate) {
ViewRootImpl root = mRoots.get(index);
View view = root.getView();
if (view != null) {
InputMethodManager imm = InputMethodManager.getInstance();
if (imm != null) {
imm.windowDismissed(mViews.get(index).getWindowToken());
}
}
boolean deferred = root.die(immediate);
if (view != null) {
view.assignParent(null);
if (deferred) {
mDyingViews.add(view);
}
}
}
removeViewLocked 是通過 ViewRootImpl 來完成刪除操作的,調(diào)用了其 die 方法。
上面代碼中的 immediate 參數(shù)需要注意,該參數(shù)對應(yīng)了 WindowManager 中的兩種刪除接口 removeView(immediate 為 false) 和 removeViewImmediate(immediate 為 true),分別表示異步刪除和同步刪除。
boolean die(boolean immediate) {
// Make sure we do execute immediately if we are in the middle of a traversal or the damage
// done by dispatchDetachedFromWindow will cause havoc on return.
if (immediate && !mIsInTraversal) {
doDie();
return false;
}
if (!mIsDrawing) {
destroyHardwareRenderer();
} else {
Log.e(mTag, "Attempting to destroy the window while drawing!\n" + " window=" + this + ", title=" + mWindowAttributes.getTitle());
}
mHandler.sendEmptyMessage(MSG_DIE);
return true;
}
die 方法內(nèi)部做了簡單判斷:
- 如果是同步刪除,那么直接調(diào)用 doDie 方法;
- 如果是異步刪除,則會發(fā)送一個 MSG_DIE 的請求刪除消息,ViewRootImpl 中的 Handler 會處理此消息并調(diào)用 doDie 方法;在此過程中,由于 View 尚未完成刪除操作,因此在上面 removeViewLocked 方法末尾部分代碼中,會將其添加到 mDyingViews 中。
在 doDie 方法內(nèi)部會調(diào)用 dispatchDetachedFromWindow 方法,在其中實(shí)現(xiàn)真正刪除 View 的邏輯,該方法中主要做 四件事:
- 垃圾回收相關(guān)的工作,比如清楚數(shù)據(jù)和消息、移除回調(diào)
- 通過 Session 的 remove 方法刪除Window,同樣是一個 IPC 過程,最終會調(diào)用 WindowManagerService 的 removeView 方法
- 調(diào)用 View 的 dispatchDetachedFromWindow 方法,內(nèi)部會調(diào)用 View 的 onDetachedFromWindow(View 從 Window 中移除時的回調(diào),做終止動畫、停止線程等一系列資源回收工作) 以及 onDetachedFromWindowInternal。
- 調(diào)用 WindowManagerGloabl 的 doRemoveView 方法刷新數(shù)據(jù),包括 mRoots、mParams 以及 mDyingViews,需要將當(dāng)前 Window 關(guān)聯(lián)的這三類對象從列表中刪除。
2.3 Window 的更新過程
直接看 WindowManagerGlobal 的 updateViewLayout 方法:
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
view.setLayoutParams(wparams);
synchronized (mLock) {
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index);
mParams.remove(index);
mParams.add(index, wparams);
root.setLayoutParams(wparams, false);
}
}
這個過程就比較簡單:
- 首先更新 View 的 LayoutParams 并替換舊的 LayoutParams
- 更新 ViewRootImpl 的 LayoutParams
- ViewRootImpl 會通過 schedulTraversals 方法對 View 重新布局,包括 測量、布局、重繪三個過程
- ViewRootImpl 還會通過 WindowSession 更新 Window 的視圖,這個過程最終由 WindowManagerService 的 relayoutWindow 具體實(shí)現(xiàn),同樣是一個 IPC 過程
3.Window 的創(chuàng)建過程
3.1 Activity 的 Window 的創(chuàng)建過程
首先應(yīng)該了解 Activity 的啟動過程,這一部分留待第九章再說。
簡單來說,最終會由 ActivityThread 的 performLaunchActivity 來完成整個啟動過程,此方法中會通過類加載器創(chuàng)建 Activity 的實(shí)例對象,并調(diào)用 attach 方法為其關(guān)聯(lián)運(yùn)行過程中所依賴的一系列上下文環(huán)境對象。
在 Activity 的 attach 方法里,系統(tǒng)會創(chuàng)建 Activity 所屬的 Window 對象并為其設(shè)置回調(diào)接口,Window 的對象創(chuàng)建由 PolicyManager 的 makeNewWindow 方法實(shí)現(xiàn)。同時當(dāng) Window 接收到外界的狀態(tài)改變時就會通過 Callback 接口,回調(diào) Activity 的方法。
關(guān)于 Window 的創(chuàng)建:
- Activity 的 Window 通過 PolicyManager 的工廠方法創(chuàng)建,PolicyManager 是一個契約類,實(shí)現(xiàn)了 IPolicy 策略接口,該接口中定義了眾多工廠方法。
- PolicyManager 的真正實(shí)現(xiàn)是 Policy 類,在 makeNewWindow 方法中,返回了 PhoneWindow 對象,由此得出 Window 的具體實(shí)現(xiàn)是 PhoneWindow。
關(guān)于 Activity 的視圖如何附屬到 Window:
Activity 的 setContentView 方法交由 Window 處理,直接分析 PhoneWindow 的 setContentView 即可
PhoneWindow 的 setContentView 方法遵循以下幾個步驟
-
如果沒有 DecorView,那么創(chuàng)建之。
- 通過 installDecor 方法內(nèi)部的 generateDecor 直接創(chuàng)建 DecorView
- 通過 generateLayout 方法加載具體的布局文件到 DecorView
- 將 View 添加到 DecorView 的 mContentParent 中
到此步,Activity 的布局文件已經(jīng)添加到 DecorView 里面。
- 回調(diào) Activity 的 onContentChanged 方法通知 Activity 視圖已經(jīng)發(fā)生改變
Activity 的 onContentChanged 方法是個空實(shí)現(xiàn),可以在 子Activity 中處理該回調(diào)。
但是!!
經(jīng)過上面的三個步驟,DecorView 尚未被 WindowManager 正式添加到 Window。由于此時 DecorView 未被 WindowManager 識別,所以這個時候的 Window 無法提供具體功能,因?yàn)樗€無法接收外界的輸入信息。
在 ActivityThread 的 handleResumeActivity 方法中,首先調(diào)用 Activity 的 onResume 方法,接著調(diào)用 Activity 的 makeVisible(),在 makeVisible 方法中,DecorView 真正完成添加和顯式這兩個過程,此時 Activity 的視圖才能被用戶看到,如下所示:
void makeVisible() {
if(!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAtrributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
3.2 Dialog 的 Window 的創(chuàng)建過程
Dialog 的 Window 創(chuàng)建過程和 Activity 類似,有如下幾個步驟:
1. 創(chuàng)建 Window:
2. 初始化 DecorView 并將 Dialog 的視圖添加到 DecorView 中
3. 將DecorView 添加到 Window 中顯示
? 在 Dialog 的 show 方法中,通過 WindowManager 的 addView 方法將 DecorView 添加到 Window 中。
Dialog 的 Window 創(chuàng)建和 Activity 的 Window 創(chuàng)建過程很類似,幾乎沒有什么區(qū)別;當(dāng) Dialog 被關(guān)閉時,會通過 WindowManager 的 removeViewImmediate(mDecor) 移除 DecorView。
需要注意:
- 普通 Dialog 必須采用 Activity 的 Context,如果采用 Application 的 Context 會報錯,因?yàn)樾枰?應(yīng)用token,而只有 Activity 擁有
- 系統(tǒng) Window 比較特殊,不需要 token,所以也可以采用 Application 的 Context 的同時,指定對話框的 Window 為系統(tǒng)類型即可正常彈出對話框
3.3 Toast 的 Window 的創(chuàng)建過程
首先 Toast 也是基于 Window 實(shí)現(xiàn)的,但是由于 Toast 具有定時取消這一功能,所以系統(tǒng)采用了 Handler。
Toast 內(nèi)部有兩類 IPC 過程:
- Toast 訪問 NotificationManagerService(NMS)
- NMS 回調(diào) Toast 里的 TN 接口
Toast 提供 show 和 cancel 分別用于顯示和隱藏 Toast,二者內(nèi)部都是一個 IPC 過程:
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
public void cancel() {
mTN.cancel();
}
顯示和隱藏都需要 NMS 實(shí)現(xiàn),而 NMS 無法運(yùn)行在系統(tǒng)的進(jìn)程中,所以需要通過遠(yuǎn)程調(diào)用的方式。
關(guān)于 TN 這個類,它是一個 Binder 類,在 Toast 和 NMS 進(jìn)行 IPC 的過程中,當(dāng) NMS 處理 Toast 的顯示或隱藏請求時會跨進(jìn)程回調(diào) TN 中的方法,此時由于 TN 運(yùn)行在 Binder 線程池中,所以需要通過 Handler 將其切換到當(dāng)前線程中。同時這也意味著 Toast 無法在沒有 Looper 的線程中彈出。
首先分析 Toast 的顯示過程( show 方法)
調(diào)用了 NMS 中的 enqueueToast 方法
-
enqueueToast 首先將 Toast 請求封裝為 ToastRecord 對象并將其添加到 mToastQueue 隊列中。
mToastQueue 是一個 ArrayList,對非系統(tǒng)應(yīng)用而言,mToastQueue 最多同時存在 50 個 ToastRecord,此舉是為了防止 DOS 拒絕服務(wù)攻擊,避免其他應(yīng)用沒有機(jī)會彈出 Toast
-
添加后,NMS 通過 showNextToastLocked 方法顯示當(dāng)前 Toast。
void showNextToastLocked() { ToastRecord record = mToastQueue.get(0); while (record != null) { if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback); try { record.callback.show(record.token); scheduleTimeoutLocked(record); return; } catch (RemoteException e) { Slog.w(TAG, "Object died trying to show notification " + record.callback + " in package " + record.pkg); // remove it from the list and let the process die int index = mToastQueue.indexOf(record); if (index >= 0) { mToastQueue.remove(index); } keepProcessAliveIfNeededLocked(record.pid); if (mToastQueue.size() > 0) { record = mToastQueue.get(0); } else { record = null; } } } }
上面的代碼很簡單,Toast 的顯示通過 ToastRecord 的 callback 來完成,這個callback 實(shí)際上就是 Toast 的 TN 對象的遠(yuǎn)程 Binder,通過 callback 來訪問 TN 中的方法是需要跨進(jìn)程來完成的,最終被調(diào)用的 TN 中的方法會運(yùn)行在發(fā)起 Toast 請求的應(yīng)用的 Binder 線程池中。
Toast 顯示之后,NMS 會通過 scheduleTimeoutLocked 方法發(fā)送延時消息,具體延時取決于 Toast 的時長:LONG_DELAY 是 3.5s,SHORT_DELAY 是 2s
-
private void scheduleTimeoutLocked(ToastRecord r) { mHandler.removeCallbacksAndMessages(r); Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r); long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY; mHandler.sendMessageDelayed(m, delay); }
延時時間過后,NMS 通過 cancelToastLocked 方法隱藏 Toast 并將其從 mToastQueue 中移除,此時如果 mToastQueue 中還有其他 Toast,那么 NMS 就繼續(xù)顯示其他 Toast。
void cancelToastLocked(int index) { ToastRecord record = mToastQueue.get(index); try { record.callback.hide(); } catch (RemoteException e) { Slog.w(TAG, "Object died trying to hide notification " + record.callback + " in package " + record.pkg); // don't worry about this, we're about to remove it from // the list anyway } ToastRecord lastToast = mToastQueue.remove(index); mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY); keepProcessAliveIfNeededLocked(record.pid); if (mToastQueue.size() > 0) { // Show the next one. If the callback fails, this will remove // it from the list, so don't assume that the list hasn't changed // after this point. showNextToastLocked(); } }
再分析 Toast 的隱藏過程(cancel 方法)
-
同樣是通過 ToastRecord 的 callback 完成的,同樣是一次 IPC 過程,其工作過程和 Toast 的顯示過程是類似的
try { record.callback.hide(); } catch (RemoteException e) { Slog.w(TAG, "Object died trying to hide notification " + record.callback + " in package " + record.pkg); // don't worry about this, we're about to remove it from // the list anyway }
再看 TN 類:
通過上面的分析,Toast 的顯示和隱藏實(shí)際上是通過 Toast 的 TN 這個類來實(shí)現(xiàn)的,它有兩個方法 show 和 hide,分別對應(yīng) Toast 的顯示和隱藏。這兩個方法運(yùn)行在 Binder 線程池中,內(nèi)部使用了 Handler。
/**
* schedule handleShow into the right thread
*/
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.obtainMessage(HIDE).sendToTarget();
}
上面代碼中,分別發(fā)送了 SHOW 和 HIDE 兩種消息,在 handleMessage 中分別調(diào)用 handleShow 和 handleHide 方法,這二者才是真正完成顯示和隱藏 Toast 的地方。
- handleShow 中 addView 方法將 Toast 的視圖添加到 Window 中
- handleHide 中調(diào)用 remoteView 方法將 Toast 的視圖從 Window 中移除
到這里 Toast 地 Window 的創(chuàng)建過程就分析完了。
本章結(jié)束。