爲什麼不能使用 Application Context 顯示 Dialog?

Android 複習筆記目錄

  1. 嘮嘮任務棧,返回棧和生命週期
  2. 嘮嘮 Activity 的生命週期
  3. 扒一扒 Context
  4. 爲什麼不能使用 Application Context 顯示 Dialog?

本文永久更新地址: https://xiaozhuanlan.com/topic/3958126407

目錄

  • 爲什麼不能使用 Application Context 顯示 Dialog?
  • 誰創建了 Token?
  • WMS 是如何拿到 Token 的?
  • WMS 是如何校驗 Token 的?

爲什麼不能使用 Application Context 顯示 Dialog?

在上一篇文章 扒一扒 Context 中遺留了一個問題:

爲什麼不能使用 Application Context 顯示 Dialog ?

寫個簡單的測試代碼,如下:

Dialog dialog = new Dialog(getApplicationContext());
dialog.show();

運行時會得到這樣一個錯誤:

Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
        at android.view.ViewRootImpl.setView(ViewRootImpl.java:951)
        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:387)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:96)
        at android.app.Dialog.show(Dialog.java:344)
        at luyao.android.context.ContextActivity.showDialog(ContextActivity.java:31)

看到報錯信息中的 token ,不知道你還記不記得上篇文章中介紹過的 Activity.attach() 方法:

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 window, ActivityConfigCallback activityConfigCallback)
 
{
    // 回調 attachBaseContext()
    attachBaseContext(context);

    ......

    // 創建 PhoneWindow
    mWindow = new PhoneWindow(this, window, activityConfigCallback);

    ......    

    // 第二個參數是 mToken
    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    ......
}

Activity 被創建之後會調用 attach() 方法,做了這麼幾件事:

  • 創建了 PhoneWindow 對象 mWondow
  • 給當前 window 綁定 mToken
  • ......

這裏的 IBinder 對象 mToken 很重要。它是一個 Binder 對象,可以在 app 進程,system_server 進程之間進行傳遞。和我們通常所說的 Token 一樣,這裏也可以把它看做是一種特殊的令牌,用來標識 Window ,在對 Window 進行視圖操作的時候就可以做一些校驗工作。

所以,Activity 對應的 Window/WMS 都是持有這個 mToken 的。結合之前 Application 創建 Dialog 的報錯信息,我們可以大膽猜測 Application Context 創建 Dialog 的過程中,並沒有實例化類似的 token。

回到 Dialog 的構造函數中,

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
    ......

    // 獲取 WindowManager
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

    final Window w = new PhoneWindow(mContext);
    mWindow = w;
    ......
}

根據傳入的 Context 調用 getSystemService(Context.WINDOW_SERVICE) 方法來得到 WindowManager 對象 mWindowManager ,最終會通過 mWindowManager.addWindow() 來顯示 dialog 。

如果傳入的 Context 是 Activity,返回的是在 Activity.attach() 方法中創建的 mWindowManager 對象,這個時候 mToken 也已經綁定。

> Activity.java

@Override
public Object getSystemService(@ServiceName @NonNull String name) {
    ......

    if (WINDOW_SERVICE.equals(name)) {
        return mWindowManager; // 在 attach() 方法中已經實例化
    } else if (SEARCH_SERVICE.equals(name)) {
        ensureSearchManager();
        return mSearchManager;
    }
    return super.getSystemService(name);
}

如果傳入的 Context 是 Application,最終調用的是父類 ContextImpl 的方法。

@Override
public Object getSystemService(String name) {
    return SystemServiceRegistry.getSystemService(this, name);
}

SystemServiceRegistry.getSystemService(this, name) 拿到的是已經提前初始化完成並緩存下來的系統服務,並沒有攜帶任何的 Token。

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

所以,Android 不允許 Activity 以外的 Context 來創建和顯示普通的 Dialog 。 這裏的 普通 指的是文章開頭示例代碼中的普通 Dialog,並非 Toast,System Dialog 等等。Android 系統爲了安全考慮,不想在 App 進入後臺之後仍然可以彈出 Dialog,這樣就會產生可以在其他 App 中彈窗的場景,造成一定的安全隱患。雖然通過 Dialog Theme 的 Activity 仍然可以實現這一需求,但是 Google 也在加強 對後臺啓動 Activity 的限制。

寫到這裏,問題似乎已經得到了解答。但是其實我們對於整個 Token 流程還是一片霧水的。試着想一下下面幾個問題。

  • mToken 是在什麼時機,什麼地方創建的?
  • WMS 是如何拿到 mToken 的?
  • WMS 是如何校驗 token 的?
  • ......

真正掌握了這些問題之後,才能形成一個完整的知識閉環,但伴隨而來的,是逃避不了的,枯燥乏味的 Read the fucking AOSP 。

誰創建了 Token?

先來看看 Token 到底是個什麼樣的類。

> ActivityRecord.java

static class Token extends IApplicationToken.Stub {
    private final WeakReference<ActivityRecord> weakActivity;
    private final String name;

    Token(ActivityRecord activity, Intent intent) {
        weakActivity = new WeakReference<>(activity);
        name = intent.getComponent().flattenToShortString();
    }

    ......
}

TokenActivityRecord 的靜態內部類,它持有外部 ActivityRecord 的弱引用。繼承自 IApplicationToken.Stub ,是一個 Binder 對象。它在 ActivityRecord 的構造函數中初始化。

> ActivityRecord.java

ActivityRecord(ActivityManagerService _service, ProcessRecord _caller, int _launchedFromPid,
            int _launchedFromUid, String _launchedFromPackage, Intent _intent, String _resolvedType,
            ActivityInfo aInfo, Configuration _configuration,
            ActivityRecord _resultTo, String _resultWho, int _reqCode,
            boolean _componentSpecified, boolean _rootVoiceInteraction,
            ActivityStackSupervisor supervisor, ActivityOptions options,
            ActivityRecord sourceRecord)
 
{
    service = _service;
 // 初始化 appToken
    appToken = new Token(this, _intent);
    ......
}

一個 ActivtyRecord 代表一個 Activity 實例, 它包含了 Activity 的所有信息。在 Activity 的啓動過程中,當執行到 ActivityStarter.startActivity() 時,會創建待啓動的 ActivityRecord 對象,也間接創建了 Token 對象。

> ActivityStarter.java

private int startActivity(IApplicationThread caller, Intent intent, Intent ephemeralIntent,
            String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo,
            IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
            IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid,
            String callingPackage, int realCallingPid, int realCallingUid, int startFlags,
            SafeActivityOptions options,
            boolean ignoreTargetSecurity, boolean componentSpecified, ActivityRecord[] outActivity,
            TaskRecord inTask, boolean allowPendingRemoteAnimationRegistryLookup,
            PendingIntentRecord originatingPendingIntent)
 
{

            ......

            // 構建 ActivityRecord,其中會初始化 token
            ActivityRecord r = new ActivityRecord(mService, callerApp, callingPid, callingUid,
                callingPackage, intent, resolvedType, aInfo, mService.getGlobalConfiguration(),
                resultRecord, resultWho, requestCode, componentSpecified, voiceSession != null,
                mSupervisor, checkedOptions, sourceRecord);

            ......
            return startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags,
                true /* doResume */, checkedOptions, inTask, outActivity);
}

到這裏, ActivityRecord.appToken 已經被賦值。所以 Token 是在 AMS 的 startActivity 流程中創建的。但是 Token 的校驗顯然是發生在 WMS 中的,所以 AMS 還得把 Token 交到 WMS 。

WMS 是如何拿到 Token 的?

繼續跟下去,startActivity() 最後會調用到 ActivityStack.startActivityLocked(),這個方法就是把 Token 給到 WMS 的關鍵。

> ActivityStack.java

void startActivityLocked(ActivityRecord r, ActivityRecord focusedTopActivity,
            boolean newTask, boolean keepCurTransition, ActivityOptions options)
 
{

            ......

            if (r.getWindowContainerController() == null) {
   // 創建 AppWindowContainerController 對象,其中包含 token 對象
            r.createWindowContainer();

            ......
        
}

其他代碼都省略了,重點關注 r.createWindowContainer(),這裏的 r 就是一開始創建的 ActivityRecord 對象。

> ActivityRecord.java

void createWindowContainer() 
{
    if (mWindowContainerController != null) {
        throw new IllegalArgumentException("Window container=" + mWindowContainerController
                + " already created for r=" + this);
    }

    inHistory = true;

    final TaskWindowContainerController taskController = task.getWindowContainerController();

    ......

 // 構造函數中會調用 createAppWindow() 創建 AppWindowToken 對象
    mWindowContainerController = new AppWindowContainerController(taskController, appToken,
            this, Integer.MAX_VALUE /* add on top */, info.screenOrientation, fullscreen,
            (info.flags & FLAG_SHOW_FOR_ALL_USERS) != 0, info.configChanges,
            task.voiceSession != null, mLaunchTaskBehind, isAlwaysFocusable(),
            appInfo.targetSdkVersion, mRotationAnimationHint,
            ActivityManagerService.getInputDispatchingTimeoutLocked(this) * 1000000L);

    task.addActivityToTop(this);

    ......
}

AppWindowContainerController 的構造函數中傳入了之前已經初始化過的 appToken

> AppWindowContainerController.java

public AppWindowContainerController(TaskWindowContainerController taskController,
        IApplicationToken token, AppWindowContainerListener listener, int index,
        int requestedOrientation, boolean fullscreen, boolean showForAllUsers, int configChanges,
        boolean voiceInteraction, boolean launchTaskBehind, boolean alwaysFocusable,
        int targetSdkVersion, int rotationAnimationHint, long inputDispatchingTimeoutNanos,
        WindowManagerService service)
 
{
    super(listener, service);
    mHandler = new H(service.mH.getLooper());
    mToken = token;
    synchronized(mWindowMap) {
           ......

        atoken = createAppWindow(mService, token, voiceInteraction, task.getDisplayContent(),
                inputDispatchingTimeoutNanos, fullscreen, showForAllUsers, targetSdkVersion,
                requestedOrientation, rotationAnimationHint, configChanges, launchTaskBehind,
                alwaysFocusable, this);
        ......
    }
}

createAppWindow() 方法中會創建 AppWindowToken 對象,注意傳入的 token 參數。

> AppWindowContainerController.java

AppWindowToken createAppWindow(WindowManagerService service, IApplicationToken token,
        boolean voiceInteraction, DisplayContent dc, long inputDispatchingTimeoutNanos,
        boolean fullscreen, boolean showForAllUsers, int targetSdk, int orientation,
        int rotationAnimationHint, int configChanges, boolean launchTaskBehind,
        boolean alwaysFocusable, AppWindowContainerController controller)
 
{
    return new AppWindowToken(service, token, voiceInteraction, dc,
            inputDispatchingTimeoutNanos, fullscreen, showForAllUsers, targetSdk, orientation,
            rotationAnimationHint, configChanges, launchTaskBehind, alwaysFocusable,
            controller);
}
> AppWindowToken.java

AppWindowToken(WindowManagerService service, IApplicationToken token, boolean voiceInteraction,
            DisplayContent dc, boolean fillsParent)
 
{
        // 父類是 WindowToken
        super(service, token != null ? token.asBinder() : null, TYPE_APPLICATION, true, dc,
                false /* ownerCanManageAppTokens */);
        appToken = token;
        mVoiceInteraction = voiceInteraction;
        mFillsParent = fillsParent;
        mInputApplicationHandle = new InputApplicationHandle(this);
}

這裏調用了父類的構造函數,AppWindowToken 的父類是 WindowToken

> WindowToken.java

WindowToken(WindowManagerService service, IBinder _token, int type, boolean persistOnEmpty,
            DisplayContent dc, boolean ownerCanManageAppTokens, boolean roundedCornerOverlay)
 
{
        super(service);
        token = _token;
        windowType = type;
        mPersistOnEmpty = persistOnEmpty;
        mOwnerCanManageAppTokens = ownerCanManageAppTokens;
        mRoundedCornerOverlay = roundedCornerOverlay;
        // 接着跟進去
        onDisplayChanged(dc);
    }
> WindowToken.java

void onDisplayChanged(DisplayContent dc) 
{
        // 調用 DisplayContent.reParentWindowToken()
        dc.reParentWindowToken(this);
        mDisplayContent = dc;
        ......
}
> DisplayContent.java
    
void reParentWindowToken(WindowToken token) 
{
    ......
    addWindowToken(token.token, token);
}

private void addWindowToken(IBinder binder, WindowToken token) {
    ......
 // mTokenMap 是一個 HashMap<IBinder, WindowToken>,
    mTokenMap.put(binder, token);
    ......
}

mTokenMap 是一個 HashMap<IBinder, WindowToken> 對象,保存了 WindowToken 以及其 Token 信息。我們從 AMS 啓動 Activity 一路追到這裏,其實已經走到了 WMS 的邏輯。AMS 和 WMS 都是運行在 system_server 進程的,並不存在 binder 調用。AMS 就是按照上面的調用鏈把 Token 傳遞給了 WMS 。

再來一張清晰的流程圖總結一下 Token 從 AMS 傳遞到 WMS 的整個流程:

WMS 是如何校驗 Token 的?

其實一直很糾結源碼解析類的文章應該怎麼寫。單純說思想,但對很多人來說並不夠;源碼說多了,文章又顯得枯燥乏味。大家有好的建議可以在評論區聊一聊。

這一塊的源碼我就不在文章裏一點一點追了。大家可以對着下面的流程圖自己啃一下源碼。從 Dialog.show() 開始,最後走到 WindowManagerService.addwWindow()

WMS.addWindow() 方法中就會對 Token 進行校驗,這一塊來看一下源碼。

 public int addWindow(Session session, IWindow client, int seq,
            LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
            Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
            DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel)
 
{
    ......

    AppWindowToken atoken = null;
    final boolean hasParent = parentWindow != null;
    // 獲取 WindowToken
    WindowToken token = displayContent.getWindowToken(
                hasParent ? parentWindow.mAttrs.token : attrs.token);

    if (token == null) {
        if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
                    Slog.w(TAG_WM, "Attempted to add application window with unknown token "
                          + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
        }
        ......
    } else {
        ......
    }
    ......
}

通過 DisplayContent.getWindowToken() 方法獲取 WindowToken 對象之後,會對其進行一系列校驗工作。看到 DisplayContent ,你應該能想到就是從上面提到過的 mTokenMap 集合中取值了。我們來看一看源碼實現。

> DisplayContent.java

WindowToken getWindowToken(IBinder binder) 
{
    return mTokenMap.get(binder);
}

沒錯,的確是從哈希表 mTokenMap 中直接獲取。寫到這裏,整個流程已經走通了。

  • AMS 在啓動 Activity 的時候,會構建表示 Activity 信息的 ActivityRecord 對象,其構造函數中會實例化 Token 對象
  • AMS 在接着上一步之後,會利用創建的 Token 構建 AppWindowContainerController 對象,最終將 Token 存儲到 WMS 中的 mTokenMap 中
  • WMS 在 addWindow 時,會根據當前 Window 對象的 Token 進行校驗

最後

Activity 複習筆記 已經進行到第四篇了。每次思考下一篇的主題都會想很久,大家有什麼好的面試題可以評論在留言區,這都將成爲我的寫作素材。

我也維護了一份共享的石墨文檔,

https://shimo.im/docs/jYhDGHGtc8gP9XgG

大家可以貢獻自己的提問,有能力的話,也可以留下自己的回答。


本文分享自微信公衆號 - 秉心說TM(gh_c6504b1af5ae)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章