開發(fā)藝術(shù)之Window

Window 表示窗口,可以實(shí)現(xiàn)懸浮窗效果。Window 是一個(gè)抽象類,它的具體實(shí)現(xiàn)是 PhonewWindow。創(chuàng)建一個(gè)Window 是很簡(jiǎn)單的事,只需要通過 WindowManager 即可完成。WindowManager 是外界訪問 Window 的入口,Window 的具體實(shí)現(xiàn)位于 WindowManagerService,WindowManager 和 WindowManagerSevcie 的交互是一個(gè) IPC 過程。

一、WIndow 和 WindowManager

通過 WindowManager 添加 Window,下面代碼將一個(gè) Button 添加到屏幕坐標(biāo)為 (100, 300) 的位置上:

    private void alterWindow() {
        mFloatingButton = new Button(this);
        mFloatingButton.setText("button");
        mLayoutParams = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT,
                0, 0, PixelFormat.TRANSPARENT
        );
        mLayoutParams.gravity = Gravity.START | Gravity.TOP;
        mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        mLayoutParams.x = 100;
        mLayoutParams.y = 300;
        mFloatingButton.setLayoutParams(mLayoutParams);
        mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
        mWindowManager.addView(mFloatingButton, mLayoutParams);
    }

其中 LayoutParams 中的 flags 和 type 這兩個(gè)參數(shù)比較重要:

  • Flags 參數(shù)表示 Window 的屬性,可以控制 Window 的顯示特性,常用選項(xiàng)如下:
    • FLAG_NOT_FOCUSABLE,表示 Window 不需要獲取焦點(diǎn),最終事件會(huì)直接傳遞給下層具有焦點(diǎn)的 Window。
    • FLAG_NOT_TOUCH_MODAL,此模式下系統(tǒng)會(huì)將當(dāng)前 Window 區(qū)域以外的單擊事件傳遞給底層 Window,當(dāng)前 Window 區(qū)域以內(nèi)的單擊事件則自己處理。
    • FLAG_SHOW_WHEN_LOCKED,此模式可以讓 WIndow 顯示在鎖屏的界面上。
  • Type 參數(shù)表示 Window 的類型,可分為三類:
    • 應(yīng)用 Window,對(duì)應(yīng) Activity
    • 子 Window,不能單獨(dú)存在,需要附屬在特定的父 Window 之中,比如 Dialog
    • 系統(tǒng) WIndow需要權(quán)限才能創(chuàng)建的 Window,比如 Toast 和系統(tǒng)狀態(tài)欄

Window 的層級(jí)概念:

  1. 每個(gè) Window 都有對(duì)應(yīng)的 z-ordered,層級(jí)大的會(huì)覆蓋在層級(jí)小的上面。

  2. 應(yīng)用 Window 層級(jí)范圍是 1-99,子 WIndow 層級(jí)范圍是 1000-1999,系統(tǒng) Window 層級(jí)范圍是 2000-2999

  3. 可以指定 type 屬性為 TYPE_APPLICATION_OVERLAY,讓它處于系統(tǒng)層級(jí),同時(shí)注意權(quán)限問題,如下所示:

    if (!Settings.canDrawOverlays(this)) {
     Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,  Uri.parse("package:" + getPackageName()));
                startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
            } else {
                alterWindow();
            }
    

WindowManager 繼承于 ViewManager,可以實(shí)現(xiàn)往 Window 中添加View、更新 View、刪除 View 的功能:

public interface ViewManager
{
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

可以為 Window 中的 View 設(shè)置 onTouchlistener 監(jiān)聽,來實(shí)現(xiàn)拖動(dòng)效果。


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

從 WindowManager 的三個(gè)方法可以看出作用對(duì)象都是 View,所以 Window 實(shí)際上是以 View 的形式存在的。每一個(gè) Window 都對(duì)應(yīng)一個(gè) View 和一個(gè) ViewRootImpl,Window 和 View 通過 ViewRootImpl 來建立連接。

WindowManager 是一個(gè)接口,所以它的 addView 方法由它的實(shí)現(xiàn)類 WindowManagerImpl 類實(shí)現(xiàn),其他兩個(gè)方法同理:

    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

    @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 沒有直接實(shí)現(xiàn)這三個(gè)方法,而是將操作委托給了 WindowManagerGlobal 對(duì)象。

1、Window 的添加過程

WindowManagerGlobal 的 addView 方法分為下面幾步:

  • 檢查參數(shù)是否合法,如果是子 Window 還需要調(diào)整布局參數(shù)
        // WindowManagerGlobal#addView
        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");
        }
  • 創(chuàng)建 ViewRootImp 并將 View 添加到列表中
    // WindowManagerGlobal
    // 存儲(chǔ)所有 Window 對(duì)應(yīng)的 View
    private final ArrayList<View> mViews = new ArrayList<View>();
    // 存儲(chǔ)所有 Window 對(duì)應(yīng)的 ViewRootImpl
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
    // 存儲(chǔ)所有 Window 對(duì)應(yīng)的布局參數(shù)
    private final ArrayList<WindowManager.LayoutParams> mParams =
            new ArrayList<WindowManager.LayoutParams>();
            // WindowManagerGlobal#addView
            ViewRootImpl root;
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
  • 通過 ViewRootImpl 來更新界面
    // WindowManagerGlobal#addView
    root.setView(view, wparams, panelParentView);
    // ViewRootImpl#setView
    requestLayout();

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals(); 
        }
    }
    // ViewRootImpl#scheduleTraversals

    // View 繪制入口,通過 IPC 過程最終會(huì)調(diào)用到下面 WindowManagerService 的 addWindow 方法
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
// WindowManagerService
addWindow(Session session, IWindow client, int seq,
            LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
            Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
            DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
            InsetsState outInsetsState)

如此一來,Window 添加 View 的請(qǐng)求就交給了 WindowManagerService。

2、Window 的刪除過程

同添加過程一樣,刪除過程也是先通過 WindowManagerImpl,再進(jìn)一步通過 WindowManagerGlobal 來實(shí)現(xiàn)的,實(shí)現(xiàn)如下:

    // WindowManagerGlobal
    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 的索引,然后通過 removeViewLocked 來做進(jìn)一步的刪除。

    // WindowManagerGlobal
    private void removeViewLocked(int index, boolean immediate) {
        ViewRootImpl root = mRoots.get(index);
        View view = root.getView();

        if (view != null) {
            InputMethodManager imm = view.getContext().getSystemService(InputMethodManager.class);
            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);
            }
        }
    }

上面代碼中先調(diào)用 die 方法,然后將 view 添加到 mDyingViews 中,表示待刪除的 View 列表。其中 die 方法如下所示:

    // ViewRootIml
    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;
    }

如果是異步刪除,那么會(huì)發(fā)送一個(gè) MSG_DIE 消息,ViewRootImpl 中的 Handler 就會(huì)調(diào)用 doDie 方法;如果是立刻刪除,那么就直接調(diào)用 doDie 方法。

在 doDie 內(nèi)部會(huì)調(diào)用 dispatchDetachedFromWindow 方法,真正刪除 View 的邏輯就在該方法內(nèi)部實(shí)現(xiàn),該方法主要做四件事:

  • 垃圾回收相關(guān)工作,比如清除數(shù)據(jù)和消息、移除回調(diào)
  • 通過 Seesion 的 remove 方法刪除 Window,這也是一個(gè) IPC 過程
  • 調(diào)用 View 的 dispatchDetachedFromWindow 方法,做一些資源回收的工作,比如終止動(dòng)畫、停止線程
  • 調(diào)用 WindowManagerGlobal 的 doRemoveView 方法刷新數(shù)據(jù),把 mRoots、mParams 以及 mDyingViews 中關(guān)聯(lián)當(dāng)前 Window 的對(duì)象從列表中刪除
3、Window 的更新過程
    // WindowManagerGlobal
    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 的 LayoutParams
        view.setLayoutParams(wparams);

        synchronized (mLock) {
            int index = findViewLocked(view, true);
            ViewRootImpl root = mRoots.get(index);
            // 更新該 Window 存儲(chǔ)的布局參數(shù)列表
            mParams.remove(index);
            mParams.add(index, wparams);
            // 更新 ViewRootImpl 的 LayoutParams,
            root.setLayoutParams(wparams, false);
        }
    }

在此方法中會(huì)調(diào)用 ViewRootImpl 的 scheduleTraversals 方法,對(duì) View 重新布局,包括測(cè)量、布局、重繪三個(gè)過程。它還會(huì)通過 WindowSession 來更新 Window 的視圖,它同樣是一個(gè) IPC 過程。


三、Window 的創(chuàng)建過程

View 是 Android 中的視圖的呈現(xiàn)方式,但是 View 不能單獨(dú)存在,它必須依附在 Window 這個(gè)抽象的概念上。Android 中可以提供視圖的地方有 Activity、Dialog、Toast,有視圖的地方就有 Window。

1、Activity 的 Window 創(chuàng)建過程

Activity 中的 Window 創(chuàng)建過程涉及到 Activity 的啟動(dòng)過程,最終啟動(dòng)過程是由 ActivityThread 中的 performLaunchActivity 方法完成的,在這個(gè)方法內(nèi)部會(huì)通過類加載器創(chuàng)建 Activity 的實(shí)例對(duì)象,并調(diào)用其 attach 方法來關(guān)聯(lián)上下文。

// Activity#performLaunchActivity
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
            cl, component.getClassName(), r.intent);

    if (activity != null) {
        ...
        activity.attach(appContext, this, getInstrumentation(), r.token,
            r.ident, app, r.intent, r.activityInfo, title, r.parent,
            r.embeddedID, r.lastNonConfigurationInstances, config,
            r.referrer, r.voiceInteractor, window, r.configCallback,
            r.assistToken);
        ...
    }
...
}

在 Activity 的 attach 方法中,系統(tǒng)會(huì)創(chuàng)建 Activity 所屬 Window 對(duì)象并為其設(shè)置回調(diào)接口:

// Activity#attach
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);

到這里 Window 已經(jīng)創(chuàng)建完成了,下面分析 Activity 的視圖是怎么附屬在 Window 上的。

由于 Activity 的視圖由 setContentView 方法提供,我們就來看看這個(gè)方法的實(shí)現(xiàn):

    // Activity#setContentView
    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

Activity 將實(shí)現(xiàn)交給了 Window 處理,Window 的具體實(shí)現(xiàn)是 PhoneWindow

    //PhoneWindow
    @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            // 1、創(chuàng)建 DecorView
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            // 2、將 View 添加到 DecorView 的 mContentParent 中
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            // 3、回調(diào) Activity 的 onContentChanged 方法通知 Activity 視圖已經(jīng)發(fā)生改變
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

到這里 DecorView 創(chuàng)建并初始化完畢,Activity 的布局文件也成功添加到了 DecorView 的 mContentParent 中了,但是此時(shí) DecorView 還沒有被 WIndowManager 正式添加到 Window 中。

在 Activity 的 handleResumeActivity 中會(huì)調(diào)用 Activity 的 onResume 方法,接著會(huì)調(diào)用 Activity 的makeVisible 方法,在這個(gè)方法中,DecorView 真正完成了添加和顯示過程:

    void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }
2、Dialog 的 Window 創(chuàng)建過程

在 Dialog 的構(gòu)造方法中,會(huì)創(chuàng)建 Window 并設(shè)置監(jiān)聽:

    // Dialog
    Dialog(@NonNull Context context, @StyleRes int themeResId, boolean      createContextThemeWrapper) {
        ...
        mWindowManager = (WindowManager)    context.getSystemService(Context.WINDOW_SERVICE);

        final Window w = new PhoneWindow(mContext);
        mWindow = w;
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        w.setOnWindowSwipeDismissedCallback(() -> {
            if (mCancelable) {
                cancel();
            }
        });
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);

        mListenersHandler = new ListenersHandler(this);
    }

通過 setContentView 方法來初始化 DecorView 并將 Dialog 的視圖添加到 DecorView 中:

    // Dialog
    public void setContentView(@LayoutRes int layoutResID) {
        mWindow.setContentView(layoutResID);
    }
    
    public void setContentView(@NonNull View view) {
        mWindow.setContentView(view);
    }   

    public void setContentView(@NonNull View view, @Nullable ViewGroup.LayoutParams             params) {
        mWindow.setContentView(view, params);
    }

最后使用 Dialog 的 show 方法,通過 WindowManager 將 DecorView 添加到 Window 中:

    // Dialog
    public void show() {
        ...
        mDecor = mWindow.getDecorView();
        mWindowManager.addView(mDecor, l);
        mShowing = true;
        ...
    }
3、Toast 的 Window 創(chuàng)建過程
  • Toast 具有定時(shí)取消功能,所以系統(tǒng)采用了 Handler
  • Toast 內(nèi)部有兩類 IPC 過程,第一類是 Toast 訪問 NotificationManagerService(下面簡(jiǎn)稱NMS),第二類是 NotificationManagerService 回調(diào) Toast 里的 TN 接口

Toast 提供了 show、cancel 方法,它們內(nèi)部是一個(gè) IPC 過程:

    // Toast
    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;
        final int displayId = mContext.getDisplayId();

        try {
            // 調(diào)用了 NMS 的 enqueueToast 方法
            service.enqueueToast(pkg, tn, mDuration, displayId);
        } catch (RemoteException e) {
            // Empty
        }
    }

    public void cancel() {
        mTN.cancel();
    }

它們最終會(huì)調(diào)用 handleShowhandleHide 兩個(gè)方法真正實(shí)現(xiàn)顯示和隱藏 Toast:

// Toast.TN
public void handleShow(IBinder windowToken) {
    ...
    mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
    // 將 Toast 視圖添加到 Window 中
    mWM.addView(mView, mParams);
    ...
}
// Toast.TN
public void handleHide() {
    if (mView.getParent() != null) {
        if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
        mWM.removeViewImmediate(mView);
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容