Android-Window(一)——初識Window
學習自
- 《Android開發藝術探索》
- blog.csdn.net/qian520ao/a… CSDN博主--凶殘的程式設計師
- blog.csdn.net/tongsiw/art…
開發環境
- SDK 24
- AS 3.0.1
- Everything 工具 用來快速查詢無法直接通過AS找到的類檔案
UI先行
Notion to say, UI對於一個APP的成功與否實在是太重要了,我想畢竟大多數人都是一個顏控。當然我也不例外,在我們選擇一軟體產品的時候,我們肯定會選擇比較漂亮的哪一款。 在Android中漂亮的UI都是由各種各樣的View來組成的,在很多時候,我們View都呈現在Activity上。 提到了View那麼View的新增、更新和刪除是由誰負責的呢? 如果你的回答是Activity,請你試想一
下,不僅僅是Activity可以顯示在螢幕上,Dialog、Toast,同樣也可以顯示在螢幕上。那麼是不是他們都各自管理著自己的View呢? 這樣顯然是不符合設計的!所以有必要讓一個具有普適性的 東西
來承載View了。 這個 東西
就是我們今天的主角——Window
.
Window是一個抽象的視窗的概念,負責承載檢視,不管是Activity的檢視,但是Dialog或者Toast的檢視都是由Window來承擔的。就像在Activity中呼叫的 setContentView
其中呼叫的Window的 setContentView 方法。可以這理解--Window是View的直接管理者。
WindowManager
要想知道,Window的工作原理,我們需要知道先怎麼新增一個Window,要先新增Window需要通過WindowManager來完成。而WindowManager是一個介面繼承自 ViewManager
。其中有三個主要的方法:
- addView 負責穿件Window並向其中新增View
- updateViewLayout 更新Window中View的佈局
- remoteView 通過移除Window中的View可以實現移除Window
class MainActivity : AppCompatActivity() {
lateinit var mButton: Button
lateinit var mLayoutParams: WindowManager.LayoutParams
lateinit var mWindowManager: WindowManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
addWindow()
}
/**
* 新增一個Window
* */
private fun addWindow() {
this.mButton = Button(this)
this.mButton.text = "Hello Button"
this.mLayoutParams = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_APPLICATION,
0,
PixelFormat.TRANSPARENT)
this.mWindowManager = this.windowManager
this.mLayoutParams.flags =
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
this.mLayoutParams.gravity = Gravity.TOP or Gravity.LEFT
this.mLayoutParams.x = 100
this.mLayoutParams.y = 300
this.mWindowManager.addView(this.mButton, this.mLayoutParams)
}
}
複製程式碼
當Activity啟動以後,我們可以發現我們定義的Button已經顯示在螢幕上,因此我們已經做了一些設定,所以此介面還會顯示在鎖屏介面。但是當我們嘗試去關閉此介面的時候,列印出了以下的Log:
android.view.WindowLeaked: Activity cn.shycoder.studywindow.MainActivity has leaked window android.widget.Button{9d337cc VFED..C.. ......ID 0,0-325,126} that was originally added here
這是因為當Activity已經銷燬的時候,但是Activity中的我們剛才手動建立的Window還沒有被銷燬,造成了Window的記憶體洩漏。為了避免出現這種情況需要在Activity的 onDestroy
方法中將我們建立的Window移除掉。
override fun onDestroy() {
super.onDestroy()
this.mWindowManager.removeViewImmediate(this.mButton)
}
複製程式碼
上面的新增Window的程式碼中,涉及到了幾個比較重要的引數,我們依次來看一看。
Flag 參數列示Windows的屬性,有很多不同的選項,通過這些選項可以控制Window的表現特性。
FLAG_NOT_FOCUSABLE
表示Window不需要獲取焦點,也不需要接收各種輸入事件,啟用此FLAG的時候,也會隱式啟用 NOT_TOUCH_MODAL
標籤,最終事件會直接傳遞給下層具有焦點的Window。
FLAG_NOT_TOUCH_MODAL 在此模式在,系統會將當前Window區域以外的點選事件傳遞給底層的Window,當前區域內的點選事件則自己處理。一般都需要開啟此FLAG。
FLAG_SHOW_WHEN——LOCKED 開啟此模式會將Widow顯示在鎖屏介面上。
Type Type 屬性表示的Window的型別,一種分為三種 應用Window(我們的Activity中的Window),子Window(Dialog就是子Window),系統 Window (Toast的Window就是一種系統Window)。其中子Window 比較特殊,必須依附於一個父Window中。如果你想要建立系統級的Window的話,得需要有相應的許可權才行。
Window是分層的,每一個Window都有著對應的 z-ordered
, 層級大的Window會覆蓋在層級小的Window上面。
- 應用層級Window 1-99
- 子Window 1000-1999
- 系統Window 2000-2999
Window新增的過程
每一個Window都對應著一個View(一般是DecorView)和一個RootViewImpl,Window和View之間通過RootViewImpl來建立連線,View是Window的具體呈現。
在上面呢, 我們通過了WindowManager新增了一個Window,現在我們來分析一下Window新增的流程,當你瞭解了這個流程之後,就會明白為什麼WindowManager呼叫了 addView
方法,卻一直在說新增一個Window,而不是新增一個View了。
我們知道WindowManager 是一個介面其中並沒有實現,WindowManager的實現類是 WindowManagerImpl
,我們先來看這個類。
public final class WindowManagerImpl implements WindowManager {
//....
@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);
}
//....
}
複製程式碼
可以看到,WindowManagerImpl類也沒有直接實現WindowManager,而是通過呼叫 mGlobal
的方法來實現,而mGlobal變數是一個 WindowManagerGlobal
。在宣告此成員變數的時候酒同時也進行了初始化(以單例的形式)。
在看addView的原始碼的時候,我們先來看一下WindowManagerGlobal 類中幾個非常重要的集合變數:
private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams =
new ArrayList<WindowManager.LayoutParams>();
private final ArraySet<View> mDyingViews = new ArraySet<View>();
複製程式碼
- mViews 存放了Window所對應的View
- mRoots 存放了所有Window的ViewRootImpl
- mParams 存放了所有Window的佈局引數
- mDyingViews 存在的是正在被刪除的View
我們來看一看WindowManagerGlobal是如何實現addView方法的。
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
//首先是一通引數合法性檢查
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
//判斷params物件是否是WindowManager.LayoutParams
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
//如果parentWindow不為null則表示,當前要建立的Window是一個子Window
//通過adjustLayoutParamsForSubWindow方法做一些額外的引數調整
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
// If there's no parent, then hardware acceleration for this view is
// set from the application's hardware acceleration setting.
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// Start watching for system property changes.
if (mSystemPropertyUpdater == null) {
mSystemPropertyUpdater = new Runnable() {
@Override public void run() {
synchronized (mLock) {
for (int i = mRoots.size() - 1; i >= 0; --i) {
mRoots.get(i).loadSystemProperties();
}
}
}
};
SystemProperties.addChangeCallback(mSystemPropertyUpdater);
}
int index = findViewLocked(view, false);
if (index >= 0) {
if (mDyingViews.contains(view)) {
// Don't wait for MSG_DIE to make it's way through root's queue.
mRoots.get(index).doDie();
} else {
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
}
// The previous removeView() had not completed executing. Now it has.
}
// If this is a panel window, then find the window it is being
// attached to for future reference.
if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
final int count = mViews.size();
for (int i = 0; i < count; i++) {
if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
panelParentView = mViews.get(i);
}
}
}
//例項化ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
//一頓新增集合的操作
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// 將View顯示出來
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
複製程式碼
setView
在此方法中完成了Window的新增的過程。該方法的程式碼量非常多,現在只來看一下主要的程式碼:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
//....
//非同步請求重新整理佈局
//通過此方法會進行一系列的操作最終會完成View的 measure -> layout -> draw 的三大流程
requestLayout();
try {
//通過 mWindowSession 完成新增Window的操作
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);
} finally {
if (restore) {
attrs.restore();
}
}
//.....
}
複製程式碼
WindowSession
在上面的程式碼中,瞭解到setView方法最終通過 MWindowSession 完成了Window的新增,那麼它是什麼呢?
mWindowSession 的型別是Binder,其實現類是Session,從Session類我們可以推斷出新增Window的過程一個 IPC(跨程式通訊)
的過程。
@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
Rect outOutsets, InputChannel outInputChannel) {
//呼叫WindowManagerService的addWindow方法
//在addWindow方法中將會完成Window的新增
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
outContentInsets, outStableInsets, outOutsets, outInputChannel);
}
複製程式碼
關於WindowManagerService這個類,因為博主水平有限,這裡就不在瞎叨叨了。大家可以看一下 文章頂部的連結中的原始碼的分析。
從上面的一系列的原始碼中,我們發現,雖然是呼叫了WindowManager類的addView方法,其實最終還是新增了一個Window。不要被方法名稱所迷惑。
類圖
通過對上面Window新增的過程,我們可以得出以下的類圖, 本圖片是從 凶殘的程式設計師 大神的部落格中模仿來的.
Window刪除的過程
前面我們已經分析了,WindowManager的實現類是 WindowManagerImpl
而在 WindowManagerImpl 則是呼叫了WindowManagerGlobal來實現,這裡我們直接來看WindowManagerGlobal的 removeView
方法。
public void removeView(View view, boolean immediate) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
synchronized (mLock) {
//找到待刪除的View的索引
int index = findViewLocked(view, true);
//獲取到要刪除的ViewRootImpl
View curView = mRoots.get(index).getView();
//刪除View
removeViewLocked(index, immediate);
if (curView == view) {
return;
}
throw new IllegalStateException("Calling with view " + view
+ " but the ViewAncestor is attached to " + curView);
}
}
複製程式碼
在上面的程式碼中,獲取到了要溢位的View的索引,飯後通過呼叫 removeViewLocked
方法:
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());
}
}
//通過RootViewImpl物件傳送一個刪除View的請求
boolean deferred = root.die(immediate);
if (view != null) {
view.assignParent(null);
//新增到待刪除的View集合中
if (deferred) {
mDyingViews.add(view);
}
}
}
複製程式碼
上面的主要的程式碼就是通過 ViewRootImpl
物件的 die
方法來刪除 View,其分為兩種情況 一種是 立即刪除(同步的) 一種是傳送訊息等待刪除(非同步)。我們來看一下die方法的程式碼。
/**
* @param immediate True, do now if not in traversal. False, put on queue and do later.
* @return True, request has been queued. False, request has been completed.
*/
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.
//如果是同步刪除的話直接呼叫 doDie方法完成刪除操作
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());
}
//如果是非同步的刪除操作,那麼傳送一個訊息,交給Handler去處理
mHandler.sendEmptyMessage(MSG_DIE);
return true;
}
複製程式碼
從上面的程式碼中,我們可以清晰地看到,會通過兩種同步和非同步的方式來刪除View。而不管是同步或者非同步,刪除View的時候都是呼叫的 doDie
方法。 我們來看一看doDie方法中做了些什麼工作。
void doDie() {
checkThread();
if (LOCAL_LOGV) Log.v(mTag, "DIE in " + this + " of " + mSurface);
synchronized (this) {
if (mRemoved) {
return;
}
mRemoved = true;
//移除View
if (mAdded) {
//真正移除View的邏輯是在此方法中
dispatchDetachedFromWindow();
}
if (mAdded && !mFirst) {
destroyHardwareRenderer();
if (mView != null) {
int viewVisibility = mView.getVisibility();
boolean viewVisibilityChanged = mViewVisibility != viewVisibility;
if (mWindowAttributesChanged || viewVisibilityChanged) {
// If layout params have been changed, first give them
// to the window manager to make sure it has the correct
// animation info.
try {
if ((relayoutWindow(mWindowAttributes, viewVisibility, false)
& WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
mWindowSession.finishDrawing(mWindow);
}
} catch (RemoteException e) {
}
}
mSurface.release();
}
}
mAdded = false;
}
//將相關物件從WindowManagerGlobal的幾個列表中刪除
WindowManagerGlobal.getInstance().doRemoveView(this);
}
複製程式碼
從doDie的原始碼中,我們可以瞭解到, 移除View的真正的邏輯在 dispatchDetachedFromWindow
方法中,我們來看看此方法。
void dispatchDetachedFromWindow() {
if (mView != null && mView.mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);
//呼叫View的dispatchDetachedFromWindow 方法,在此方法中會進行資源的回收
mView.dispatchDetachedFromWindow();
}
//一頓回收資源
mAccessibilityInteractionConnectionManager.ensureNoConnection();
mAccessibilityManager.removeAccessibilityStateChangeListener(
mAccessibilityInteractionConnectionManager);
mAccessibilityManager.removeHighTextContrastStateChangeListener(
mHighContrastTextManager);
removeSendWindowContentChangedCallback();
destroyHardwareRenderer();
setAccessibilityFocus(null, null);
mView.assignParent(null);
mView = null;
mAttachInfo.mRootView = null;
mSurface.release();
if (mInputQueueCallback != null && mInputQueue != null) {
mInputQueueCallback.onInputQueueDestroyed(mInputQueue);
mInputQueue.dispose();
mInputQueueCallback = null;
mInputQueue = null;
}
if (mInputEventReceiver != null) {
mInputEventReceiver.dispose();
mInputEventReceiver = null;
}
try {
//通過Session呼叫WinderManagerService完成移除Window的操作
//一次IPC
mWindowSession.remove(mWindow);
} catch (RemoteException e) {
}
// Dispose the input channel after removing the window so the Win
// doesn't interpret the input channel being closed as an abnorma
if (mInputChannel != null) {
mInputChannel.dispose();
mInputChannel = null;
}
mDisplayManager.unregisterDisplayListener(mDisplayListener);
unscheduleTraversals();
}
複製程式碼
上面的程式碼比較長,但是大部分都是一些回收各種資源的程式碼,這些程式碼不需要我們太過注意。其中有兩點需要我們注意的是 mView.dispatchDetachedFromWindow(); 這個方法將會對View所佔用的資源進行回收。不同的View可能會對此方法有不同的實現,比如說ViewGroup的實現是 先呼叫子View的 dispatchDetachedFromWindow 方法 回收子View的資源,然後在回收自己所佔用的資源。 在 View.dispatchDetachedFromWindow 方法中呼叫了 onDetachedFromWindow
方法,這個方法你可比較熟悉,因為當View從Window中被移除的時候,此方法會被呼叫, 我們通常會重寫這個方法做一些回收資源的事情。 比如說:停止動畫和執行緒等。
當呼叫完View的dispatchDetachedFromWindow方法後,緊接著就是一些釋放資源的操作,這些我們不用管,在釋放完資源後,就通過 mWindowSession.remove
呼叫了 WindowManagerService的移除Window的方法,哦,請不要忘記這是一個IPC的操作。
接著回到 doDie
方法,在doDie 方法的最後 呼叫了 WindowManagerGlobal的 doRemoveView
方法,將相關的內容從各個集合中移除掉。至此移除Window的過程結束。
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 WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
view.setLayoutParams(wparams);
synchronized (mLock) {
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index);
//將原來的LayoutParams移除掉
mParams.remove(index);
//將新的LayoutParams新增到原理的位置
mParams.add(index, wparams);
//通過ViewRootImpl來進行更新
root.setLayoutParams(wparams, false);
}
}
複製程式碼
方法很簡單,僅僅是設定了一下新的LayoutParams,然後呼叫 ViewRootImpl的 setLayoutParams 方法。
setLayoutParams方法程式碼比較多,但都是一些根據LayoutParams進行各種設定的程式碼,這裡就不貼出來了,在setLayoutParams方法中比較重要的是呼叫了 scheduleTraversals
方法來對View進行重新佈局,該方法最終會呼叫 performTraversals
進行 View的 measure layout draw 三大過程。通知在方法中也會呼叫 relayoutWindow
方法,在該方法中會呼叫 Session 的 relayout
方法,最終會通過呼叫 WindowManagerService 的 relayoutWindow
方法來完成 Window的更新。這依然是一個IPC操作。
總結
最後,如果讀者你能夠看到這裡,那我得十分感謝你能夠忍受我這枯燥的文風,和我那三流的技術水平。本章的內容就到這了,如果本章的內容能夠幫助你對Window有一個大致的瞭解,那對我來說真是一個值得高興的事情,本文對Window的新增、刪除和更新的流程做了一個簡要的概覽,並不是深入的分析其實現,主要目的是瞭解這些過程的脈絡。Window是一個比較難的知識點,如果文中有任何紕漏,還望斧正。