【Android 源碼解析】Activity、Dialog、PopWindow、Toast窗口添加機制

一、WindowManager

WindowManager 是一個接口,它繼承自 ViewManager,ViewManager 接口很簡單,只提供了三個在 Activity 中添加和移除子 View 的抽象方法 addView、updateView、removeView:

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

WindowManager 接口繼承了 ViewManager,同時自己還定義了一個內部類 LayoutParams,這個 LayoutParams 類是繼承自 ViewGroup 的內部類 LayoutParams
WindowManager 的實現類是 WindowManagerImpl,實現 WindowManager 中定義的接口功能。
而 ViewGroup 類也實現了 ViewManager 接口,因為 ViewGroup 要添加或者刪除子 View,ViewGroup 層層嵌套,最頂層的是 DecorView,最終顯示在 Window 中,Window 是 View 的實際管理者。

二、Window

我們都知道 Window 是一個抽象類,唯一實現類是 PhoneWindow。創建一個 Window 很簡單,只需要通過 WindowManager 即可完成,WindowManager 是外界訪問 Window 的入口,Window 具體實現在 WindowManagerService 中,WindowManager 和 WindowManagerService 的交互是一個 IPC 過程,這里不作深究。
怎么通過 WindowManager 添加一個 Window 呢?很簡單,使用 addView 方法
windowManager.addView(view,layoutParams);

1、WindowManager.LayoutParams

Window 的一些屬性都是通過 WindowManager.LayoutParams 來定義的。

Window 類型

Window 是分層的,層級大的會覆蓋在層級小的上面。
Window 有三種類型,

  1. 應用 Window。一個應用 Window 對應著 Activity,層級范圍1-99;
  2. 子 Window。不能單獨存在,需要依附在特定的父 Window 之中,比如一些 Dialog,層級范圍 1000-1999;
  3. 系統 Window。需要聲明權限才能創建,比如 Toast 和系統狀態欄,層級范圍2000-2999;
    Window 的層級范圍對應著 WindowManager.LayoutParams 的 type 參數,WindowManager.LayoutParams 內部定義了一些靜態常量值。需要注意的是,如果定義系統 Window,別忘記聲明權限。
//以下定義都是描述窗口的類型
        public int type;
        //第一個應用窗口
        public static final int FIRST_APPLICATION_WINDOW = 1;
        //所有程序窗口的base窗口,其他應用程序窗口都顯示在它上面
        public static final int TYPE_BASE_APPLICATION   = 1;
        //所有Activity的窗口
        public static final int TYPE_APPLICATION        = 2;
        //目標應用窗口未啟動之前的那個窗口
        public static final int TYPE_APPLICATION_STARTING = 3;
        //最后一個應用窗口
        public static final int LAST_APPLICATION_WINDOW = 99;

        //第一個子窗口
        public static final int FIRST_SUB_WINDOW        = 1000;
        // 面板窗口,顯示于宿主窗口的上層
        public static final int TYPE_APPLICATION_PANEL  = FIRST_SUB_WINDOW;
        // 媒體窗口(例如視頻),顯示于宿主窗口下層
        public static final int TYPE_APPLICATION_MEDIA  = FIRST_SUB_WINDOW+1;
        // 應用程序窗口的子面板,顯示于所有面板窗口的上層
        public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW+2;
        //對話框窗口
        public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW+3;
        //
        public static final int TYPE_APPLICATION_MEDIA_OVERLAY  = FIRST_SUB_WINDOW+4;
        //最后一個子窗口
        public static final int LAST_SUB_WINDOW         = 1999;

        //系統窗口,非應用程序創建
        public static final int FIRST_SYSTEM_WINDOW     = 2000;
        //狀態欄,只能有一個狀態欄,位于屏幕頂端,其他窗口都位于它下方
        public static final int TYPE_STATUS_BAR         = FIRST_SYSTEM_WINDOW;
        //搜索欄,只能有一個搜索欄,位于屏幕上方
        public static final int TYPE_SEARCH_BAR         = FIRST_SYSTEM_WINDOW+1;
        //電話窗口,它用于電話交互(特別是呼入),置于所有應用程序之上,狀態欄之下
        public static final int TYPE_PHONE              = FIRST_SYSTEM_WINDOW+2;
        //系統警告提示窗口,出現在應用程序窗口之上
        public static final int TYPE_SYSTEM_ALERT       = FIRST_SYSTEM_WINDOW+3;
        //鎖屏窗口
        public static final int TYPE_KEYGUARD           = FIRST_SYSTEM_WINDOW+4;
        //信息窗口,用于顯示Toast
        public static final int TYPE_TOAST              = FIRST_SYSTEM_WINDOW+5;
        //系統頂層窗口,顯示在其他一切內容之上,此窗口不能獲得輸入焦點,否則影響鎖屏
        public static final int TYPE_SYSTEM_OVERLAY     = FIRST_SYSTEM_WINDOW+6;
        //電話優先,當鎖屏時顯示,此窗口不能獲得輸入焦點,否則影響鎖屏
        public static final int TYPE_PRIORITY_PHONE     = FIRST_SYSTEM_WINDOW+7;
        //系統對話框窗口
        public static final int TYPE_SYSTEM_DIALOG      = FIRST_SYSTEM_WINDOW+8;
        //鎖屏時顯示的對話框
        public static final int TYPE_KEYGUARD_DIALOG    = FIRST_SYSTEM_WINDOW+9;
        //系統內部錯誤提示,顯示在任何窗口之上
        public static final int TYPE_SYSTEM_ERROR       = FIRST_SYSTEM_WINDOW+10;
        //內部輸入法窗口,顯示于普通UI之上,應用程序可重新布局以免被此窗口覆蓋
        public static final int TYPE_INPUT_METHOD       = FIRST_SYSTEM_WINDOW+11;
        //內部輸入法對話框,顯示于當前輸入法窗口之上
        public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;
        //墻紙窗口
        public static final int TYPE_WALLPAPER          = FIRST_SYSTEM_WINDOW+13;
        //狀態欄的滑動面板
        public static final int TYPE_STATUS_BAR_PANEL   = FIRST_SYSTEM_WINDOW+14;
        //安全系統覆蓋窗口,這些窗戶必須不帶輸入焦點,否則會干擾鍵盤
        public static final int TYPE_SECURE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+15;
        //最后一個系統窗口
        public static final int LAST_SYSTEM_WINDOW      = 2999;
Window 顯示特性

WindowManager.LayoutParams 的 flags 屬性定義了 Window 的顯示特性

//窗口特征標記
        public int flags;
        //當該window對用戶可見的時候,允許鎖屏
        public static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON     = 0x00000001;
        //窗口后面的所有內容都變暗
        public static final int FLAG_DIM_BEHIND        = 0x00000002;
        //Flag:窗口后面的所有內容都變模糊
        public static final int FLAG_BLUR_BEHIND        = 0x00000004;
        //窗口不能獲得焦點
        public static final int FLAG_NOT_FOCUSABLE      = 0x00000008;
        //窗口不接受觸摸屏事件
        public static final int FLAG_NOT_TOUCHABLE      = 0x00000010;
        //即使在該window在可獲得焦點情況下,允許該窗口之外的點擊事件傳遞到當前窗口后面的的窗口去
        public static final int FLAG_NOT_TOUCH_MODAL    = 0x00000020;
        //當手機處于睡眠狀態時,如果屏幕被按下,那么該window將第一個收到觸摸事件
        public static final int FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040;
        //當該window對用戶可見時,屏幕出于常亮狀態
        public static final int FLAG_KEEP_SCREEN_ON     = 0x00000080;
        //:讓window占滿整個手機屏幕,不留任何邊界
        public static final int FLAG_LAYOUT_IN_SCREEN   = 0x00000100;
        //允許窗口超出整個手機屏幕
        public static final int FLAG_LAYOUT_NO_LIMITS   = 0x00000200;
        //window全屏顯示
        public static final int FLAG_FULLSCREEN      = 0x00000400;
        //恢復window非全屏顯示
        public static final int FLAG_FORCE_NOT_FULLSCREEN   = 0x00000800;
        //開啟窗口抖動
        public static final int FLAG_DITHER             = 0x00001000;
        //安全內容窗口,該窗口顯示時不允許截屏
        public static final int FLAG_SECURE             = 0x00002000;


        //鎖屏時顯示該窗口
        public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000;
        //系統的墻紙顯示在該窗口之后
        public static final int FLAG_SHOW_WALLPAPER = 0x00100000;
        //當window被顯示的時候,系統將把它當做一個用戶活動事件,以點亮手機屏幕
        public static final int FLAG_TURN_SCREEN_ON = 0x00200000;
        //該窗口顯示,消失鍵盤
        public static final int FLAG_DISMISS_KEYGUARD = 0x00400000;
        //當該window在可以接受觸摸屏情況下,讓因在該window之外,而發送到后面的window的觸摸屏可以支持split touch
        public static final int FLAG_SPLIT_TOUCH = 0x00800000;
        //對該window進行硬件加速,該flag必須在Activity或Dialog的Content View之前進行設置
        public static final int FLAG_HARDWARE_ACCELERATED = 0x01000000;
        //讓window占滿整個手機屏幕,不留任何邊界
        public static final int FLAG_LAYOUT_IN_OVERSCAN = 0x02000000;
        //透明狀態欄
        public static final int FLAG_TRANSLUCENT_STATUS = 0x04000000;
        //透明導航欄
        public static final int FLAG_TRANSLUCENT_NAVIGATION = 0x08000000;

除了 type 和 flags 外,WindowManager.LayoutParams 還有一些屬性,這里說幾個比較重要的:

//窗口的起點坐標
public int x;
public int y;
//窗口內容的對齊方式
public int gravity;
//描述窗口的寬度和高度,是父類 ViewGroup.LayoutParams 的成員變量
public int width;
public int height;

2、Window 內部機制

Window 是一個抽象概念,并不是實際存在的,而是以 View 的形式存在的,這從 WindowManager 的定義可以看出來,addView,updateView,removeView 都是針對 View 的,View 才是 Window 的實體,在實際使用中必須通過 WindowManager 才能訪問 Window。
每個 Window 都對應著一個 View 和 ViewRootImpl,Window 和 View 是通過 ViewRootImpl 聯系起來的。

Window 添加過程

Window 的添加、更新和刪除需要 WindowManager 的 addView、updateView、removeView 方法,WindowManager 是接口,真正的實現是 WindowManagerImpl,但是其實 WindowManagerImpl 也沒有直接實現 WindowManager 的三大操作,而是交給 WindowManagerGlobal 來處理。WindowManagerImpl 這種工作模式是典型的橋接模式
WindowManagerImpl 的 addView 方法主要分如下幾步:
1)檢查 view、diaplay、params 等參數是否合法,如果是子 Window(parentWindow != null) 還需要調整一些布局參數,LayoutParams 必須是 WindowManager.LayoutParams
2)創建 ViewRootImpl 并將 View 加入 List
WindowManagerGlobal 有幾個 ArrayList 比較重要:

//存儲所有 Window 對應的 View
private final ArrayList<View> mViews = new ArrayList<View>();
//存儲所有 Window 對應的 ViewRootImpl
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
//存儲所有 Window 的布局參數 LayoutParams
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();
//存儲正在被刪除的 View 對象,已經調用 removeView 還未執行
private final ArrayList<> mDyingViews = new ArrayList<>();

在 addView 中如下方式把一系列對象添加到 List

root = newViewRootImpl(view.getContext,display);
view.setLayoutParams(wparams);

mViews.add(view);
mRoots.add(root);
mParams.add(wparam);

3)通過 ViewRootImpl 來更新界面并完成 Window 的添加過程
這個步驟由 ViewRootImpl 的 setView 來完成,setView 內部通過 requestLayout 完成異步刷新請求,requestLayout 內部調用 scheduleTraversals 來進行 View 的繪制。
接著通過 WindowSession 完成 Window 的添加過程,Session 內部是通過 WindowManagerService 實現 Window 的添加的。

Window 的刪除過程

WindowManagerGlobal 的 removeView 先通過 findViewLocked 來查找待刪除的 View 的索引,然后調用 removeViewLoacked 來做進一步刪除,內部是通過 ViewRootImpl 來完成刪除操作的

Window 更新過程

首先更新 View 的LayoutParams,接著更新 ViewRootImpl 中的 LayoutParams,這一步是通過 ViewRootImpl 的 setLayoutParams 來實現的,在 ViewRootImpl 中會通過 scheduleTraversals 來對 View 重新布局,并通過 WindowSession 來更新 Window 視圖。

三、Window 創建過程

Activity 窗口添加流程

前面的文章說過,Activity 的實例化是在 ActivityThread 的 performLaunchActivity 方法開始的,在創建好 ContextImpl 對象后調用了 Activity 的 attach 方法,把 ContextImpl 的實例賦值給 mBase 成員變量,除此之外,attach 方法里面還做了初始化 Activity 成員變量 mWindow 和 mWindowManager 的工作:

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
        ......
        //創建Window類型的mWindow對象,實際為PhoneWindow類實現了抽象Window類
        mWindow = PolicyManager.makeNewWindow(this);
        ......
        //通過抽象Window類的setWindowManager方法給Window類的成員變量WindowManager賦值實例化
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        ......
        //把抽象Window類相關的WindowManager對象拿出來關聯到Activity的WindowManager類型成員變量mWindowManager
        mWindowManager = mWindow.getWindowManager();
        ......
    }
  1. 通過 PolicyManager 的 makeNewWindow 方法創建一個 PhoneWindow 對象,賦值給 Activity 的成員變量mWindow
  2. 通過 Window 類的 setWindowManager 方法給 Window 的 mWindowManager 成員變量賦值實例化;
  3. 把前面實例化的 Window 的成員變量 mWindowManager 賦值給 Activity 的成員變量 mWindowManager,使 Activity 和 WindowManager 關聯起來;

再看 Window 的 setWindowManager 方法,看WindowManager 的實例是怎么創建的:

public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        ......
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        //實例化Window類的WindowManager類型成員mWindowManager
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }

就是調用穿進去的或者重新獲取的 WindowManagerImpl 對象的 createLocalWindowManager 方法

public final class WindowManagerImpl implements WindowManager {
    ......
    private WindowManagerImpl(Display display, Window parentWindow) {
        mDisplay = display;
        mParentWindow = parentWindow;
    }
    ......
    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mDisplay, parentWindow);
    }
    ......
}

而 createLocalWindowManager 方法很簡單,就是通過 WindowMangerImpl 兩個參數的構造函數 new 了一個 WindowManagerImpl 對象。
那在 setWindowManager 中傳進去的第一個參數 (WindowManager)context.getSystemService(Context.WINDOW_SERVICE) 是從哪來的呢?
在 Context 的實現類 ContextImpl 中有靜態代碼塊:

class ContextImpl extends Context {
    ......
    //靜態代碼塊,類加載時執行一次
    static {
        ......
        //這里有一堆類似的XXX_SERVICE的注冊
        ......
        registerService(WINDOW_SERVICE, new ServiceFetcher() {
                Display mDefaultDisplay;
                public Object getService(ContextImpl ctx) {
                    //搞一個Display實例
                    Display display = ctx.mDisplay;
                    if (display == null) {
                        if (mDefaultDisplay == null) {
                            DisplayManager dm = (DisplayManager)ctx.getOuterContext().
                                    getSystemService(Context.DISPLAY_SERVICE);
                            mDefaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY);
                        }
                        display = mDefaultDisplay;
                    }
                    //返回一個WindowManagerImpl實例
                    return new WindowManagerImpl(display);
                }});
        ......
    }
    //這就是你在外面調運Context的getSystemService獲取到的WindowManagerImpl實例
    @Override
    public Object getSystemService(String name) {
        ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
        return fetcher == null ? null : fetcher.getService(this);
    }
    //上面static代碼塊創建WindowManagerImpl實例用到的方法
    private static void registerService(String serviceName, ServiceFetcher fetcher) {
        if (!(fetcher instanceof StaticServiceFetcher)) {
            fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++;
        }
        SYSTEM_SERVICE_MAP.put(serviceName, fetcher);
    }
}

在靜態代碼塊中注冊了一堆服務,其中就包括 WINDOW_SERVICE 服務,返回了一個 WindowManagerImpl 實例。
這個 WindowManagerIMpl 與 Activity 中關聯的 WindowManagerImpl 實例的不同之處在于這個是通過一個參數的構造方法創建的,其實也就是 parentWindow 是 null,而Activity 中 Window 的 WindowManager 成員在構造實例化時傳入給 WindowManagerImpl 中 mParentWindow 成員的是當前 Window 對象,;還要就是靜態代碼塊是只在初始時加載一次,所以這個 WindowManager 是全局單例的。
每一個 Activity 都會新創建一個 WindowManager 實例來顯示 Activity 的界面的,在 setContentView 觸發 Activity 的 resume 狀態后會調用 makeVisible 方法,其中就是獲取 Activity 的 mWindowManager 成員 addView 的:

 void makeVisible() {
        if (!mWindowAdded) {
            //也就是獲取Activity的mWindowManager
            //這個mWindowManager是在Activity的attach中通過mWindow.getWindowManager()獲得
            ViewManager wm = getWindowManager();
            //調運的實質就是ViewManager接口的addView方法,傳入的是mDecorView
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

可以看到這里 addView 的 View 參數是 mDecor 也就是 Phone Window 的內部類 DecorView。
Context的WindowManager對每個APP來說是一個全局單例的,而Activity的WindowManager是每個Activity都會新創建一個的(其實你從上面分析的兩個實例化WindowManagerImpl的構造函數參數傳遞就可以看出來,Activity中Window的WindowManager成員在構造實例化時傳入給WindowManagerImpl中mParentWindow成員的是當前Window對象,而ContextImpl的static塊中單例實例化WindowManagerImpl時傳入給WindowManagerImpl中mParentWindow成員的是null值),使用 Activity 的 getSysytemService(WINDOW_SERVICE) 獲取的是 Local 的WindowManager。

Dialog 窗口添加顯示機制

從 Dialog 的構造函數說起:

public class Dialog implements DialogInterface, Window.Callback,
        KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback {
    ......
    public Dialog(Context context) {
        this(context, 0, true);
    }
    //構造函數最終都調運了這個默認的構造函數
    Dialog(Context context, int theme, boolean createContextThemeWrapper) {
        //默認構造函數的createContextThemeWrapper為true
        if (createContextThemeWrapper) {
            //默認構造函數的theme為0
            if (theme == 0) {
                TypedValue outValue = new TypedValue();
                context.getTheme().resolveAttribute(com.android.internal.R.attr.dialogTheme,
                        outValue, true);
                theme = outValue.resourceId;
            }
            mContext = new ContextThemeWrapper(context, theme);
        } else {
            mContext = context;
        }
        //mContext已經從外部傳入的context對象獲得值(一般是個Activity)!!!非常重要,先記住!!!

        //獲取WindowManager對象
        mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
        //為Dialog創建新的Window
        Window w = PolicyManager.makeNewWindow(mContext);
        mWindow = w;
        //Dialog能夠接受到按鍵事件的原因
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        //關聯WindowManager與新Window,特別注意第二個參數token為null,也就是說Dialog沒有自己的token
        //一個Window屬于Dialog的話,那么該Window的mAppToken對象是null
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);
        mListenersHandler = new ListenersHandler(this);
    }
    ......
}

Dialog 的 Window 也是通過 PolicyManager.makeNewWindow 方法創建的,但是 WindowManager 是 context.getSystemService 方法獲取的,也就是說沒有新建 WindowManager 的實例,這個 context 是通過構造方法傳進來的,一般是 Activity Context,所以 Dialog 的 WindowManager 其實是 Activity 的 mWindowManager,并通過 Window 類的 setWindowManager 方法與 Window 關聯,Dialog 類也實現了 Window.Callback,Window.OnWindowDismissedCallback并給 Window.setCallback,所以 Dialog 能夠接受到點擊等事件。
至此Dialog的創建過程Window處理已經完畢,接下來我們繼續看看Dialog的show與cancel方法:

  public void show() {
        ......
        if (!mCreated) {
            //回調Dialog的onCreate方法
            dispatchOnCreate(null);
        }
        //回調Dialog的onStart方法
        onStart();
        //類似于Activity,獲取當前新Window的DecorView對象,所以有一種自定義Dialog布局的方式就是重寫Dialog的onCreate方法,使用setContentView傳入布局,就像前面文章分析Activity類似
        mDecor = mWindow.getDecorView();
        ......
        //獲取新Window的WindowManager.LayoutParams參數,和上面分析的Activity一樣type為TYPE_APPLICATION
        WindowManager.LayoutParams l = mWindow.getAttributes();
        ......
        try {
            //把一個View添加到Activity共用的windowManager里面去
            mWindowManager.addView(mDecor, l);
            ......
        } finally {
        }
    }

就是把獲取要添加的 Window 的 mDecor 和 LayoutParams,調用 WindowManager 的 addView 方法完成添加。
Activity 和 Dialog 共用了一個 Token 對象,Dialog 必須依賴于 Activity 而顯示(通過別的 context 搞完之后 token 都為 null,最終會在 ViewRootImpl 的 setView 方法中加載時因為 token 為 null 拋出異常),所以 Dialog 的 Context 傳入參數一般是一個存在的 Activity,如果 Dialog 彈出來之前 Activity 已經被銷毀了,則這個 Dialog 在彈出的時候就會拋出異常,因為 token 不可用了。在 Dialog 的構造函數中我們關聯了新 Window 的 callback 事件監聽處理,所以當 Dialog 顯示時 Activity 無法消費當前的事件,直接回調了 Dialog 的 Window 的 Callback 監聽,Activity 的 Window 接收不到。

PopupWindow 窗口添加顯示機制

PopWindow 實質就是彈出式菜單,它與 Dialag 不同的地方是不會使依賴的 Activity 組件失去焦點(PopupWindow 彈出后可以繼續與依賴的 Activity 進行交互),Dialog 卻不能這樣。同時PopupWindow 與 Dialog 另一個不同點是 PopupWindow 是一個阻塞的對話框,如果你直接在 Activity 的 onCreate 等方法中顯示它則會報錯,所以 PopupWindow 必須在某個事件中顯示地或者是開啟一個新線程去調用。
先看 PopupWindow 最常用的一種構造函數:

public class PopupWindow {
    ......
    //我們只分析最常用的一種構造函數
    public PopupWindow(View contentView, int width, int height, boolean focusable) {
        if (contentView != null) {
            //獲取mContext,contentView實質是View,View的mContext都是構造函數傳入的,View又層級傳遞,所以最終這個mContext實質是Activity!!!很重要
            mContext = contentView.getContext();
            //獲取Activity的getSystemService的WindowManager
            mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        //進行一些Window類的成員變量初始化賦值操作
        setContentView(contentView);
        setWidth(width);
        setHeight(height);
        setFocusable(focusable);
    }
    ......
}

mContext 變量是通過 contentView 的 getContext 賦值的,contentView 實質是 View,View 的 context 是通過構造函數傳入的,并且是層層傳遞的,所以這個 context 實際是 Activity。然后獲取 Activity 的 WindowManager,但是并沒有創建新的 Window
再看展示方法:

public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
        ......
        //anchor是Activity中PopWindow準備依附的View,這個View的token實質也是Activity的Window中的token,也即Activity的token
        //第一步   初始化WindowManager.LayoutParams
        WindowManager.LayoutParams p = createPopupLayout(anchor.getWindowToken());
        //第二步
        preparePopup(p);
        ......
        //第三步
        invokePopup(p);
    }

private WindowManager.LayoutParams createPopupLayout(IBinder token) {
        //實例化一個默認的WindowManager.LayoutParams,其中type=TYPE_APPLICATION
        WindowManager.LayoutParams p = new WindowManager.LayoutParams();
        //設置Gravity
        p.gravity = Gravity.START | Gravity.TOP;
        //設置寬高
        p.width = mLastWidth = mWidth;
        p.height = mLastHeight = mHeight;
        //依據背景設置format
        if (mBackground != null) {
            p.format = mBackground.getOpacity();
        } else {
            p.format = PixelFormat.TRANSLUCENT;
        }
        //設置flags
        p.flags = computeFlags(p.flags);
        //修改type=WindowManager.LayoutParams.TYPE_APPLICATION_PANEL,mWindowLayoutType有初始值,type類型為子窗口
        p.type = mWindowLayoutType;
        //設置token為Activity的token
        p.token = token;
        ......
        return p;
    }

private void preparePopup(WindowManager.LayoutParams p) {
        ......
        //有無設置PopWindow的background區別
        if (mBackground != null) {
            ......
            //如果有背景則創建一個PopupViewContainer對象的ViewGroup
            PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);
            PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, height
            );
            //把背景設置給PopupViewContainer的ViewGroup
            popupViewContainer.setBackground(mBackground);
            //把我們構造函數傳入的View添加到這個ViewGroup
            popupViewContainer.addView(mContentView, listParams);
            //返回這個ViewGroup
            mPopupView = popupViewContainer;
        } else {
            //如果沒有通過PopWindow的setBackgroundDrawable設置背景則直接賦值當前傳入的View為PopWindow的View
            mPopupView = mContentView;
        }
        ......
    }

private void invokePopup(WindowManager.LayoutParams p) {
        if (mContext != null) {
            p.packageName = mContext.getPackageName();
        }
        mPopupView.setFitsSystemWindows(mLayoutInsetDecor);
        setLayoutDirectionFromAnchor();
        mWindowManager.addView(mPopupView, p);
    }

anchor 是 Activity 中 PopWindow 準備依附的 View,這個 View 的 token 實質也是 Activity 的 Window 中的 token,也即 Activity 的 token。
第一步是創建一個 WindowManager.LayoutParams;
第二步 preparePopup 方法的作用就是判斷設置 View,如果有背景則會在傳入的 contentView 外面包一層PopupViewContainer(實質是一個重寫了事件處理的 FrameLayout)之后作為 mPopupView,如果沒有背景則直接用 contentView 作為 mPopupView。
PopupViewContainer 是一個 PopWindow 的內部私有類,它繼承了 FrameLayout,在其中重寫了 Key 和 Touch 事件的分發處理邏輯。同時查閱 PopupView 可以發現,PopupView 類自身沒有重寫 Key 和 Touch 事件的處理,所以如果沒有將傳入的 View對象放入封裝的 ViewGroup 中,則點擊 Back 鍵或者PopWindow 以外的區域 PopWindow 是不會消失的(其實PopWindow 中沒有向 Activity 及 Dialog 一樣 new 新的 Window ,所以不會有新的 callback 設置,也就沒法處理事件消費了)。這也是為什么我們設置 PopupWindow 點擊外部消失之前要先設置背景才有效。
第三步就是使用 WindowManager addView,因為 PopupWindow 沒有 new 新的 Window,所以 mDecor 不是從 Window 里面獲取的,而是在 preparPopup 方法中調用 createDecorView 方法生成的:

 private PopupDecorView createDecorView(View contentView) {
        final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
        final int height;
        if (layoutParams != null && layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
            height = ViewGroup.LayoutParams.WRAP_CONTENT;
        } else {
            height = ViewGroup.LayoutParams.MATCH_PARENT;
        }

        final PopupDecorView decorView = new PopupDecorView(mContext);
        decorView.addView(contentView, ViewGroup.LayoutParams.MATCH_PARENT, height);
        decorView.setClipChildren(false);
        decorView.setClipToPadding(false);

        return decorView;
    }
Toast 窗口添加顯示機制

我們常用的Toast窗口其實和前面分析的Activity、Dialog、PopWindow都是不同的,因為它和輸入法、墻紙類似,都是系統窗口。
通過分析TN類的handler可以發現,如果想在非UI線程使用Toast需要自行聲明Looper,否則運行會拋出Looper相關的異常;UI線程不需要,因為系統已經幫忙聲明。
在使用Toast時context參數盡量使用getApplicationContext(),可以有效的防止靜態引用導致的內存泄漏。
有時候我們會發現Toast彈出過多就會延遲顯示,因為上面源碼分析可以看見Toast.makeText是一個靜態工廠方法,每次調用這個方法都會產生一個新的Toast對象,當我們在這個新new的對象上調用show方法就會使這個對象加入到NotificationManagerService管理的mToastQueue消息顯示隊列里排隊等候顯示;所以如果我們不每次都產生一個新的Toast對象(使用單例來處理)就不需要排隊,也就能及時更新了。
總結一下:添加 Window 就是WindowManager 的 addView 方法,需要三個對象,WindowManager、View、LayoutParams

  • Activity 的添加操作是在 makeVisible 方法里wm.addView(mDecor, getWindow().getAttributes()); WindowManager 是在 Window 的 setWindowManager 方法里 調用 WindowManagerImpl 的 createLocalWindowManager 方法
    通過 WindowManagerImpl(display,parentWindow) 構造函數構創建的,mDecor 是在 Window 的 setContentView 方法中調用 initDecor 方法創建的,LayoutParams 是 Window 的 getAttributes 方法獲得一個默認為 MATCH_PARENT 的 LayoutParams 成員變量。
  • Dialog 的 addView 是在 show 方法里調用的,WindowManger 是context.getSystemService(Context.WINDOW_SERVICE)獲取的 Activity 的 WindowManager,mDecor 是 mWindow.getDecorView(); 獲取到 Window 的 DecorView,LayoutParams 也是 Window 的 MATCH_PARENT 的 LayoutParamms 成員變量
  • PopupWindow 也是 mContext.getSystemService(Context.WINDOW_SERVICE) 獲取的 Activity 的 WindowManager,因為 PopupWindow 沒有創建新的 Window, 添加的 mPopupView 是在 preparePopup 方法中在傳入的 contentView 外面包一層 PopupViewContainer 或者直接就是 contentView,LayoutParams 是在 showXXX 方法中調用的 createPopupLayout 方法中構造的。
    參考:
    Android應用Activity、Dialog、PopWindow、Toast窗口添加機制及源碼分析
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容