Android-Window(一)——初識Window

魯迅認識的那隻猹發表於2018-09-12

Android-Window(一)——初識Window

學習自

開發環境

  • 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新增的過程,我們可以得出以下的類圖, 本圖片是從 凶殘的程式設計師 大神的部落格中模仿來的.

2018-09-11_19-03-24

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是一個比較難的知識點,如果文中有任何紕漏,還望斧正。