Android 開發藝術探索讀書筆記 8 -- 理解 Window 和 WindowManager

本篇文章主要介紹以下幾個知識點:

  • Window 和 WindowManager
  • Window 的內部機制
  • Window 的創建過程
hello,夏天 (圖片來源于網絡)

Window 表示一個窗口的概念,是一個抽象類,其具體實現是 PhoeWindow。

WindowManager 是外界訪問 Window 的入口,創建一個 Window 需要通過 WindowManager。

Android 中的所有視圖都是通過 Window 來呈現的(如 Activity、Dialog、Toast 等),Window 是 View 的直接管理者。

8.1 Window 和 WindowManager

下面代碼將一個 Button 添加到屏幕坐標為(100,300)的位置上,演示了通過 WindowManager 添加 Window 的過程:

mFloatingButton = new Button(this);
mFloatingButton.setText("button");
mLayoutParams = new WindowManager.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSPARENT);
// Flags 參數表 Window 的屬性,常用的有如下:
// 1. FLAG_NOT_FOCUSABLE  表示 Window 不需要獲取焦點,也不需要接收各種輸入事件,此標記會同時啟用 FLAG_NOT_TOUCH_MODAL,最終事件會直接傳遞給下層具有焦點的 Window
// 2. FLAG_NOT_TOUCH_MODAL  此模式下,系統會將當前 Window 區域外的單擊事件傳遞給底層的 Window,當前 Window 區域內的單擊事件則自己處理。若不開啟此標記則其他 Window 將無法收到單擊事件。
// 3. FLAG_SHOW_WHEN_LOCKED  開啟此模式可以讓 Window 顯示在鎖屏的界面上
mLayoutParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL ;
mLayoutParams.gravity = Gravity.LEFT;
mLayoutParams.x = 100;
mLayoutParams.y = 300;
mWindowManager.addView(mFloatingButton, mLayoutParams);

WindowManager 繼承了 ViewManager, 所提供的功能常用的有以下3個方法:

public interface ViewManager{
    // 添加 View
    public void addView(View view, ViewGroup.LayoutParams params);
    // 更新 View
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    // 刪除 View
    public void removeView(View view);
}

根據上面的方法,實現拖動 Window 的效果只需根據手指的位置來設定 LayoutParams 中的 x 和 y 的值即可:

  1. 給 View 設置 onTouchListener
  2. onTouch 方法中不斷更新 View 的位置。

代碼如下:

public boolean onTouch(View v, MotionEvent event){
    int rawX = (int)event.getRawX();
    int rawY = (int)event.getRawY();
    switch(event.getAction()){
        case MotionEvent.ACTION_MOVE:
            mLayoutParams.x = rawX;
            mLayoutParams.y = rawY;
            mWindowManager.updateViewLayout(mFloatingButton, mLayoutParams);
           break;
        default:
           break;
    }
    return false;
}

8.2 Window 的內部機制

Window 是一個抽象的概念,每一個 Window 都對應著一個 View 和一個 ViewRootImpl,Window 和 View 通過 ViewRootImpl 來建立聯系,即 Window 是以 View 的形式存在。

下面從添加、刪除、更新來分析 Window 的內部機制。

8.2.1 Window 的添加過程

Window 的添加需通過接口 WindowManager 的 addView 來實現,而其真正的實現類是 WindowManagerImpl

public final class WindowManagerImpl implements WindowManager {
    // 將所有的操作全部委托給 WindowManagerGlobal 來實現
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    private final Context mContext;

    . . .

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

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

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

    . . .
}

可以看出 WindowManagerImpl 并沒直接實現 Window 的三大操作,而是全交給了 WindowManagerGlobal 來處理:

public final class WindowManagerGlobal {

    // 存儲所有 Window 所對應的 View
    private final ArrayList<View> mViews = new ArrayList<View>();
    // 存儲所有 Window 所對應的 ViewRootImpl
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
    // 存儲所有 Window 所對應的布局參數
    private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();
    // 存儲那些正在被刪除的 View 對象
    private final ArraySet<View> mDyingViews = new ArraySet<View>();

    . . .

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        // 1. 檢查參數是否合法,如果是子 Window 那么還需要調整一些布局參數
        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);
        }
       
        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {

            . . .

            // 2. 創建 ViewRootImpl 并將 View 添加到列表中
            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }

        // do this last because it fires off messages to start doing things
        try {
            // 3. 通過 ViewRootImpl 來更新界面并完成 Window 的添加過程
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            . . .
        }
    }
 }

上面第3步由 ViewRootImplsetView 方法來完成:

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;

                . . .

                // 1. requestLayout 來完成異步刷新請求
                requestLayout();
                . . .
                // 2. 通過 WindowSession 最終來完成 Window 的添加過程
                try {
                    mOrigWindowType = mWindowAttributes.type;
                    mAttachInfo.mRecomputeGlobalAttributes = true;
                    collectViewAttributes();

                    // mWindowSession 的類型是 IWindowSession,是一個 Binder 對象,
                    // 真正的實現類是 Session,也就是 Window 的添加過程是一次 IPC 調用
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
                } catch (RemoteException e) {
                   . . .
                } 
                . . .
            }
        }
    }

其中 setView 內部的 requestLayout 方法如下:

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            // scheduleTraversals 是 View 的繪制入口
            scheduleTraversals();
        }
    }

上面 setView 內部的 mWindowSession,即 Session,內部會通過 WindowManagerService 來實現 Window 的添加,具體怎么添加這里就不分析了。

以上就是 Window 的添加流程。

8.2.2 Window 的刪除過程

Window 的刪除過程和添加過程一樣是通過WindowManagerGlobal 來實現的, 其removeView 如下:

public final class WindowManagerGlobal {

    . . .

    public void removeView(View view, boolean immediate) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }

        synchronized (mLock) {
            // 1. 通過 findViewLocked 來查找待刪除的 View 的索引(查找過程就是建立的數組遍歷)
            int index = findViewLocked(view, true);
            View curView = mRoots.get(index).getView();
            // 2. 調用 removeViewLocked 來做進一步的刪除
            removeViewLocked(index, immediate);
            if (curView == view) {
                return;
            }

            throw new IllegalStateException("...");
        }
    }
 }

上面的 removeViewLocked 如下:

    private void removeViewLocked(int index, boolean immediate) {
        // removeViewLocked 是通過 ViewRootImpl 來完成刪除操作的
        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());
            }
        }
       // 具體的刪除操作由 ViewRootImpl 的 die 方法完成
        boolean deferred = root.die(immediate);
        if (view != null) {
            view.assignParent(null);
            if (deferred) {
                mDyingViews.add(view);
            }
        }
    }

上面 ViewRootImpldie 方法如下:

   /**
     * @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.
        if (immediate && !mIsInTraversal) {
            // 若是同步刪除,則不發消息直接調用 doDie 方法
            doDie();
            return false;
        }

        if (!mIsDrawing) {
            destroyHardwareRenderer();
        } else {
            Log.e(mTag, "... ");
        }
        // 若是異步刪除,則發送 MSG_DIE 消息,ViewRootImpl 中的 Handler 會處理此消息并調用 doDie 方法
        mHandler.sendEmptyMessage(MSG_DIE);
        return true;
    }

上面 doDie 方法在內部會調用方法 dispatchDetachedFromWindow 來實現真正的刪除 View 邏輯。

方法 dispatchDetachedFromWindow 主要做四件事:

(1)垃圾回收相關的工作

(2)通過 Session 的 remove 方法刪除 Window

(3)在內部調用 View 的 onDetachedFromWindow() 以及 onDetachedFromWindowInternal(),做資源回收工作。

(4)調用 WindowManagerGlobaldoRemoveView 方法刷新數據。

8.2.3 Window 的更新過程

Window 的更新過程還是看 WindowManagerGlobal 中的 updateViewLayout 方法,如下:

public final class 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;

        // 1. 更新 View 的 LayoutParams
        view.setLayoutParams(wparams);

        synchronized (mLock) {
            int index = findViewLocked(view, true);
            ViewRootImpl root = mRoots.get(index);
            mParams.remove(index);
            mParams.add(index, wparams);
            // 2. 更新 ViewRootImpl 中的 LayoutParams
            // ViewRootImpl 會對 View 重新布局、更新 Window 的視圖
            root.setLayoutParams(wparams, false);
        }
    }
 }

8.3 Window 的創建過程

上面分析可知,View 是 Android 中的視圖的呈現方式,但 View 不能單獨存在,必須附著在 Window 上面,因此有視圖的地方就有 Window。

下面分析一些視圖元素中的 Window 的創建過程。

8.3.1 Activity 的 Window 創建過程

要分析 Activity 的 Window 創建過程就必須了解 Activity 的啟動過程。

Activity 的啟動過程很復雜,最終會由 ActivityThread 中的 performLaunchActivity() 來完成整個啟動過程,代碼如下:

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
   
        . . .

        Activity activity = null;
        try {
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            // 通過類加載器創建 Activity 的實例對象
            activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
            . . .
        } catch (Exception e) { . . . }

        try {
            . . .

            if (activity != null) {
                Context appContext = createBaseContextForActivity(r, activity);
                CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
                Configuration config = new Configuration(mCompatConfiguration);
                if (DEBUG_CONFIGURATION) Slog.v(TAG, ". . . ");

                // 調用其 attach 方法為其關聯運行過程中所依賴的一系列上下文環境變量
                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);

               . . .
            }
        } catch (Exception e) { . . . } 

        return activity;
    }

在 Activity 的 attach 方法里,系統會創建 Activity 所屬的 Window 對象并為其設置回調接口:

// 創建 Window 對象
mWindow = PolicyManager.makeNewWindow(this);
// 實現 Window 的 Callback 接口
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
. . .

上面 Activity 的 Window 是通過 PolicyManager 的一個工廠方法來創建的,其 makeNewWindow 如下:

public Window makeNewWindow(Context context){
    // Window 的具體實現是 PhoneWindow
    return new PhoneWindow(context);
}

到這里 Window 已經創建完了,接下來分析 Activity 的視圖是如何附屬在 Window 上。

由于 Activity 的視圖由 setContentView 方法提供,只需看其實現即可:

public void setContentView(int layoutResID){
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

從上面代碼可知 Activity 將具體實現交給 Window 處理,而 Window 的具體實現是 PhoneWindowPhoneWindowsetContentView 方法大致遵循如下步驟:

1. 若無 DecorView,則創建它

DecorView 是一個 FrameLayout,是 Activity 中的頂級 View,其創建過程由 installDecor 方法來完成,此方法內部通過 generateDecor 方法來直接創建 DecorView

protected DecorView generateDecor(){
    return new DecorView(getContext(), -1);
}

2. 將 View 添加到 DecorViewmContentParent

步驟1已創建和初始化 DecorView,接下來直接將 Activity 的視圖添加到 DecorViewmContentParent 中即可:

mLayoutInflater.inflate(layoutResID, mContentParent);

3. 回調 Activity 的 onContentChanged 方法通知 Activity 視圖已發生改變

由于 Activity 實現了 Window 的 Callback 接口,其布局文件已被添加到 DecorViewmContentParent 中,需要通知 Activity 做相應的處理:

final Callback cb = getCallback();
if(cb != null && !isDestroyed()){
    cb.onContentChanged();
}

經過上面3個步驟,DecorView 已被創建和初始完畢,Activity 的布局文件也添加到了 DecorViewmContentParent 中。

接下來在 ActivityThreadhandleResumeActivity 方法中會先調用 Activity 的 onResume 方法,再調用 Activity 的 makeVisible() 方法:

void makeVisible(){
    if(!mWindowAdded){
        ViewManager wm = getWindowManager();
        vm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = ture;
    }
    mDecor.setVisibility(View.VISIBLE);
}

在上面 makeVisible 方法中,DecorView 真正地完成了添加和顯示這兩個過程,Activity 的視圖才能被用戶看到。

以上便是 Activity 中的 Window 的創建過程。

8.3.2 Dialog 的 Window 創建過程

Dialog 的 Window 的創建過程和 Activity 類似,有如下幾個步驟:

1. 創建 Window

Dialog 中的 Window 的創建同樣是通過 PolicyManagermakeNewWindow 方法來完成的,過程和 Activity 類似:

Dialog(Context context, int theme, boolean createContextThemeWrapper){
    . . .
    mWindowManger = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
    Window w = PolicyManage.makeNewWindow(mContext);
    mWindow = w;
    w.setCallback(this);
    w.setOnWindowDismissedCallback(this);
    . . .
}

2. 初始化 DecorView 并將 Dialog 的視圖添加到 DecorView

這個過程也和 Acitivity 的類似,通過 Window 去添加指定的布局文件:

public void setContentView(int layoutResID){
    mWindow.setContentView(layoutResID);
}

3. 將 DecorView 添加到 Window 中并顯示

在 Dialog 的 show 方法中,會通過 WindowManagerDecorView 添加到 Window 中:

mWindowManager.addView(mDecor, 1);
mShowing = true;

以上便是 Dialog 的 Window 創建過程,和 Activity 的類似。

注:普通的 Dialog 必須采用 Activity 的 Context,若采用 Application 的 Context 會報錯。

8.3.3 Toast 的 Window 創建過程

Toast 和 Dialog 不同,稍復雜。Toast 也是基于 Window 來實現的,由于具有定時取消功能,系統采用了 Handler。

具體的過程這里不再介紹了,有興趣的可去看看書。本篇文章主要是對 Window 有一個更加清晰的認識,理解 Window 和 View 的依賴關系。

本篇文章就介紹到這。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容