Activity和dialog的窗口添加源碼分析

本文是最近看Window有關(guān)源碼的部分記錄和分析, 可能有差錯(cuò); 另有很多代碼,比較枯燥

先說部分結(jié)論

  1. Window是一個(gè)抽象類, 具體的實(shí)現(xiàn)是PhoneWindow
  2. android系統(tǒng)中, 每個(gè)界面,對(duì)應(yīng)著一個(gè)window; 但其實(shí)在android系統(tǒng)中window也是一個(gè)抽象的概率,它是以view的形式存在; 在使用中, 無法直接訪問Window, 只能通過WindowManager才能訪問Window; 每個(gè)Window都對(duì)應(yīng)一個(gè)View和一個(gè)ViewRootImpl, ViewRootImpl是連接Window和WMS的橋梁, WMS的一些消息,通過ViewRootImpl轉(zhuǎn)發(fā)給View;
  3. WindowManager繼承自ViewManager(間接證明Window其實(shí)對(duì)應(yīng)的是View?),常用的只有三個(gè)方法:addView、updateView和removeView
  4. 各種Window的不同, 主要是 token及type的不同
  5. app中控制window, 是通過WindowManager.LayoutParams去控制, eg: 通過x,y,gravity去控制位置...

Activity 的window添加

Activity創(chuàng)建之后, 在ActivityThread中 調(diào)用 Activity#attach, 進(jìn)行一些初始化操作

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) {
    attachBaseContext(context);
    ....
    //創(chuàng)建對(duì)應(yīng)的Window 并設(shè)置callback, 其實(shí)為PhoneWindow
    mWindow = new PhoneWindow(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    ....
    // 設(shè)置Window的WindowManager, 對(duì)Window的mWindowManager賦值,
    // 事實(shí)上, Window中 并未使用傳遞進(jìn)去的windowManager, 而是在此方法中 調(diào)用WindowManagerImpl.createLocalWindowManager 重新創(chuàng)建了一個(gè)
    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    if (mParent != null) {
        mWindow.setContainer(mParent.getWindow());
    }
    // 把window對(duì)象中的 windowManager 關(guān)聯(lián)到 Activity的 mWindowManager
    mWindowManager = mWindow.getWindowManager();
    ...
  }

在Activity#attach中, 可以看出創(chuàng)建了一個(gè)PhoneWindow對(duì)象, 并且通過context.getSystemService(Context.WINDOW_SERVICE)設(shè)置了WindowManager對(duì)象, 還通過Window#getWindowManager對(duì)Activity的mWindowManager賦值, 這樣 Activity的Window WindowManager就關(guān)聯(lián)起來了;

接著看上面代碼,里面會(huì)通過context.getSystemService(Context.WINDOW_SERVICE)拿到一個(gè)windowManager對(duì)象, 這個(gè)window對(duì)象是什么呢? 如果對(duì)Context有過研究的話, 可以知道Context的實(shí)際對(duì)象是ContetImpl, 其中所有通過 getSystemService返回的對(duì)象, 都是通過static的代碼塊靜態(tài)添加的, 只需找WINDOW_SERVICE注冊(cè)的地方(在6.0中 注冊(cè)的代碼 已經(jīng)提取到'android.app.SystemServiceRegistry#registerService'中了)

registerService(Context.WINDOW_SERVICE, WindowManager.class,
               new CachedServiceFetcher<WindowManager>() {
           @Override
           public WindowManager createService(ContextImpl ctx) {
               return new WindowManagerImpl(ctx.getDisplay());
           }});

可以看見,實(shí)際的對(duì)象是WindowManagerImpl; (另外 Activity本身重寫了getSystemService方法,如果使用android.app.Activity#getSystemService, 返回的其實(shí)不是這個(gè)對(duì)象, 下面會(huì)說到);

上面注釋說到, android.view.Window#setWindowManager中, 并未使用傳遞進(jìn)去的WindowManager,而創(chuàng)建了一個(gè)新對(duì)象, 可以看一下代碼

public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        // activity把token傳進(jìn)來, 保存在Window.mAppToken
        mAppToken = appToken;
        mAppName = appName;
        mHardwareAccelerated = hardwareAccelerated
                || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        //創(chuàng)建了一個(gè)新的WindowManager, 并且把this 傳遞給了WindowManager
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }

而在android.view.WindowManagerImpl#createLocalWindowManager中, 可以明顯看出

// Context.getSystemService 獲得的WindowManager實(shí)例, 是沒有parentWindow的
public WindowManagerImpl(Display display) {
        this(display, null);
    }

private WindowManagerImpl(Display display, Window parentWindow) {
    mDisplay = display;
    mParentWindow = parentWindow;
}

public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
    // 將傳遞的 window對(duì)象保存, 對(duì)于activity來說,會(huì)將PhoneWindow對(duì)應(yīng)的對(duì)象傳入, 而對(duì)于 Context.getSystemService 獲得的WindowManager實(shí)例, 是沒有parentWindow的
    return new WindowManagerImpl(mDisplay, parentWindow);
}

window時(shí)我們看見的窗口,接下來看在Activity的window是怎么顯示出來, 看Activity.makeVisiable

void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        // windowManger中 添加window的根view( 即 decorView)
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}

上面已經(jīng)說過, WindowManager對(duì)應(yīng)的實(shí)例是WindowManagerImpl, 接著看android.view.WindowManagerImpl#addView

 private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

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

代碼很簡(jiǎn)單, 最后調(diào)用的是 WindowManagerGlobal#addView, 并且WindowManagerGlobal是一個(gè)單例, 看下android.view.WindowManagerGlobal#addView的代碼

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
              ...
         // 首先獲得 layoutParams
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        if (parentWindow != null) {
            // parentWindow是 WindowManagerImpl的成員變量mParentWindow;
            // 對(duì)于 activity來說, 即在mWindow.setWindowManager中 調(diào)用WindowManagerImpl.createLocalWindowManager(Window)去創(chuàng)建 mWindowManager實(shí)例時(shí), 傳入的phoneWindow對(duì)象
            // 調(diào)用下面這個(gè)方法, 會(huì)調(diào)整 WindowManager.LayoutParams, 主要是 設(shè)置 token 和硬件加速的標(biāo)志位
            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;
        ....
        // ViewRootImpl是一個(gè)很重要的類, 是ViewParent的子類, 可以說是Window和WMS之間的橋梁, 所有和View有關(guān)的Evnet,layout,draw,都在底層產(chǎn)生后通過WMS->ViewRootImpl傳遞給view
        root = new ViewRootImpl(view.getContext(), display);

        view.setLayoutParams(wparams);

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

        ...
        root.setView(view, wparams, panelParentView);
        ...
}

在WindowManagerGlobal中, 會(huì)為每一個(gè)window創(chuàng)建對(duì)應(yīng)的ViewRootImpl, 并把對(duì)應(yīng)的 decorView, ViewRootImpl 和 WindowManager.LayoutParams 保存, 最后把decorView 通過android.view.ViewRootImpl#setView添加進(jìn)WMS 媽蛋,好累啊, 終于快到WMS了

接著看一下android.view.ViewRootImpl#setView

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
      ...
      // 把Window對(duì)應(yīng)的View保存下來, 對(duì)應(yīng)的其實(shí)就是 PhoneWindow的 mDecorView
      mView = view;
      ...
      // 調(diào)用WMS后返回的結(jié)果
     int res; /* = WindowManagerImpl.ADD_OKAY; */

     // 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.
     // 請(qǐng)求 layout
     requestLayout();
    ....

    // 調(diào)用 WMS添加window, 并返回一個(gè)結(jié)果 用于判定添加的結(jié)果
    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
        ......                              

    // 根據(jù)返回的結(jié)果 判斷是否添加成功, 沒成功 就throw exception, 經(jīng)??匆姷膕ervices打開dialog的異常 就在這兒產(chǎn)生
    if (res < WindowManagerGlobal.ADD_OKAY) {
        mAttachInfo.mRootView = null;
        mAdded = false;
        mFallbackEventHandler.setView(null);
        unscheduleTraversals();
        setAccessibilityFocus(null, null);
        switch (res) {
            case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
            case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
                throw new WindowManager.BadTokenException(
                        "Unable to add window -- token " + attrs.token
                        + " is not valid; is your activity running?");
            case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
                throw new WindowManager.BadTokenException(
                        "Unable to add window -- token " + attrs.token
                        + " is not for an application");   
            .....
          }                          
     ......
}

ViewRootImpl里面, 首先會(huì)把window對(duì)應(yīng)的decorView存一份, 然后通過mWindowSession向WMS發(fā)消息, mWindowSession是通過WindowManagerGlobal的靜態(tài)方法獲得, 前面說過,這玩意是一個(gè)單例, 在哪兒都可以拿到; android本身是一個(gè)CS架構(gòu), 調(diào)用WMS,需要先獲取WMS對(duì)應(yīng)的client, 而client端的獲取,就是同 WindowManagerGlobal.initialize()進(jìn)行, 連通WMS的服務(wù)端; android.view.WindowManagerGlobal#getWindowSession, 是通過getWindowManagerService().openSession()獲得, 最后返回的是com.android.server.wm.Session, 而mWindowSession.addToDisplay最終調(diào)用的會(huì)是 com.android.server.wm.Session#addToDisplay, 最終會(huì)還是會(huì)調(diào)用com.android.server.wm.WindowManagerService#addWindow; 這一段在WMS Server端的代碼也比較多, 就不貼代碼了, 最后在WindowManagerService#addWindow, 會(huì)對(duì)WindowManager.LayoutParams做一些檢驗(yàn) 并返回值, 下面會(huì)說到;

上面說過事件是通過WMS傳遞給ViewRootImpl,然后傳遞給View,Activity, 具體事件在ViewRootImpl的分發(fā)過程, 可以看這篇博客here

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

1.dialog的窗口創(chuàng)建顯示過程,其實(shí)與activity相似, 最后對(duì)應(yīng)的Window其實(shí)也是PhoneWindow 首先直接看Dialog的構(gòu)造方法

    Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        ......
        // 用getSystemService獲取 WindowManger實(shí)例
        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        // 直接創(chuàng)建一個(gè) PhoneWindow的實(shí)例
        final Window w = new PhoneWindow(mContext);
        mWindow = w;
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);
        mListenersHandler = new ListenersHandler(this);
    }

代碼很明顯, 首先獲取windowManager實(shí)例, 然后創(chuàng)建window對(duì)象,然后在設(shè)置callback,對(duì)PhoneWindow的實(shí)例設(shè)置WindowManager;(這里有一點(diǎn)和Activity不同, Activity是調(diào)用Window的setWindowManager后, 把Window對(duì)應(yīng)的WindowManger獲取,然后賦值給自己的變量Activity#mWindowManger, 而dialog沒這個(gè)操作; dialog對(duì)應(yīng)的Window對(duì)象,就只是傳入的Context實(shí)例去getSystemService所得); 接下來,就是設(shè)置dialog的view,和Activity一樣,都是調(diào)用setContentView, 看下代碼

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

代碼很簡(jiǎn)單,直接調(diào)用的mWindow.setContentView, 把對(duì)應(yīng)的視圖,添加到PhoneWindow的DecorView中去; 接下來就是顯示了, 看一下Dialog#show方法

    public void show() {
        ...
        // 調(diào)用Dialog#onCreate方法, 通常也是在onCreate方法中,改變Window.LayoutParams, 從而調(diào)整dialog的寬高, 動(dòng)畫, 顯示位置
        onStart();
        .......
        mDecor = mWindow.getDecorView();
        WindowManager.LayoutParams l = mWindow.getAttributes();
        // 調(diào)用WindowManager去顯示
        mWindowManager.addView(mDecor, l);
        mShowing = true;

    }

顯示的代碼邏輯,和Activity顯示的一樣, 就是調(diào)用WindowManger#addView方法 在這兒就有一個(gè)開發(fā)中常遇到的問題, Activity可以直接調(diào)用show()顯示一個(gè)dialog, 而Service調(diào)用會(huì)報(bào)錯(cuò),解決這個(gè)問題也很簡(jiǎn)單,一般就是聲明使用系統(tǒng)Window的權(quán)限, 然后在設(shè)置dialog的type, dialog.getWindow().setType(LayoutParams.TYPE_SYSTEM_ERROR)

一般錯(cuò)誤如下

那產(chǎn)生這種情況,具體原因是什么呢?

首先說下結(jié)論,產(chǎn)生這個(gè)情況是因?yàn)閃indowManger實(shí)例的緣故;我們看代碼,分析具體的原因,首先肯定傳入的Context對(duì)象不同,而context的不同,會(huì)影響什么呢, 在上面dialog創(chuàng)建過程中說過, dialog中的WindowMange對(duì)象, 是Context#getSystemService獲得, 在Activity獲取WindowManger時(shí)候,我提到過Activity重寫了getSystemService方法, 看一下Activity#getSystemService的代碼

@Override
    public Object getSystemService(@ServiceName @NonNull String name) {
        ...
        // 獲取WMS時(shí), 直接返回自身的windowManger
        if (WINDOW_SERVICE.equals(name)) {
            return mWindowManager;
        }
        ...
        return super.getSystemService(name);
    }

在傳入的Context是Activity實(shí)例時(shí), 獲取到的WindowManger, 就是Activity自身的mWindowManager,而其他的context對(duì)象,獲取的都是默認(rèn)的WindowManger; 這2個(gè)WindowManger實(shí)例有什么不同呢, 上面也說到過,Activity的WindowManger創(chuàng)建過程中, 把其對(duì)應(yīng)的PhoneWindow實(shí)例傳給了WindowManager#mParentView, 最后在調(diào)用WindowManagerGlobal#addView時(shí), 會(huì)調(diào)用parentWindow.adjustLayoutParamsForSubWindow(wparams), 從而調(diào)整了LayoutParams, 設(shè)置了window的token;

下一個(gè)問題,調(diào)用dialog.getWindow().setType(LayoutParams.TYPE_SYSTEM_ERROR)為啥就不報(bào)錯(cuò)了呢?

首先,看一下這個(gè)錯(cuò)誤在哪兒拋出來的, 在上面的Activity的window顯示流程中,說道window創(chuàng)建過程,最后會(huì)調(diào)用android.view.ViewRootImpl#setView, 這個(gè)方法中, 會(huì)調(diào)用WMS服務(wù)端, 并且返回一個(gè)值, 根據(jù)返回值,確定WMS服務(wù)端是否添加成功, 如果返回值不是WindowManagerGlobal.ADD_OKAY, 就拋出的異常, 其中有一個(gè)異常就是;

接著在看WMS服務(wù)端,什么時(shí)候會(huì)返回這個(gè), 看一下com.android.server.wm.WindowManagerService#addWindow

//com.android.server.wm.WindowManagerService#addWindow
public int addWindow(Session session, IWindow client, int seq,
            WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
            Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
            InputChannel outInputChannel) {
              ...
        // 檢查權(quán)限, mPolicy是 com.android.server.policy.PhoneWindowManager 的實(shí)例
        int res = mPolicy.checkAddPermission(attrs, appOp);
        if (res != WindowManagerGlobal.ADD_OKAY) {
            return res;
        }
        ...
        // token為null時(shí), 對(duì)type進(jìn)行判斷
        WindowToken token = mTokenMap.get(attrs.token);
        if (token == null) {
            if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
                Slog.w(TAG, "Attempted to add application window with unknown token "
                      + attrs.token + ".  Aborting.");
                return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
            ...
          }
        .....
}

//com.android.server.policy.PhoneWindowManager#checkAddPermission
com.android.server.policy.PhoneWindowManager#checkAddPermission{
    int type = attrs.type;
    ....
    // 根據(jù)類型 判斷權(quán)限
    String permission = null;
    switch (type) {
        case TYPE_TOAST:
            // XXX right now the app process has complete control over
            // this...  should introduce a token to let the system
            // monitor/control what they are doing.
            outAppOp[0] = AppOpsManager.OP_TOAST_WINDOW;
            break;
        case TYPE_DREAM:
        case TYPE_INPUT_METHOD:
        case TYPE_WALLPAPER:
        case TYPE_PRIVATE_PRESENTATION:
        case TYPE_VOICE_INTERACTION:
        case TYPE_ACCESSIBILITY_OVERLAY:
            // The window manager will check these.
            break;
        case TYPE_PHONE:
        case TYPE_PRIORITY_PHONE:
        case TYPE_SYSTEM_ALERT:
        case TYPE_SYSTEM_ERROR:
        case TYPE_SYSTEM_OVERLAY:
            permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW;
            outAppOp[0] = AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
            break;
        default:
            permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
    }
    ....

    //權(quán)限不夠  也返回錯(cuò)誤代碼
    if (mContext.checkCallingOrSelfPermission(permission)
                    != PackageManager.PERMISSION_GRANTED) {
        return WindowManagerGlobal.ADD_PERMISSION_DENIED;
    }
    ...
}

上面代碼可以看見, 首先回去檢測(cè)權(quán)限, 如果是type系統(tǒng)的window, 則需要權(quán)限, 如果沒有權(quán)限, 最后一樣會(huì)拋出異常

當(dāng)token為null時(shí), 會(huì)去檢查type, 如果type和對(duì), 也會(huì)拋異常;

而Activity可以顯示dialog, 上面的代碼已經(jīng)分析了, 是對(duì)dialog的windowLayoutParams設(shè)置了token, 從而通過檢測(cè)

上面說了, 設(shè)置系統(tǒng)彈窗, 需要聲明權(quán)限, 沒有權(quán)限也會(huì)失敗,在國(guó)內(nèi)各種rom的手機(jī)上, 系統(tǒng)彈窗的權(quán)限,有可能會(huì)被默認(rèn)禁止掉, 有沒有 不用系統(tǒng)彈窗權(quán)限, 而顯示系統(tǒng)彈窗 的呢 其實(shí)也是可以的,可以設(shè)置彈窗的類型是Toast(其實(shí)toast也是一個(gè)window,下面再說), 從而規(guī)避這個(gè), 具體的分析可以看下面2篇博客 有些手機(jī)上彈框需要權(quán)限,通過TYPE_TOAST突破權(quán)限的分析 here, 和here


還有PopupWindow 和Toast的window, 以后在寫吧 - -

最后編輯于
?著作權(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)容