1. 程式人生 > >解析Activity、Window、View三者關係

解析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的建立
  • 流程
    • Activity中的setContentView
    • PhoneWindow中的setContentView
    • 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又是什麼?

  • ViewRoot的實現類是ViewRootImpl,是WindowManagerService和DecorView的紐帶
  • ViewRoot不是View的根節點
  • 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相關的整體設計

image

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