Android視窗管理分析(3):視窗分組及Z-order的確定

看書的小蝸牛發表於2017-09-08

在Android系統中,視窗是有分組概念的,例如,Activity中彈出的所有PopupWindow會隨著Activity的隱藏而隱藏,可以說這些都附屬於Actvity的子視窗分組,對於Dialog也同樣如此,只不過Dialog與Activity屬於同一個分組。之間已經簡單介紹了視窗型別劃分:應用視窗、子視窗、系統視窗,Activity與Dialog都屬於應用視窗,而PopupWindow屬於子視窗,Toast、輸入法等屬於系統視窗。只有應用視窗與系統視窗可以作為父視窗,子視窗不能作為子視窗的父視窗,也就說Activity與Dialog或者系統視窗中可以彈出PopupWindow,但是PopupWindow不能在自己內部彈出PopupWindow子視窗。日常開發中,一些常見的問題都同視窗的分組有關係,比如為什麼新建Dialog的時候必須要用Activity的Context,而不能用Application的;為什麼不能以PopupWindow的View為錨點彈出子PopupWindow?其實這裡面就牽扯都Android的視窗組織管理形式,本文主要包含以下幾點內容:

  • 視窗的分組管理 :應用視窗組、子視窗組、系統視窗組
  • Activity、Dialg應用視窗及PopWindow子視窗的新增原理跟注意事項
  • 視窗的Z次序管理:視窗的分配序號、次序調整等
  • WMS中視窗次序分配如何影響SurfaceFlinger服務

WMS視窗新增一文中分析過,視窗的新增是通過WindowManagerGlobal.addView()來完成 函式原型如下

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow)複製程式碼

前三個引數是必不可少的,view、params、display,其中display表示要輸出的顯示裝置,先不考慮。view 就是APP要新增到WindowManagerGlobal管理的View,而 params是WindowManager.LayoutParams,主要用來描述視窗屬性,WindowManager.LayoutParams有兩個很重要的引數type與token,

public static class LayoutParams extends ViewGroup.LayoutParams
        implements Parcelable {
  ...
  public int type;
  ...
  public IBinder token = null;

  }複製程式碼

type用來描述視窗的型別,而token其實是標誌視窗的分組,token相同的視窗屬於同一分組,後面會知道這個token其實是WMS在APP端對應的一個WindowToken的鍵值。這裡先看一下type引數,之前曾新增過Toast視窗,它的type值是TYPE_TOAST,標識是一個系統提示視窗,下面先簡單看下三種視窗型別的Type對應的值,首先看一下應用視窗

視窗TYPE值 視窗型別
FIRST_APPLICATION_WINDOW = 1 開始應用程式視窗
TYPE_BASE_APPLICATION=1 所有程式視窗的base視窗,其他應用程式視窗都顯示在它上面
TYPE_APPLICATION =2 普通應用程式視窗,token必須設定為Activity的token
TYPE_APPLICATION_STARTING =3 應用程式啟動時所顯示的視窗
LAST_APPLICATION_WINDOW = 99 結束應用程式視窗

一般Activity都是TYPE_BASE_APPLICATION型別的,而TYPE_APPLICATION主要是用於Dialog,再看下子視窗型別

視窗TYPE值 視窗型別
FIRST_SUB_WINDOW = 1000 SubWindows子視窗,子視窗的Z序和座標空間都依賴於他們的宿主視窗
TYPE_APPLICATION_PANEL =1000 皮膚視窗,顯示於宿主視窗的上層
TYPE_APPLICATION_MEDIA =1001 媒體視窗(例如視訊),顯示於宿主視窗下層
TYPE_APPLICATION_SUB_PANEL =1002 應用程式視窗的子皮膚,顯示於所有皮膚視窗的上層
TYPE_APPLICATION_ATTACHED_DIALOG = 1003 對話方塊,類似於皮膚視窗,繪製類似於頂層視窗,而不是宿主的子視窗
TYPE_APPLICATION_MEDIA_OVERLAY =1004 媒體資訊,顯示在媒體層和程式視窗之間,需要實現半透明效果
LAST_SUB_WINDOW=1999 結束子視窗

最後看幾個系統視窗型別,

視窗TYPE值 視窗型別
FIRST_SYSTEM_WINDOW = 2000 系統視窗
TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW 狀態列
TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW+3 系統提示,出現在應用程式視窗之上
TYPE_TOAST = FIRST_SYSTEM_WINDOW+5 顯示Toast

瞭解視窗型別後,我們需要面對的首要問題是:視窗如何根據型別進行分組歸類的?Dialog是如何確定附屬Activity,PopupWindow如何確定附屬父視窗?

視窗的分組原理

如果用一句話概括視窗分組的話:Android視窗是以token來進行分組的,同一組視窗握著相同的token,什麼是token呢?在 Android WMS管理框架中,token一個IBinder物件,IBinder在實體端與代理端會相互轉換,這裡只看實體端,它的取值只有兩種:ViewRootImpl中ViewRootImpl.W,或者是ActivityRecord中的IApplicationToken.Stub物件,其中ViewRootImpl.W的實體物件在ViewRootImpl中例項化,而IApplicationToken.Stub在ActivityManagerService端例項化,之後被AMS新增到WMS服務中去,作為Activity應用視窗的鍵值標識。之前說過Activity跟Dialog屬於同一分組,現在就來看一下Activity跟Dialog的token是如何複用的,這裡的複用分為APP端及WMS服務端,關於視窗的新增流程之前已經分析過,這裡只跟隨視窗token來分析視窗的分組,我們知道在WMS端,WindowState與視窗的一一對應,而WindowToken與視窗分組,這可以從兩者的定義看出如下:

class WindowToken {

    final WindowManagerService service;
    final IBinder token;
    final int windowType;
    final boolean explicit;
    <!--當前視窗對應appWindowToken,是不是同Activity存在依附關係-->
    AppWindowToken appWindowToken;
    <!--關鍵點1 當前WindowToken對應的視窗列表-->
    final WindowList windows = new WindowList();
    ...
}

final class WindowState implements WindowManagerPolicy.WindowState {
    static final String TAG = "WindowState";

    final WindowManagerService mService;
    final WindowManagerPolicy mPolicy;
    final Context mContext;
    final Session mSession;
    <!--當前WindowState對應IWindow視窗代理-->
    final IWindow mClient;
    <!--當前WindowState對應的父視窗-->
    final WindowState mAttachedWindow;
    ...
    <!--當前WindowState隸屬的token-->
    WindowToken mToken;
    WindowToken mRootToken;
    AppWindowToken mAppToken;
    AppWindowToken mTargetAppToken;
    ...
    }複製程式碼

可以看到WindowToken包含一個 WindowList windows = new WindowList(),其實就是WindowState列表;而WindowState有一個WindowToken mToken,也就是WindowToken包含一個WindowState列表,而每個WindowState附屬一個WindowToken視窗組,示意圖如下:

WindowToken與WindowState關係.jpg
WindowToken與WindowState關係.jpg

Activity對應token及WindowToken(AppWindowToken)的新增

AMS在為Activity建立ActivityRecord的時候,會新建IApplicationToken.Stub appToken物件,在startActivity之前會首先向WMS服務登記當前Activity的Token,隨後,通過Binder通訊將IApplicationToken傳遞給APP端,在通知ActivityThread新建Activity物件之後,利用Activity的attach方法新增到Activity中,先看第一步AMS將Activity的token加入到WMS中,並且為Activity建立APPWindowToken。

<!--AMS ActivityStack.java中程式碼 -->
 final void startActivityLocked(ActivityRecord r, boolean newTask,
            boolean doResume, boolean keepCurTransition, Bundle options) {
    ...<!--關鍵點1  新增Activity token到WMS-->
    mWindowManager.addAppToken(task.mActivities.indexOf(r), r.appToken,XXX);
   }複製程式碼
  @Override
    public void addAppToken(int addPos, IApplicationToken token, int taskId, int stackId,
            int requestedOrientation, boolean fullscreen, boolean showForAllUsers, int userId,
            int configChanges, boolean voiceInteraction, boolean launchTaskBehind) {
             synchronized(mWindowMap) {
             <!--新建AppWindowToken-->
            AppWindowToken atoken = findAppWindowToken(token.asBinder());
            atoken = new AppWindowToken(this, token, voiceInteraction);
            ...
            <!--將AppWindowToken以IApplicationToken.Stub為鍵值放如WMS的mTokenMap中-->
            mTokenMap.put(token.asBinder(), atoken);
            <!--開始肯定是隱藏狀態,因為還沒有resume-->
            atoken.hidden = true;
            atoken.hiddenRequested = true;
        }
    }複製程式碼

也就是說Activity分組的Token其實是早在Activity顯示之前就被AMS新增到WMS中去的,之後AMS才會通知App端去新建Activity,並將Activity的Window新增到WMS中去,接著看下APP端的流程:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    <!--關鍵點1 新建Activity-->
    Activity activity = null;
    try {
        java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
      ...
   try {
        Application app = r.packageInfo.makeApplication(false, mInstrumentation);
        if (activity != null) {
        <!--關鍵點2 新建appContext-->
            Context appContext = createBaseContextForActivity(r, activity);
            CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
            Configuration config = new Configuration(mCompatConfiguration);
        <!--關鍵點3 attach到WMS-->
           activity.attach(appContext, this, getInstrumentation(), r.token,XXX);
          ...
      } 複製程式碼

關鍵點1,新建一個Activity,之後會為Activiyt建立一個appContext,這個Context主要是為了activity.attach使用的,其實就是單純new一個ContextImpl,之後Activity會利用attach函式將ContextImpl繫結到自己身上。

static ContextImpl createActivityContext(ActivityThread mainThread,
        LoadedApk packageInfo, int displayId, Configuration overrideConfiguration) {
    return new ContextImpl(null, mainThread, packageInfo, null, null, false,
            null, overrideConfiguration, displayId);
}

 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) {
        <!--關鍵點1 為Activity繫結ContextImpl 因為Activity只是一個ContextWraper-->
        attachBaseContext(context);
        mFragments.attachHost(null /*parent*/);
        <!--關鍵點2 new一個PhoneWindow 並設定回撥-->
        mWindow = new PhoneWindow(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        ...
        <!--關鍵點3 Token的傳遞-->
        mToken = token;
        mIdent = ident;
        mApplication = application;
        ...
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        <!--將Window的WindowManager賦值給Activity-->
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;
    }複製程式碼

mWindow.setWindowManager並不是直接為Window設定WindowManagerImpl,而是利用當前的WindowManagerImpl重新為Window建立了一個WindowManagerImpl,並將自己設定此WindowManagerImpl的parentWindow:

public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
        boolean hardwareAccelerated) {
    mAppToken = appToken;
    mAppName = appName;
    mHardwareAccelerated = hardwareAccelerated
            || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
    if (wm == null) {
        wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
    }
    mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

 public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
    return new WindowManagerImpl(mDisplay, parentWindow);
}複製程式碼

之後將Window的WindowManagerImpl傳遞給Activity,作為Activity的WindowManager將來Activity通過getSystemService獲取WindowManager服務的時候,其實是直接返回了Window的WindowManagerImpl,

@Override
public Object getSystemService(String name) {

    if (WINDOW_SERVICE.equals(name)) {
        return mWindowManager;
    } else if (SEARCH_SERVICE.equals(name)) {
        ensureSearchManager();
        return mSearchManager;
    }
    return super.getSystemService(name);
}複製程式碼

之後看一下關鍵點3,這裡傳遞的token其實就是AMS端傳遞過來的IApplicationToken代理,一個IBinder物件。之後利用ContextImpl的getSystemService()函式得到一個一個WindowManagerImpl物件,再通過setWindowManager為Activity建立自己的WindowManagerImpl。到這一步,Activity已經準備完畢,剩下的就是在resume中通過addView將視窗新增到到WMS,具體實現在ActivityThread的handleResumeActivity函式中:

 final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {
        ActivityClientRecord r = performResumeActivity(token, clearHide);

        if (r != null) {
            final Activity a = r.activity;
            ...
            if (r.window == null && !a.mFinished && willBeVisible) {
               <!--關鍵點1-->
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                <!--關鍵點2 獲取WindowManager-->
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (a.mVisibleFromClient) {
                    a.mWindowAdded = true;
                <!--關鍵點3 新增到WMS管理-->
                    wm.addView(decor, l);
                }
             ...
             }   複製程式碼

關鍵點1是為了獲取Activit的Window及DecorView物件,如果使用者沒有通過setContentView方式新建DecorView,這裡會利用PhoneWindow的getDecorView()新建DecorView,

@Override
public final View getDecorView() {
    if (mDecor == null) {
        installDecor();
    }
    return mDecor;
}複製程式碼

之後通過Activity的getWindowManager()獲取WindowManagerImpl物件,這裡獲取的WindowManagerImpl其實是Activity自己的WindowManagerImpl,

private WindowManagerImpl(Display display, Window parentWindow) {
    mDisplay = display;
    mParentWindow = parentWindow;
}複製程式碼

它的mParentWindow 是非空的,獲取WindowManagerImpl之後,便利用 addView(decor, l)將DecorView對應的視窗新增到WMS中去,最後呼叫的是

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

可以看到這裡會傳遞mParentWindow給WindowManagerGlobal物件,作為調整WindowMangaer.LayoutParams 中token的依據:

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {

    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    <!--調整wparams的token引數-->
    if (parentWindow != null) {
        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    } 
     ViewRootImpl root;
     View panelParentView = null;
         ..
        <!--新建ViewRootImpl ,並利用wparams引數新增視窗-->
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
       ..
      <!--新建ViewRootImpl -->
      root.setView(view, wparams, panelParentView);
     }複製程式碼

parentWindow.adjustLayoutParamsForSubWindow是一個很關鍵的函式,從名字就能看出,這是為了他調整子視窗的引數:

   void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
        CharSequence curTitle = wp.getTitle();
        <!--如果是子視窗如何處理-->
        if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
            wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
            <!--後面會看到,其實PopupWindow類的子視窗的wp.token是在上層顯示賦值的-->
            if (wp.token == null) {
                View decor = peekDecorView();
                if (decor != null) {
                    // 這裡其實是父視窗的IWindow物件 Window只有Dialog跟Activity才有
                    wp.token = decor.getWindowToken();
                }
            }

        } else {
        <!--這裡其實只對應用視窗有用 Activity與Dialog都一樣-->
            if (wp.token == null) {
                wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
            }
        }
    }複製程式碼

對於Activity來說,wp.token = mContainer == null ? mAppToken : mContainer.mAppToken,其實就是AMS端傳過來的IApplicationToken,之後在ViewRootImpl中setView的時候,會利用IWindowSession代理與WMS端的Session通訊,將視窗以及token資訊傳遞到WMS端,其中IApplicationToken就是該Activity所處於的分組,在WMS端,會根據IApplicationToken IBinder鍵值,從全域性的mTokenMap中找到對應的AppWindowToken。既然說分組,就應該有其他的子元素,下面看一下Activity上彈出Dialog的流程,進一步瞭解為什麼Activity與它彈出的Dialog是統一分組(複用同一套token)。

Dialg分組及顯示原理:為什麼Activity與Dialog算同一組?

在新增到WMS的時候,Dialog的視窗屬性是WindowManager.LayoutParams.TYPE_APPLICATION,同樣屬於應用視窗,因此,必須使用Activity的AppToken才行,換句話說,必須使用Activity內部的WindowManagerImpl進行addView才可以。Dialog和Activity共享同一個WindowManager(也就是WindowManagerImpl),而WindowManagerImpl裡面有個Window型別的mParentWindow變數,這個變數在Activity的attach中建立WindowManagerImpl時傳入的為當前Activity的Window,而Activity的Window裡面的mAppToken值又為當前Activity的token,所以Activity與Dialog共享了同一個mAppToken值,只是Dialog和Activity的Window物件不同,下面用程式碼確認一下:

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
<!--關鍵點 1 根據theme封裝context-->
    if (createContextThemeWrapper) {
        ...
        mContext = new ContextThemeWrapper(context, themeResId);
    } else {
        mContext = context;
    }
        <!--獲取mWindowManager-->

    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    <!--建立PhoneWindow-->
    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);
}複製程式碼

以上程式碼先根據Theme調整context,之後利用context.getSystemService(Context.WINDOW_SERVICE),這裡Dialog是從Activity彈出來的,所以context是Activity,如果你設定Application,會有如下error,至於為什麼,後面分析會看到。

 android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
       at android.view.ViewRootImpl.setView(ViewRootImpl.java:563)
       at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:269)
       at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)複製程式碼

接著看Activity的getSystemService,上文分析過這種方法獲取的其實是Activity中PhoneWindow的WindowManagerImpl,所以後面利用WindowManagerImpl addView的時候,走的流程與Activity一樣。看一下show的程式碼:

public void show() {
    ...
    onStart();
    mDecor = mWindow.getDecorView();
    ...
    <!--關鍵點 WindowManager.LayoutParams的獲取-->
    WindowManager.LayoutParams l = mWindow.getAttributes();
    ...
    try {
        mWindowManager.addView(mDecor, l);
        mShowing = true;
        sendShowMessage();
    } finally {
    }
}複製程式碼

Window在建立的時候,預設新建WindowManager.LayoutParams mWindowAttributes

private final WindowManager.LayoutParams mWindowAttributes =
    new WindowManager.LayoutParams();複製程式碼

採用的是無參構造方法,

    public LayoutParams() {
        super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        type = TYPE_APPLICATION;
        format = PixelFormat.OPAQUE;
    }複製程式碼

因此這裡的type = TYPE_APPLICATION,也就是說Dialog的視窗型別其實是應用視窗。因此在addView走到上文的adjustLayoutParamsForSubWindow的時候,仍然按照Activity的WindowManagerImpl addView的方式處理,並利用Activity的PhoneWindow的 adjustLayoutParamsForSubWindow調整引數,賦值給WindowManager.LayoutParams token的值仍然是Activity的IApplicationToken,同樣在WMS端,對應就是APPWindowToken,也就是Activity與Dialog屬於同一分組。

   void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
        CharSequence curTitle = wp.getTitle();
                 <!--這裡其實只對應用視窗有用 Activity與Dialog都一樣-->
            if (wp.token == null) {
                wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
        }
    }複製程式碼

回到之前遺留的一個問題,為什麼Dialog用Application作為context不行呢?Dialog的視窗型別屬於應用視窗,如果採用Application作為context,那麼通過context.getSystemService(Context.WINDOW_SERVICE)獲取的WindowManagerImpl就不是Activity的WindowManagerImpl,而是Application,它同Activity的WindowManagerImpl的區別是沒有parentWindow,所以adjustLayoutParamsForSubWindow函式不會被呼叫,WindowManager.LayoutParams的token就不會被賦值,因此ViewRootImpl在通過setView向WMS在新增視窗的時候會失敗:

public int addWindow(Session session, IWindow client, XXX )
        ...
        <!--對於應用視窗 token不可以為null-->
        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;
            }複製程式碼

WMS會返回WindowManagerGlobal.ADD_BAD_APP_TOKEN的錯誤給APP端,APP端ViewRootImpl端收到後會丟擲如下異常

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
                   ....
                    case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
                        throw new WindowManager.BadTokenException(
                                "Unable to add window -- token " + attrs.token
                                + " is not for an application");複製程式碼

以上就為什麼不能用Application作為Dialog的context的理由(不能為Dialog提供正確的token),接下來看一下PopupWindow是如何處理分組的。

PopupWindow是最典型的子視窗,必須依附父視窗才能存在,先看下PopupWindow一般用法:

     View root = LayoutInflater.from(AppProfile.getAppContext()).inflate(R.layout.pop_window, null);
    PopupWindow popupWindow = new PopupWindow(root, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, true);
    popupWindow.setBackgroundDrawable(new BitmapDrawable());
    popupWindow.showAsDropDown(archorView);複製程式碼

PopupWindow的建構函式很普通,主要是一些預設入場、出廠動畫的設定,如果在新建PopupWindow的時候已經將根View傳遞到建構函式中去,PopupWindow的建構函式會呼叫setContentView,如果在show之前,沒有呼叫setContentView,則丟擲異常。

public PopupWindow(View contentView, int width, int height, boolean focusable) {
    if (contentView != null) {
        mContext = contentView.getContext();
        mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
    }
    setContentView(contentView);
    setWidth(width);
    setHeight(height);
    setFocusable(focusable);
}複製程式碼

下面主要看PopupWindow的showAsDropDown函式

public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
    <!--關鍵點1  利用通過View錨點所在視窗顯性構建PopupWindow的token-->
    final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken());
    <!--關鍵點2-->
    preparePopup(p);
    ...
    <!--關鍵點3-->
    invokePopup(p);
}複製程式碼

showAsDropDown有3個關鍵點,關鍵點1是生成WindowManager.LayoutParams引數,WindowManager.LayoutParams引數裡面的type、token是非常重要引數,PopupWindow的type是TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW,是一個子視窗。關鍵點2是PopupDecorView的生成,這個View是PopupWindow的根ViewGroup,類似於Activity的DecorView,關鍵3利用WindowManagerService的代理,將View新增到WMS視窗管理中去顯示,先看關鍵點1:

private WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
    final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
    p.gravity = computeGravity();
    p.flags = computeFlags(p.flags);
    p.type = mWindowLayoutType;
    <!--顯性賦值token-->
    p.token = token;
    p.softInputMode = mSoftInputMode;
    p.windowAnimations = computeAnimationResource();
    if (mBackground != null) {
        p.format = mBackground.getOpacity();
    } else {
        p.format = PixelFormat.TRANSLUCENT;
    }
    ..
    p.privateFlags = PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH
            | PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME;
    return p;
}複製程式碼

上面的Token其實用的是anchor.getWindowToken(),如果是Activity中的View,其實用的Token就是Activity的ViewRootImpl中的IWindow物件,如果這個View是一個系統視窗中的View,比如是Toast視窗中彈出來的,用的就是Toast ViewRootImpl的IWindow物件,歸根到底,PopupWindow自視窗中的Token是ViewRootImpl的IWindow物件,同Activity跟Dialog的token(IApplicationToken)不同,該Token標識著PopupWindow在WMS所處的分組,最後來看一下PopupWindow的顯示:

private void invokePopup(WindowManager.LayoutParams p) {
    if (mContext != null) {
        p.packageName = mContext.getPackageName();
    }
    final PopupDecorView decorView = mDecorView;
    decorView.setFitsSystemWindows(mLayoutInsetDecor);
    setLayoutDirectionFromAnchor();
    <!--關鍵點1-->
    mWindowManager.addView(decorView, p);
    if (mEnterTransition != null) {
        decorView.requestEnterTransition(mEnterTransition);
    }
}複製程式碼

主要是呼叫了WindowManager的addView新增檢視並顯示,這裡首先需要關心一下mWindowManager,

    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);複製程式碼

這的context 可以是Activity,也可以是Application,因此WindowManagerImpl也可能不同,不過這裡並沒有多大關係,因為PopupWindow的token是顯性賦值的,就是是就算用Application,也不會有什麼問題,對於PopupWindow子視窗,關鍵點是View錨點決定其token,而不是WindowManagerImpl物件:

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}複製程式碼

之後利用ViewRootImpl的setView函式的時候,WindowManager.LayoutParams裡的token其實就是view錨點獲取的IWindow物件,WindowManagerService在處理該請求的時候,

public int addWindow(Session session, IWindow client, XXX ) {

      <!--關鍵點1,必須找到子視窗的父視窗,否則新增失敗-->
       WindowState attachedWindow = null;
        if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
            attachedWindow = windowForClientLocked(null, attrs.token, false);
            if (attachedWindow == null) {
                return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
            }
        }
        <!--關鍵點2 如果Activity第一次新增子視窗 ,子視窗分組對應的WindowToken一定是null-->
        boolean addToken = false;
        WindowToken token = mTokenMap.get(attrs.token);
        AppWindowToken atoken = null;
        if (token == null) {
        ...
            token = new WindowToken(this, attrs.token, -1, false);
            addToken = true;
        }             
        <!--關鍵點2 新建視窗WindowState物件 注意這裡的attachedWindow非空-->
       WindowState win = new WindowState(this, session, client, token,
                attachedWindow, appOp[0], seq, attrs, viewVisibility, displayContent);
       ...
        <!--關鍵點4 新增更新全部map,-->
        if (addToken) {
            mTokenMap.put(attrs.token, token);
         }
        mWindowMap.put(client.asBinder(), win);
        }複製程式碼

從上面的分析可以看出,WMS會為PopupWindow視窗建立一個子視窗分組WindowToken,每個子視窗都會有一個指向父視窗的引用,因為是利用父視窗的IWindow作為鍵值,父視窗可以很方便的利用自己的IWindow獲取WindowToken,進而得到全部的子視窗,

關於系統視窗,前文層分析過Toast系統視窗,Toast類系統視窗在WMS端只有一個WindowToken,鍵值是null,這個比較奇葩,不過還沒驗證過。

視窗的Z次序管理:視窗的分配序號、次序調整等

雖然我們看到的手機螢幕只是一個二維平面X*Y,但其實Android系統是有隱形的Z座標軸的,其方向與手機螢幕垂直,與我們的實現平行,所以並不能感知到。

Z order.jpg
Z order.jpg

前面分析了視窗分組的時候涉及了兩個物件WindowState與Windtoken,但僅限分組,分組無法決定視窗的顯示的Z-order,那麼再WMS是怎麼管理所有視窗的Z-order的? 在WMS中視窗被抽象成WindowState,因此WindowState內部一定有屬性來標誌這個視窗的Z-order,實現也確實如此,WindowState採用三個個int值mBaseLayer+ mSubLayer + mLayer 來標誌視窗所處的位置,前兩個主要是根據視窗型別確定視窗位置,mLayer才是真正的值,定義如下:

final class WindowState implements WindowManagerPolicy.WindowState {

    final WindowList mChildWindows = new WindowList();
    final int mBaseLayer;
    final int mSubLayer;
     <!--最終Z次序的賦值-->
   int mLayer;

    }複製程式碼

從名字很容知道mBaseLayer是標誌視窗的主次序,面向的是一個視窗組,而mSubLayer主要面向單獨視窗,要來標誌一個視窗在一組視窗中的位置,對兩者來說值越大,視窗越靠前,從此final屬性知道,兩者的值是不能修改的,而mLayer可以修改,對於系統視窗,一般不會同時顯示兩個,因此,可以用主序決定,比較特殊的就是Activity與子視窗,首先子視窗的主序肯定是父視窗決定的,子視窗只關心次序就行。而父視窗的主序卻相對麻煩,比如對於應用視窗來說,他們的主序都是一樣的,因此還要有一個其他的維度來作為參考,比如對於Activity,主序都是一樣的,怎麼定他們真正的Z-order呢?其實Activity的順序是由AMS保證的,這個順序定了,WMS端Activity視窗的順序也是定了,這樣下來次序也方便定了

WindowState(WindowManagerService service, Session s, IWindow c, WindowToken token,
           WindowState attachedWindow, int appOp, int seq, WindowManager.LayoutParams a,
           int viewVisibility, final DisplayContent displayContent) {
        ...
            <!--關鍵點1  子視窗型別的Z order-->
        if ((mAttrs.type >= FIRST_SUB_WINDOW &&
                mAttrs.type <= LAST_SUB_WINDOW)) {
            mBaseLayer = mPolicy.windowTypeToLayerLw(
                    attachedWindow.mAttrs.type) * WindowManagerService.TYPE_LAYER_MULTIPLIER
                    + WindowManagerService.TYPE_LAYER_OFFSET;
            mSubLayer = mPolicy.subWindowTypeToLayerLw(a.type);
            mAttachedWindow = attachedWindow;                final WindowList childWindows = mAttachedWindow.mChildWindows;
            final int numChildWindows = childWindows.size();
            if (numChildWindows == 0) {
                childWindows.add(this);
            } else {
             ...
        } else {
            <!--關鍵點2  普通視窗型別的Z order-->
            mBaseLayer = mPolicy.windowTypeToLayerLw(a.type)
                    * WindowManagerService.TYPE_LAYER_MULTIPLIER
                    + WindowManagerService.TYPE_LAYER_OFFSET;
            mSubLayer = 0;
            mAttachedWindow = null;
            mLayoutAttached = false;
        }
       ...
    }複製程式碼

由於視窗所能選擇的型別是確定的,因此mBaseLayer與mSubLayer所能選擇的值只有固定幾個,很明顯這兩個引數不能精確的確定Z-order,還會有其他微調的手段,也僅限微調,在系統層面,決定了不同型別視窗所處的位置,比如系統Toast型別的視窗一定處於所有應用視窗之上,不過我們最關心的是Activity類的視窗如何確定Z-order的,在new WindowState之後,只是粗略的確定了Activity視窗的次序,看一下新增視窗的示意程式碼:

addWindow(){
    <!--1-->
    new WindowState
    <!--2-->
    addWindowToListInOrderLocked(win, true);
    <!--3-->
    assignLayersLocked(displayContent.getWindowList());
     }複製程式碼

新建state物件之後,Z-order還要通過addWindowToListInOrderLocked及assignLayersLocked才能確定,addWindowToListInOrderLocked主要是根據視窗的Token找到歸屬,插入到對應Token的WindowState列表,如果是子視窗還要插入到父視窗的對應位置中:

次序確定.jpg
次序確定.jpg

插入到特定位置後其實Z-order就確定了,接下來就是通過assignLayersLocked為WindowState分配真正的Z-order mLayer,

   private final void assignLayersLocked(WindowList windows) {
        int N = windows.size();
        int curBaseLayer = 0;
        int curLayer = 0;
        int i;

        boolean anyLayerChanged = false;
            for (i=0; i<N; i++) {
            final WindowState w = windows.get(i);
            final WindowStateAnimator winAnimator = w.mWinAnimator;
            boolean layerChanged = false;
            int oldLayer = w.mLayer;
            if (w.mBaseLayer == curBaseLayer || w.mIsImWindow
                    || (i > 0 && w.mIsWallpaper)) {
                <!--通過偏移量-->
                curLayer += WINDOW_LAYER_MULTIPLIER;
                w.mLayer = curLayer;
            } else {
                curBaseLayer = curLayer = w.mBaseLayer;
                w.mLayer = curLayer;
            }
            if (w.mLayer != oldLayer) {
                layerChanged = true;
                anyLayerChanged = true;
            }
            ...
    }複製程式碼

mLayer最終確定後,視窗的次序也就確定了,這個順序要最終通過後續的relayout更新到SurfaceFlinger服務,之後,SurfaceFlinger在圖層混排的時候才知道如何處理。

WMS中視窗次序分配如何影響SurfaceFlinger服務

SurfaceFlinger在圖層混排的時候應該不會混排所有的視窗,只會混排可見的視窗,比如有多個全屏Activity的時候,SurfaceFlinger只會處理最上面的,那麼SurfaceFlinger如何知道哪些視窗可見哪些不可見呢?前文分析了WMS分配Z-order之後,要通過setLayer更新到SurfaceFlinger,接下來看具體流程,建立SurfaceControl之後,會建立一次事務,確定Surface的次序:

   SurfaceControl.openTransaction();
            try {
                mSurfaceX = left;
                mSurfaceY = top;
                    try {
                    mSurfaceControl.setPosition(left, top);
                    mSurfaceLayer = mAnimLayer;
                    final DisplayContent displayContent = w.getDisplayContent();
                    if (displayContent != null) {
                        mSurfaceControl.setLayerStack(displayContent.getDisplay().getLayerStack());
                    }
                    <!--設定次序-->
                    mSurfaceControl.setLayer(mAnimLayer);
                    mSurfaceControl.setAlpha(0);
                    mSurfaceShown = false;
                } catch (RuntimeException e) {
                    mService.reclaimSomeSurfaceMemoryLocked(this, "create-init", true);
                }
                mLastHidden = true;
            } finally {
                SurfaceControl.closeTransaction();
            }
        }複製程式碼

這裡通過openTransaction與closeTransaction保證一次事務的完整性,中間就Surface次序的調整,closeTransaction會與SurfaceFlinger通訊,通知SurfaceFlinger更新Surface資訊,這其中就包括Z-order。

總結

本文簡要分析了Android視窗的分組,以及WMS視窗次序的確定,最後簡單提及了一下視窗次序如何更新到SurfaceFlinger服務的,也方便將來理解圖層合成。

作者:看書的小蝸牛
原文連結:Android視窗管理分析(3):視窗分組及Z-order的確定
僅供參考,歡迎指正

相關文章