從問題出發,解析Activity、Window、View三者關係

從問題出發,往往能更明確的找到所求。本文將帶着一個個的問題,結合源碼,逐步解析Activity、Window、View的三者關係。

 

什麼地方需要window?

 

  • 一句話總結:有視圖的地方就需要window

  • Activity、Dialog、Toast...

 

PopupWindow和Dialog有什麼區別?

 

兩者最根本的區別在於有沒有新建一個window,PopupWindow沒有新建,而是將view加到DecorView;Dialog是新建了一個window,相當於走了一遍Activity中創建window的流程

 

PopupWindow的相關源碼

 

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);
}

 

private void invokePopup(WindowManager.LayoutParams p) {
    if (mContext != null) {
        p.packageName = mContext.getPackageName();
    }

    final PopupDecorView decorView = mDecorView;
    decorView.setFitsSystemWindows(mLayoutInsetDecor);

    setLayoutDirectionFromAnchor();

    mWindowManager.addView(decorView, p);

    if (mEnterTransition != null) {
        decorView.requestEnterTransition(mEnterTransition);
    }
}

 

從源碼中可以看出,PopupWindow最終是執行了mWindowManager.addView方法,全程沒有新建window

 

Dialog的相關源碼如下:

 

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
    if (createContextThemeWrapper) {
        if (themeResId == 0) {
            final TypedValue outValue = new TypedValue();
            context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
            themeResId = outValue.resourceId;
        }
        mContext = new ContextThemeWrapper(context, themeResId);
    } else {
        mContext = context;
    }

    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

    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);
}

 

很明顯,我們看到Dialog執行了window的創建:final Window w = new PhoneWindow(mContext);

 

一句話概括三者的基本關係

 

Activity中展示視圖元素通過window來實現,window可以理解爲一個容器,盛放着一個個的view,用來執行具體的展示工作。

 

各自是在何時被創建的?

 

以Activity舉例

 

Activity實例的創建

 

ActivityThread中執行performLaunchActivity,從而生成了Activity的實例:

 

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ...
    Activity activity = null;
    try {
        java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
        ...
    } catch (Exception e) {
        ...
    }

    try {
        ...
        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);
            ...
        }
        ...
    } catch (SuperNotCalledException e) {
        throw e;
    } catch (Exception e) {
        ...
    }

    return activity;
}

 

Activity中Window的創建

 

從上面的performLaunchActivity可以看出,在創建Activity實例的同時,會調用Activity的內部方法attach.

 

在該方法中完成window的初始化

 

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) {
    ...

    mWindow = new PhoneWindow(this);
    mWindow.setCallback(this);

    ...

    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    ...
}

 

DecorView的創建

 

用戶執行Activity的setContentView方法,內部是調用PhoneWindow的setContentView方法,在PhoneWindow中完成DecorView的創建

 

流程

1. Activity中的setContentView

2. PhoneWindow中的setContentView

3. PhoneWindow中的installDecor

 

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

@Override
public void setContentView(int layoutResID) {
    ...
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    ...
}

private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor();
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    }
    ...
}

 

Activity中的mDecor和Window裏面的mDecor有什麼關係?

 

  • 兩者指向同一個對象,都是DecorView

  • Activity中的mDecor是通過ActivityThread中的handleResumeActivity方法來賦值的

 

 

final void handleResumeActivity(IBinder token,
        boolean clearHide, boolean isForward, boolean reallyResume) {
    ...
    if (r != null) {
        ...

        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            ...
            a.mDecor = decor;
            ...
        } else if (!willBeVisible) {
            ...
        }
        ...
    } else {
        ...
    }
}

 

ViewRoot是什麼?ViewRootImpl又是什麼?

 

  1. ViewRoot的實現類是ViewRootImpl,是WindowManagerService和DecorView的紐帶

  2. ViewRoot不是View的根節點

  3. View的繪製是從ViewRootImpl的performTraversals方法開始的

 

ViewRootImpl何時創建?

 

當window被裝進WindowManager時,完成ViewRootImpl的創建,最終是通過WindowManagerGlobal.addView方法中進行創建的

 

public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
    ...
    root = new ViewRootImpl(view.getContext(), display);

    view.setLayoutParams(wparams);

    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);
    ...
    try {
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
      ...
    }
    ...
}

 

Activity中的Window何時被裝進WindowManager?

 

  • 發生在Activity的onResume階段

  • 執行順序

    • ActivityThread中的handleResumeActivity

    • Activity中的makeVisible

 

 

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) {
    ...
    if (r != null) {
        ...
        // The window is now visible if it has been added, we are not
        // simply finishing, and we are not starting another activity.
        if (!r.activity.mFinished && willBeVisible
                && r.activity.mDecor != null && !r.hideForNow) {
            ...
            if (r.activity.mVisibleFromClient) {
                r.activity.makeVisible();
            }
        }
        ...
    } else {
        ...
    }
}

 

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

 

什麼是WindowManagerGlobal?WindowManager、WindowManagerGlobal、WindowManagerImpl、WindowManagerPolicy有什麼區別?

 

WindowManagerGlobal中實現了ViewManager中addView、removeView、updateViewLayout這個三個view相關的方法。

 

WindowManager是一個接口類,對應的實現類是WindowManagerImpl,該實現類中持有mGlobal對象,這個mGlobal對象就是WindowManagerGlobal,具體的實現交給了WindowManagerGlobal,WindowManagerImpl相當於WindowManagerGlobal的代理類。

 

WindowManagerPolicy提供所有和UI有關的接口,PhoneWindowManager實現了該接口。需要注意的是PhoneWindowManager並不是WindowManager的子類。WindowManagerService中持有mPolicy對象,這個mPolicy就是PhoneWindowManager。

 

public class WindowManagerService extends IWindowManager.Stub implements Watchdog.Monitor, WindowManagerPolicy.WindowManagerFuncs {  
    ... 
    final WindowManagerPolicy mPolicy = new PhoneWindowManager();  
    ...
}

 

下面是Android視圖框架圖,可以直觀的看到window相關的整體設計

 

 

Window的作用是什麼?

 

  • 實現了Activity與View的解耦,Activity將視圖的全部工作都交給Window來處理

  • WindowManager支持對多條View鏈的管理

 

Dialog爲什麼不能使用Application的Context

 

  • Dialog窗口類型是TYPE_APPLICATION,與Activity一樣

  • TYPE_APPLICATION要求Token不能爲null,Application沒有AppWindowToken

 

源碼分析如下

 

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

 

public void setWindowManager(WindowManager wm, IBinder appToken, String appName)

 

上面是Dialog的初始化方法,注意setWindowManager方法中,第二個參數是appToken,Dialog初始化默認直接傳入的是null,這是與Activity的一個顯著區別。此時又會有一個新的疑問,那無論Context是來自Application還是Activity,都是傳入null,那爲什麼Application會報錯呢。

 

其實,上面的getSystemService也是問題的原因之一。

 

我們先看一下Activity中的getSystemService方法

 

@Override
public Object getSystemService(@ServiceName @NonNull String name) {
    if (getBaseContext() == null) {
        throw new IllegalStateException(
                "System services not available to Activities before onCreate()");
    }

    if (WINDOW_SERVICE.equals(name)) {
        return mWindowManager;
    } else if (SEARCH_SERVICE.equals(name)) {
        ensureSearchManager();
        return mSearchManager;
    }
    return super.getSystemService(name);
}

 

Activity中重寫了getSystemService方法,按照Dialog中初始化的代碼分析,此時返回的是當前Activity的mWindowManager,這個mWindowManager中存在Activity的Token。

 

Application中的Context沒有重寫getSystemService方法,那麼在setWindowManager內部,會通過執行createLocalWindowManager方法獲得一個WindowManager接口類的實現類WindowManagerImpl,在WindowManagerImpl中mDefaultToken默認也是null

 

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);
}

 

那麼爲什麼appToken是null,就會報錯呢?

 

Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
    at android.view.ViewRootImpl.setView(ViewRootImpl.java:685)
    at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
    at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93)
    at android.app.Dialog.show(Dialog.java:316)

 

從Logcat中的error信息可以看出,問題是出在了ViewRootImpl的setView方法中

 

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            ...
            int res; /* = WindowManagerImpl.ADD_OKAY; */
            ...
            try {
                ...
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                        getHostVisibility(), mDisplay.getDisplayId(),
                        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                        mAttachInfo.mOutsets, mInputChannel);
            } catch (RemoteException e) {
                ...
            } finally {
                ...
            }
            ...

            if (res < WindowManagerGlobal.ADD_OKAY) {
                ...
                switch (res) {
                    ...
                    case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
                        throw new WindowManager.BadTokenException(
                                "Unable to add window -- token " + attrs.token
                                + " is not for an application");
                    ...
                }
                ...
            }
            ...
        }
    }
}

 

從上述源碼中可以看出res在執行完addToDisplay方法後,被置爲了一個非法值,從而報錯。

 

mWindowSession是IWindowSession接口類,IWindowSession的實現類是Session,從Session的源碼中我們發現,最終是由WindowManagerService中的addWindow方法對res進行了賦值

 

@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
        int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
        Rect outOutsets, InputChannel outInputChannel) {
    return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
            outContentInsets, outStableInsets, outOutsets, outInputChannel);
}

 

下面是addWindow方法的源碼,我們看到由於AppWindowToken爲null,從而返回了非法值。

 

public int addWindow(Session session, IWindow client, int seq,
        WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
        Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
        InputChannel outInputChannel) {
    ...
    final int type = attrs.type;

    synchronized(mWindowMap) {
        ...
        WindowToken token = mTokenMap.get(attrs.token);
        if (token == null) {
            ...
        } else if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
            AppWindowToken atoken = token.appWindowToken;
            if (atoken == null) {
                Slog.w(TAG, "Attempted to add window with non-application token "
                      + token + ".  Aborting.");
                return WindowManagerGlobal.ADD_NOT_APP_TOKEN;
            } else if (atoken.removed) {
                ...
            }
            ...
        } 
        ...
    }
    ...
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章