第8章 理解Window和WindowManager

Window表示一個(gè)窗口,是View的實(shí)際管理者。在第4章的事件分發(fā)中已經(jīng)知道了,點(diǎn)擊事件是通過Window->DecorView->View來傳遞的。

Window是一個(gè)抽象類,具體實(shí)現(xiàn)是PhoneWindow類。我們可以通過WindowManager來操作Window,具體實(shí)現(xiàn)是在WindowManagerService中實(shí)現(xiàn)的;WindowManager和WindowManagerService通過IPC連接。

8.1 Window和WindowManager

8.1.1 創(chuàng)建一個(gè)Window

首先,通過WindowManager添加一個(gè)窗口:

        Button button = new Button(this);
        button.setText("button");
        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.TYPE_APPLICATION, 0,
                PixelFormat.TRANSPARENT);
        lp.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
        lp.gravity = Gravity.LEFT | Gravity.TOP;
        lp.x = 100;
        lp.y = 300;
        WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
        wm.addView(button, lp);

這里注意,原書中代碼報(bào)錯(cuò),必須給窗口指定一個(gè)type,我這里指定的WindowManager.LayoutParams.TYPE_APPLICATION;不清楚是否是系統(tǒng)版本的問題。

8.1.2 Type

Type有三種類型:

  • 應(yīng)用Window
    應(yīng)用類Window對(duì)應(yīng)一個(gè)Activity。取值在1~99之間。

  • 子Window
    子Window附屬于一個(gè)特定的父Window,而不能單獨(dú)存在。取值在1000~1999之間。

  • 系統(tǒng)Window
    系統(tǒng)Window需要系統(tǒng)權(quán)限才能創(chuàng)建。取值在2000~2999之間。

8.1.3 Flag

Flag參數(shù)表示W(wǎng)indow的屬性,可以控制Window的顯示特性。

  • FLAG_NOT_FOCUSABLE
    表示W(wǎng)indow不需要獲取焦點(diǎn),也不需要接受各種輸入事件。

  • FLAG_NOT_TOUCH_MODAL
    表示W(wǎng)indow會(huì)處理當(dāng)前區(qū)域以內(nèi)的單擊事件,同時(shí)把當(dāng)前Window區(qū)域以外的單擊事件傳遞到底層。

  • FLAG_SHOW_WHEN_LOCKED
    表示W(wǎng)indow可以顯示在鎖屏的界面上。

8.1.4 權(quán)限

想要顯示系統(tǒng)級(jí)的Window是需要申請(qǐng)權(quán)限的。例如當(dāng)type = TYPE_SYSTEM_ERROR的時(shí)候,需要在AndroidManifest中添加<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

在Android 6.0及以后,需要手動(dòng)開啟懸浮窗權(quán)限:

    /**
     * 檢查是否有懸浮窗權(quán)限
     */
    @TargetApi(Build.VERSION_CODES.M)
    private void checkOverlay() {
        if (!Settings.canDrawOverlays(this)) {
            AlertDialog dialog = new AlertDialog.Builder(this)
                    .setTitle("權(quán)限禁止")
                    .setMessage("懸浮窗權(quán)限被禁止,點(diǎn)擊確定前往設(shè)置")
                    .setPositiveButton("確定", 
                        (dialog1, which) -> jumpToOverlaySetting())
                    .setCancelable(false)
                    .create();
            dialog.show();
        }
    }

    private void jumpToOverlaySetting() {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
            startActivityForResult(intent, 1);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == 1) {
            checkOverlay();
        }
    }

然后在onActivityResult()中,再判斷是否開啟權(quán)限。

如果不想申請(qǐng)權(quán)限,使用type = TYPE_TOAST也可以創(chuàng)建一個(gè)懸浮窗。但是在Android 7.1.1(API 25)開始,TYPE_TOAST被限制使用[1]。如果想要在7.1.1以后使用懸浮窗,必須開啟權(quán)限。

8.2 Window的內(nèi)部機(jī)制

Window是一個(gè)抽象概念,而不是實(shí)際存在的。每一個(gè)Window都對(duì)應(yīng)著一個(gè)View和一個(gè)ViewRootImpl,Window和View是通過ViewRootImpl來建立聯(lián)系的。Window實(shí)際存在的形式是View。
對(duì)Window的操作只能通過WindowManager來進(jìn)行。WindowManager主要提供了三個(gè)方法:

  • addView(View, LayoutParams)
  • removeView(View)
  • updateView(View, LayoutParams)

可以看到,WindowManager的操作對(duì)象是View。

8.2.1 Window的添加過程

在WindowManager的實(shí)現(xiàn)類WindowManagerImpl中,找到addView()方法:

    public void addView(View view, ViewGroup.LayoutParams params) {
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

    @Override
    public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
        mGlobal.updateViewLayout(view, params);
    }

    @Override
    public void removeView(View view) {
        mGlobal.removeView(view, false);
    }

而mGlobal的定義:

    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

這個(gè)單例類,在第四章[2]的時(shí)候已經(jīng)接觸過了。這里我們看到,WindowManager的添加、刪除、更新三個(gè)方法,其實(shí)都全權(quán)交給了WindowManagerGlobal。

轉(zhuǎn)到WindowManagerGlobal的addView()方法中:

    public void addView(View view, 
        ViewGroup.LayoutParams params, 
        Display display, 
        Window parentWindow) {
        // ... 省略若干代碼

        // 創(chuàng)建ViewRoot
        ViewRootImpl root;
        root = new ViewRootImpl(view.getContext(), display);

        view.setLayoutParams(wparams);

        // 將View添加到列表中
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            if (index >= 0) {
                removeViewLocked(index, true);
            }
            throw e;
         }
    }

這里主要是將待添加的View、ViewRoot、LayoutParams保存到列表中,創(chuàng)建了ViewRoot,以及調(diào)用了ViewRoot的setView方法。
接下來繼續(xù)跟蹤setView方法。

    public void setView(View view, 
          WindowManager.LayoutParams attrs, 
          View panelParentView) {
          // ...省略若干代碼

          // Schedule the first layout -before- adding to the window
          // manager, to make sure we do the relayout before receiving
          // any other events from the system.
          requestLayout();
          // ...
          res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                   getHostVisibility(), mDisplay.getDisplayId(),
                   mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                   mAttachInfo.mOutsets, mInputChannel);
          // ...
    }

requestLayout()之前有一個(gè)注釋:

// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.

大概意思是說,為了保證在接收到其他系統(tǒng)事件之前布局好,我們?cè)趯iew添加到Window之前進(jìn)行第一次的布局。

然后調(diào)用了WindowSession.addToDisplay()方法。mWindowSession是通過WindowManagerGlobal.getWindowSession()來獲取的,而跟蹤進(jìn)去發(fā)現(xiàn)發(fā)現(xiàn)這是一個(gè)Binder對(duì)象,進(jìn)行了和WindowManagerService之間的IPC。也就是說,Window的添加最后交給了WindowManagerService來進(jìn)行。

8.2.2 Window的刪除過程

Window的刪除的過程和添加類似,通過WindowManager.removeView() -> ViewRoot.die() -> WindowSession.remove()最終交付WindowManagerService來處理。
在這個(gè)過程中,會(huì)調(diào)用View的onDetachedFromWindow()回調(diào)。然后會(huì)調(diào)用WindowManager.doRemoveView()來將之前添加到列表中的View、ViewRoot、LayoutParams(8.2.1)給刪掉。

8.2.3 Window的更新過程

更新過程相較添加和刪除比較簡單,WindowManager.updateViewLayout() -> ViewRoot.setLayoutParams() -> ViewRoot.scheduleTraversals()
最終會(huì)調(diào)用ViewRoot.performTraversals()方法,在這里會(huì)對(duì)View進(jìn)行measure、layout、draw的步驟。同時(shí)會(huì)調(diào)用relayoutWindow -> WindowSession.relayoutWindow(),通過WindowSession來更新Window視圖,這個(gè)過程仍然是經(jīng)過IPC,由WindowManagerService來實(shí)現(xiàn)的。

8.2.* 小結(jié)

  • Window是一個(gè)抽象概念,實(shí)際上是不存在的,通過View來體現(xiàn)。View不能單獨(dú)存在,而必須依托于Window;而Window的具體呈現(xiàn)方式則是通過View。
  • 一個(gè)Window對(duì)應(yīng)一個(gè)ViewRoot對(duì)應(yīng)一個(gè)View,ViewRoot是WindowManager和View之間的紐帶。而ViewRoot所代表的意義,則是這個(gè)Window的ViewTree的根節(jié)點(diǎn)。
    WindowManager ----> ViewRoot --(IPC)-> WindowManagerService 這是操作Window的一般流程。而所有關(guān)于Window的操作,最終都是在WindowManagerService中實(shí)現(xiàn)的。

8.3 Window的創(chuàng)建過程

8.3.1 Activity的Window

在第四章里面,我分析過Activity的啟動(dòng)流程:第4章 View的工作原理

當(dāng)系統(tǒng)將要啟動(dòng)一個(gè)Activity的時(shí)候,會(huì)調(diào)用ActivityThread.performLaunchActivity()方法,在這個(gè)方法中會(huì)創(chuàng)建Activity實(shí)例,并調(diào)用Activity的attach方法。在attach方法中,會(huì)賦予Activity上下文,創(chuàng)建Window并將其和Activity綁定。Activity實(shí)現(xiàn)了Window.Callback接口,所以Activity可以收到Window的若干回調(diào),比如onAttachedToWindow、onDetachedFromWindow、dispatchTouchEvent等。

Window實(shí)例的創(chuàng)建,是通過PolicyManager的工廠方法makeNewWindow(Context)來進(jìn)行的。
當(dāng)我們?cè)贏ctivity調(diào)用setContentView方法時(shí),會(huì)創(chuàng)建DecorView并將xml的布局加載進(jìn)content中。DecorView雖然已經(jīng)創(chuàng)建完畢,但是他還沒有被WindowManager添加到Window中。Window的概念,更多表示的是一種抽象的功能集合。在Window和DecorView都創(chuàng)建完畢之后,ActivityThread會(huì)調(diào)用handleResumeActivity方法->makeVisible方法,在其中會(huì)調(diào)用WindowManager.addView(),這就回到了8.2.1的流程了,通過addView將DecorView添加到Window。
(有點(diǎn)吃驚,書中的內(nèi)容和我之前在第四章中分析的幾乎一模一樣,不過這也說明我沒有誤入歧途。)

8.3.2 Dialog的Window

Dialog和Activity創(chuàng)建Window的方式類似。

1、創(chuàng)建Window
2、初始化DecorView并將Dialog的視圖添加進(jìn)去
3、將DecorView添加到Window中

在Dialog被關(guān)閉時(shí),他會(huì)通過WindowManager來移除DecorView。

8.3.3 Toast的Window

Toast也是基于Window來實(shí)現(xiàn)的,不過過程和Dialog不同。
總的來說,Toast是通過IPC過程調(diào)用NotificationManagerService來進(jìn)行顯示和隱藏的。

    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
        }
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容