Android之window機制token驗證

前言
很高興遇見你~ 歡迎閱讀我的文章

這篇文章講解關于window token的問題,同時也是Context機制Window機制這兩篇文章的一個補充。如果你對Android的Window機制和Context機制目前位了解過,強烈建議你先閱讀前面兩篇文章,可以幫助理解整個源碼的解析過程以及對token的理解。同時文章涉及到Activty啟動流程源碼,讀者可先閱讀Activity啟動流程這篇文章。文章涉及到這些方面的內(nèi)容默認讀者已經(jīng)閱讀且了解,不會對這方面的內(nèi)容過多闡述,如果遇到一些內(nèi)容不理解,可以找到對應的文章看一下。那么,我們開始吧。

當我們想要在屏幕上展示一個Dialog的時候,我們可能會在Activity的onCreate方法里這么寫:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val dialog = AlertDialog.Builder(this)
    dialog.run{
        title = "我是標題"
        setMessage("我是內(nèi)容")
    }
    dialog.show()
}

他的構造參數(shù)需要一個context對象,但是這個context不能是ApplicationContext等其他context,只能是ActivityContext(當然沒有ApplicationContext這個類,也沒有ActivityContext這個類,這里這樣寫只是為了方便區(qū)分context類型,下同)。這樣的代碼運行時沒問題的,如果我們使用Application傳入會怎么樣呢?

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    // 注意這里換成了ApplicationContext
    val dialog = AlertDialog.Builder(applicationContext)
    ...
}

運行一下:


報錯了,原因是You need to use a Theme.AppCompat theme (or descendant) with this activity.,那我們給他添加一個Theme:

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    // 注意這里添加了主題
    val dialog = AlertDialog.Builder(applicationContext,R.style.AppTheme)
    ...
}

好了再次運行:


嗯嗯?又崩潰了,原因是:Unable to add window -- token null is not valid; is your activity running?token為null?這個token是什么?為什么同樣是context,使用activity沒問題,用ApplicationContext就出問題了?他們之間有什么區(qū)別?那么這篇文章就圍繞這個token來展開討論一下。

文章采用思考問題的思路來展開講述,我會根據(jù)我學習這部分內(nèi)容時候的思考歷程進行復盤。希望這種解決問題的思維可以幫助到你。
對token有一定了解的讀者可以看到最后部分的整體流程把握,再選擇想閱讀的部分仔細閱讀。

什么是token

首先我們看到報錯是在ViewRootImpl.java:907,這個地方肯定有進行token判斷,然后拋出異常,這樣我們就能找到token了,那我們直接去這個地方看看。:

  • ViewRootImpl.class(api29)
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    ...
    int res;
    ...
    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                        getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
                        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                        mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
                        mTempInsets);
    ...
    if (res < WindowManagerGlobal.ADD_OKAY) {
        ...
        switch (res) {
            case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
            case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
                /*
                *   1
                */
                throw new WindowManager.BadTokenException(
                    "Unable to add window -- token " + attrs.token
                    + " is not valid; is your activity running?");    
                ...
        }
        ...
    }
    ...
}

我們看到代碼就是在注釋1的地方拋出了異常,是根據(jù)一個變量res來判斷的,這個res來自方法addToDisplay,那么token的判斷肯定在這個方法里面了,res只是一個 判斷的結果,那么我們需要進到這個addToDisplay里去看一下。mWindowSession的類型是IWindowSession,他是一個接口,那他的實現(xiàn)類是什么?找不到實現(xiàn)類就無法知道他的具體代碼。這里涉及到window機制的相關內(nèi)容,簡單講一下:

WindowManagerService是系統(tǒng)服務進程,應用進程跟window聯(lián)系需要通過跨進程通信:AIDL,這里的IWindowSession只是一個Binder接口,他的具體實現(xiàn)類在系統(tǒng)服務進程的Session類。所以這里的邏輯就跳轉到了Session類的addToDisplay方法中。關于window機制更加詳細的內(nèi)容,讀者可以閱讀Android全面解析之Window機制這篇文章進一步了解,限于篇幅這里不過多講解。

那我們繼續(xù)到Session的方法中看一下:

  • Session.class(api29)
class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
    final WindowManagerService mService; 
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets,
            Rect outStableInsets, Rect outOutsets,
            DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
            InsetsState outInsetsState) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
                outContentInsets, outStableInsets, outOutsets, outDisplayCutout, outInputChannel,
                outInsetsState);
    }
}

可以看到,Session確實是繼承自接口IWindowSession,因為WMS和Session都是運行在系統(tǒng)進程,所以不需要跨進程通信,直接調(diào)用WMS的方法:

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,
        InsetsState outInsetsState) {
    ...
    WindowState parentWindow = null;
    ...
    // 獲取parentWindow
    parentWindow = windowForClientLocked(null, attrs.token, false);
    ...
    final boolean hasParent = parentWindow != null;
    // 獲取token
    WindowToken token = displayContent.getWindowToken(
        hasParent ? parentWindow.mAttrs.token : attrs.token);
    ...
    // 驗證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;
        }
       ...//各種驗證
    }
    ...
}

WMS的addWindow方法代碼這么多怎么找到關鍵代碼?還記得viewRootImpl在判斷res是什么值的情況下拋出異常嗎?沒錯是WindowManagerGlobal.ADD_BAD_APP_TOKENWindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN,我們只需要找到其中一個就可以找到token的判斷位置,從代碼中可以看到,當token==null的時候,會進行各種判斷,第一個返回的就是WindowManagerGlobal.ADD_BAD_APP_TOKEN,這樣我們就順利找到token的類型:WindowToken。那么根據(jù)我們這一路跟過來,終于找到token的類型了。再看一下這個類:

class WindowToken extends WindowContainer<WindowState> {
    ...
    // The actual token.
    final IBinder token;
}

官方告訴我們里面的token變量才是真正的token,而這個token是IBinder對象。

好了到這里關于token是什么已經(jīng)弄清楚了:

  • token是一個IBinder對象
  • 只有利用token才能成功添加dialog

那么接下來就有更多的問題需要思考了:

  • Dialog在show過程中是如何拿到token并給到WMS驗證的?
  • 這個token在activity和application兩者之間有什么不同?
  • WMS怎么知道這個token是合法的,換句話說,WMS怎么驗證token的?

dialog如何獲取到context的token的?

首先,我們解決第一個問題:Dialog在show過程中是如何拿到token并給到WMS驗證的?

我們知道導致兩種context(activity和application)彈出dialiog的不同結果,原因在于token的問題。那么在彈出Dialog的過程中,他是如何拿到context的token并給到WMS驗證的?源碼內(nèi)容很多,我們需要先看一下token是封裝在哪個參數(shù)被傳輸?shù)搅薟MS,確定了參數(shù)我們的搜索范圍就減小了,我們回到WMS的代碼:

parentWindow = windowForClientLocked(null, attrs.token, false);
WindowToken token = displayContent.getWindowToken(
        hasParent ? parentWindow.mAttrs.token : attrs.token);

我們可以看到token和一個attrs.token關系非常密切,而這個attrs從調(diào)用棧一路往回走到了viewRootImpl中:

  • ViewRootImpl.class(api29)
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
   ...
}

可以看到這是一個WindowManager.LayoutParams類型的對象。那我們接下來需要從最開始show()開始,追蹤這個token是如何被獲取到的:

  • Dialog.class(api30)
public void show() {
    ...
    WindowManager.LayoutParams l = mWindow.getAttributes();
    ...
    mWindowManager.addView(mDecor, l);
    ...
}

這里的mWindow和mWindowManager是什么?我們到Dialog的構造函數(shù)一看究竟:

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
    // 如果context沒有主題,需要把context封裝成ContextThemeWrapper
    if (createContextThemeWrapper) {
        if (themeResId == Resources.ID_NULL) {
            final TypedValue outValue = new TypedValue();
            context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
            themeResId = outValue.resourceId;
        }
        mContext = new ContextThemeWrapper(context, themeResId);
    } else {
        mContext = context;
    }
    // 初始化windowManager
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    // 初始化PhoneWindow
    final Window w = new PhoneWindow(mContext);
    mWindow = w;
    ...
    // 把windowManager和PhoneWindow聯(lián)系起來
    w.setWindowManager(mWindowManager, null, null);
    ...
}

初始化的邏輯我們看重點就好:首先判斷這是不是個有主題的context,如果不是需要設置主題并封裝成一個ContextThemeWrapper對象,這也是為什么我們文章一開始使用application但是沒有設置主題會拋異常。然后獲取windowManager,注意,這里是重點,也是我當初看源碼的時候忽略的地方。這里的context可能是Activity或者Application,他們的getSystemService返回的windowManager是一樣的嗎,看代碼:

  • Activity.class(api29)
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)) {
        // 返回的是自身的WindowManager
        return mWindowManager;
    } else if (SEARCH_SERVICE.equals(name)) {
        ensureSearchManager();
        return mSearchManager;
    }
    return super.getSystemService(name);
}
  • ContextImpl.class(api29)
public Object getSystemService(String name) {
    return SystemServiceRegistry.getSystemService(this, name);
}

Activity返回的其實是自身的WindowManager而Application是調(diào)用ContextImpl的方法,返回的是應用服務windowManager。這兩個有什么不同,我們暫時不知道,先留意著,再繼續(xù)把源碼看下去尋找答案。我們回到前面的方法,看到mWindowManager.addView(mDecor, l);我們知道一個PhoneWindow對應一個WindowManager,這里使用的WindowManager并不是Dialog自己創(chuàng)建的WindowManager,而是參數(shù)context的windowManager,也意味著并沒有使用自己創(chuàng)建的PhoneWindow。Dialog創(chuàng)建PhoneWindow的目的是為了使用DecorView模板,我們可以看到addView的參數(shù)里并不是window而只是mDecor。

我們繼續(xù)看代碼,,同時要注意這個l參數(shù),最終token就是封裝在里面。addView方法最終會調(diào)用到了WindowManagerGlobaladdView方法,具體調(diào)用流程可以看我文章開頭的文章:

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    ...
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    if (parentWindow != null) {
        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    }
    ...
    ViewRootImpl root;
    ...
    root = new ViewRootImpl(view.getContext(), display);
    ...
    try {
        root.setView(view, wparams, panelParentView);
    } 
    ...
}

這里我們只看WindowManager.LayoutParams參數(shù),parentWindow是與windowManagerPhoneWindow,所以這里肯定不是null,進入到adjustLayoutParamsForSubWindow方法進行調(diào)整參數(shù)。最后調(diào)用ViewRootImpl的setView方法。到這里WindowManager.LayoutParams這個參數(shù)依舊沒有被設置token,那么最大的可能性就是在adjustLayoutParamsForSubWindow方法中了,馬上進去看看:

  • Window.class(api29)
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
    CharSequence curTitle = wp.getTitle();
    if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
            wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
        // 子窗口token獲取邏輯
        if (wp.token == null) {
            View decor = peekDecorView();
            if (decor != null) {
                wp.token = decor.getWindowToken();
            }
        }
        ...
    } else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW &&
                wp.type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
        // 系統(tǒng)窗口token獲取邏輯
        ...
    } else {
        // 應用窗口token獲取邏輯
        if (wp.token == null) {
            wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
        }
        ...
    }
    ...
}

終于看到了token的賦值了,這里分為三種情況:應用層窗口、子窗口系統(tǒng)窗口,分別進行token賦值。

應用窗口直接獲取的是與WindowManager對應的PhoneWindow的mAppToken,而子窗口是拿到DecorView的token,系統(tǒng)窗口屬于比較特殊的窗口,使用Application也可以彈出,但是需要權限,這里不深入討論。而這里的關鍵就是:這個dialog是什么類型的窗口?以及windowManager對應的PhoneWindow中有沒有token?

而這個判斷跟我們前面賦值的不同WindowManagerImpl有直接的關系。那么這里,就必須到Activity和Application創(chuàng)建WindowManager的過程一看究竟了。

Activity與Application的WindowManager

首先我們看到Activity的window創(chuàng)建流程。這里需要對Activity的啟動流程有一定的了解,有興趣的讀者可以閱讀Activity啟動流程。追蹤Activity的啟動流程,最終會到ActivityThread的performLaunchActivity

  • ActivityThread.class(api29)
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ...
    // 最終會調(diào)用這個方法來創(chuàng)建window
    // 注意r.token參數(shù)
    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, window, r.configCallback,
        r.assistToken);
    ...
}

這個方法調(diào)用了activity的attach方法來初始化window,同時我們看到參數(shù)里有了r.token這個參數(shù),這個token最終會給到哪里,我們趕緊繼續(xù)看下去:

  • Activity.class(api29)
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, IBinder assistToken) {
    ...
    // 創(chuàng)建window
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    ...
    // 創(chuàng)建windowManager
    // 注意token參數(shù)
    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    mWindowManager = mWindow.getWindowManager();
    ...
}

attach方法里創(chuàng)建了PhoneWindow以及對應的WindowManager,再把創(chuàng)建的windowManager給到activity的mWindowManager屬性。我們看到創(chuàng)建WindowManager的參數(shù)里有token,我們繼續(xù)看下去:

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

這里利用應用服務的windowManager給Activity創(chuàng)建了WindowManager,同時把token保存在了PhoneWindow內(nèi)。到這里我們知道Activity的PhoneWindow是擁有token的。那么Application呢?

Application的創(chuàng)建具體流程可以看到這篇文章contentProvider啟動流程,因為contentProvider是伴隨著應用的啟動而啟動的。最終Application的啟動會來到ActivityThread的handleBindApplication方法:

  • /frameworks/base/core/java/android/app/ActivityThread.java
private void handleBindApplication(AppBindData data) {
    ...
    // 獲取到Application實例
    Application app;
    ...
    try {
        app = data.info.makeApplication(data.restrictedBackupMode, null);
        ...
    }
    ...
}

這里的data.info是LoadedApk對象,我們深入這個方法看一下:

  • LoadedApk.java(api29)
public Application makeApplication(boolean forceDefaultAppClass,
        Instrumentation instrumentation) {
    if (mApplication != null) {
        return mApplication;
    }
    ...
    Application app = null;
    ...
    try {
        java.lang.ClassLoader cl = getClassLoader();
        ...
        ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
        app = mActivityThread.mInstrumentation.newApplication(
                cl, appClass, appContext);
        appContext.setOuterContext(app);
    } 
    ...
    mActivityThread.mAllApplications.add(app);
    mApplication = app;

    if (instrumentation != null) {
        try {
            instrumentation.callApplicationOnCreate(app);
        } 
        ...
    }
    ...
    return app;
}

構建Application的邏輯也不復雜。首先判斷Application如果存在則直接返回。后面通過Instrumentation來創(chuàng)建Application對象,最后再通過Instrumentation來回調(diào)Application的onCreate方法,我們分別看一下這兩個方法:

  • Instrumentation.java(api29)
public Application newApplication(ClassLoader cl, String className, Context context)
        throws InstantiationException, IllegalAccessException, 
        ClassNotFoundException {
    Application app = getFactory(context.getPackageName())
            .instantiateApplication(cl, className);
    app.attach(context);
    return app;
}
public void callApplicationOnCreate(Application app) {
    app.onCreate();
}

Application.java
final void attach(Context context) {
    attachBaseContext(context);
    mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
}

我們可以看到,直到Application回調(diào)onCreate方法,整個過程沒有涉及token的賦值。Activity是在attach方法中傳入了token參數(shù),而這里Application并沒有,所以Application并沒有token,也沒有初始化PhoneWindow和WindowManager。那么Application的getSystemService返回的是什么呢?Application調(diào)用的是ContextImpl的getSystemService方法,而這個方法返回的是應用服務的windowManager,Application本身并沒有創(chuàng)建自己的PhoneWindow和WindowManager,所以也沒有給PhoneWindow賦值token的過程。

因此,Activity擁有自己PhoneWindow以及WindowManager,同時它的PhoneWindow擁有token;而Application并沒有自己的PhoneWindow,他返回的WindowManager是應用服務windowManager,并沒有賦值token的過程。

那么到這里結論已經(jīng)快要出來了,還差最后一步,我們回到賦值token的那個方法中:

  • Window.class(api29)
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
    if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
            wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
        // 子窗口token獲取邏輯
        if (wp.token == null) {
            View decor = peekDecorView();
            if (decor != null) {
                wp.token = decor.getWindowToken();
            }
        }
        ...
    } else {
        // 應用窗口token獲取邏輯
        if (wp.token == null) {
            wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
        }
        ...
    }
    ...
}

當我們使用Activity來添加dialog的時候,Activity本身是帶有token的,Dialog是屬于應用級窗口(至于為什么讀者可以前往dialog的show方法中跟蹤源碼),他的window層級數(shù)是2,而Activity的界面是1,所以他會顯示在Activity之上。而因為這里Dialog是應用級窗口,所以他最終就拿到了Activity的token。

但是如果使用的是Application,因為它內(nèi)部并沒有token,那么這里獲取到的token就是null,后面到WMS也就會拋出異常了。而這也就是為什么使用Activity可以彈出Dialog而Application不可以的原因。因為受到了token的限制。

WMS是如何驗證token的

到這里我們已經(jīng)知道。我們從WMS的token判斷找到了token的類型以及token的載體:WindowManager.LayoutParams,然后我們再從dialog的創(chuàng)建流程追到了賦值token的時候會因為windowManager的不同而不同。因此我們再去查看了兩者不同的windowManager,最終得到結論Activity的PhoneWindow擁有token,而Application使用的是應用級服務windowManager,并沒有token。

那么此時還是會有疑問:

  • token到底是在什么時候被創(chuàng)建的?
  • WMS怎么知道我這個token是合法的?
    雖然到目前我們已經(jīng)弄清原因,但是知識卻少了一塊,秉著探索知識的好奇心我們繼續(xù)研究下去。

我們從前面Activity的創(chuàng)建window過程知道token來自于r.token,這個rActivityRecord,是AMS啟動Activity的時候傳進來的Activity信息。那么要追蹤這個token的創(chuàng)建就必須順著這個r的傳遞路線一路回溯。同樣這涉及到Activity的完整啟動流程,我不會解釋詳細的調(diào)用棧情況,默認你清楚activity的啟動流程,如果不清楚,可以先去閱讀Activity的啟動流程。首先看到這個ActivityRecord是在哪里被創(chuàng)建的:

  • /frameworks/base/core/java/android/app/servertransaction/LaunchActivityItem.java/;
public void execute(ClientTransactionHandler client, IBinder token,
        PendingTransactionActions pendingActions) {
    Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
    ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
            mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
            mPendingResults, mPendingNewIntents, mIsForward,
            mProfilerInfo, client);
    // ClientTransactionHandler是ActivityThread實現(xiàn)的接口,具體邏輯回到ActivityThread
    client.handleLaunchActivity(r, pendingActions, null /* customIntent */);
    Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
}

這樣我們需要繼續(xù)往前回溯,看看這個token是在哪里被獲取的:

-/frameworks/base/core/java/android/app/servertransaction/TransactionExecutor.java

public void execute(ClientTransaction transaction) {
    ...
    executeCallbacks(transaction);
    ...
}
public void executeCallbacks(ClientTransaction transaction) {
    ...
        final IBinder token = transaction.getActivityToken();
        item.execute(mTransactionHandler, token, mPendingActions);
    ...
}

可以看到我們的tokenClientTransaction對象獲取到。ClientTransaction是AMS傳來的一個事務,負責控制activity的啟動,里面包含兩個item,一個負責執(zhí)行activity的create工作,一個負責activity的resume工作。那么這里我們就需要到ClientTransaction的創(chuàng)建過程一看究竟了。下面我們的邏輯就要進入系統(tǒng)進程了:

  • ActivityStackSupervisor.class(api28)
final boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app,
    boolean andResume, boolean checkConfig) throws RemoteException {
    ...
    final ClientTransaction clientTransaction = ClientTransaction.obtain(app.thread,
            r.appToken);
    ...
}

這個方法創(chuàng)建了ClientTransaction,但是token并不是在這里被創(chuàng)建的,我們繼續(xù)往上回溯(注意代碼的api版本,不同版本的代碼會不同):

  • ActivityStarter.java(api28)
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) {
    ...
  
    //記錄得到的activity信息
    ActivityRecord r = new ActivityRecord(mService, callerApp, callingPid, callingUid,
            callingPackage, intent, resolvedType, aInfo, mService.getGlobalConfiguration(),
            resultRecord, resultWho, requestCode, componentSpecified, voiceSession != null,
            mSupervisor, checkedOptions, sourceRecord);
   ...
}

我們一路回溯,終于看到了ActivityRecord的創(chuàng)建,我們進去構造方法中看看有沒有token相關的構造:

  • ActivityRecord.class(api28)
ActivityRecord(... Intent _intent,...) {
    appToken = new Token(this, _intent);
    ...
}
static class Token extends IApplicationToken.Stub {
   ...
    Token(ActivityRecord activity, Intent intent) {
        weakActivity = new WeakReference<>(activity);
        name = intent.getComponent().flattenToShortString();
    }
    ...
}

可以看到確實這里進行了token創(chuàng)建。而這個token看接口就知道是個Binder對象,他持有ActivityRecord弱引用,這樣可以訪問到activity的所有信息。到這里token的創(chuàng)建我們也找到了。那么WMS是怎么知道一個token是否合法呢?每個token創(chuàng)建后,會在后續(xù)發(fā)送到WMS ,WMS對token進行緩存,而后續(xù)對于應用發(fā)送來的token只需要在緩存拿出來匹配一下就知道是否合法了。那么WMS是怎么拿到token的?

activity的啟動流程后續(xù)會走到一個方法:startActivityLocked,這個方法在我前面的activity啟動流程并沒有講到,因為它并不屬于“主線”,但是他有一個非常重要的方法調(diào)用,如下:

  • ActivityStack.class(api28)
void startActivityLocked(ActivityRecord r, ActivityRecord focusedTopActivity,
        boolean newTask, boolean keepCurTransition, ActivityOptions options) {
    ...
    r.createWindowContainer();
    ...
}

這個方法就把token送到了WMS 那里,我們繼續(xù)看下去:

  • ActivityRecord.class(api28)
void createWindowContainer() {
    ...
    // 注意參數(shù)有token,這個token就是之前初始化的token
    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);
    ...
}

注意參數(shù)有token,這個token就是之前初始化的token,我們進入到他的構造方法看一下:

  • AppWindowContainerController.class(api28)
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) {
    ...
    synchronized(mWindowMap) {
        AppWindowToken atoken = mRoot.getAppWindowToken(mToken.asBinder());
       ...
        atoken = createAppWindow(mService, token, voiceInteraction, task.getDisplayContent(),
                inputDispatchingTimeoutNanos, fullscreen, showForAllUsers, targetSdkVersion,
                requestedOrientation, rotationAnimationHint, configChanges, launchTaskBehind,
                alwaysFocusable, this);
        ...
    }
}

還記得我們在一開始看WMS的時候他驗證的是什么對象嗎?WindowToken,而AppWindowToken是WindowToken的子類。那么我們繼續(xù)追下去:

  • AppWindowContainerController.class(api28)
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(WindowManagerService service, IApplicationToken token, ...) {
    this(service, token, voiceInteraction, dc, fullscreen);
    ...
}
  • WindowToken.class
WindowToken(WindowManagerService service, IBinder _token, int type, boolean persistOnEmpty,
        DisplayContent dc, boolean ownerCanManageAppTokens, boolean roundedCornerOverlay) {
    token = _token;
    ...
    onDisplayChanged(dc);
}

createAppWindow方法調(diào)用了AppWindow的構造器,然后再調(diào)用了父類WindowToken的構造器,我們可以看到這里最終對token進行了緩存,并調(diào)用了一個方法,我們看看這個方法做了什么:

  • WindowToken.class
void onDisplayChanged(DisplayContent dc) {
    dc.reParentWindowToken(this);
    ...
}
  • DisplayContent.class(api28)
void reParentWindowToken(WindowToken token) {
    addWindowToken(token.token, token);
}
private void addWindowToken(IBinder binder, WindowToken token) {
    ...
    mTokenMap.put(binder, token);
    ...
}

mTokenMap 是一個 HashMap<IBinder, WindowToken> 對象,這里就可以保存一開始初始化的token以及后來創(chuàng)建的windowToken兩者的關系。這里的邏輯其實已經(jīng)在WMS中了,所以這個也是保存在WMS中。AMS和WMS都是運行在系統(tǒng)服務進程,因而他們之間可以直接調(diào)用方法,不存在跨進程通信。WMS就可以根據(jù)IBinder對象拿到windowToken進行信息比對了。至于怎么比對,代碼位置在一開始的時候已經(jīng)有涉及到,讀者可自行去查看源碼,這里就不講了。

那么,到這里關于整個token的知識就全部走了一遍了,AMS怎么創(chuàng)建token,WMS怎么拿到token的流程也根據(jù)我們回溯的思路走了一遍。

整體流程把握

前面根據(jù)我們思考問題的思維走完了整個token流程,但是似乎還是有點亂,那么這一部分,就把前面講的東西整理一下,對token的知識有一個整體上的感知,同時也當時前面內(nèi)容的總結。先來看整體圖:


1、token在創(chuàng)建ActivityRecord的時候一起被創(chuàng)建,他是一個IBinder對象,實現(xiàn)了接口IApplicationToken。
2、token創(chuàng)建后會發(fā)送到WMS,在WMS中封裝成WindowToken,并存在一個HashMap<IBinder,WindowToken>。
3、token會隨著ActivityRecord被發(fā)送到本地進程,ActivityThread根據(jù)AMS的指令執(zhí)行Activity啟動邏輯。
4、Activity啟動的過程中會創(chuàng)建PhoneWindow和對應的WindowManager,同時把token存在PhoneWindow中。
5、通過Activity的WindowManager添加view/彈出dialog時會把PhoneWindow中的token放在窗口LayoutParams中。
6、通過viewRootImpl向WMS進行驗證,WMS在LayoutParams拿到IBinder之后就可以在Map中獲取WindowToken。
7、根據(jù)獲取的結果就可以判斷該token的合法情況。
這就是整個token的運作流程了。而具體的源碼和細節(jié)在上面已經(jīng)解釋完了,讀者可自行選擇重點部分再次閱讀源碼。

從源碼設計看token

我在Context機制一文中講到,不同的context擁有不同的職責,系統(tǒng)對不同的context限制了不同的權利,讓在對應情景下的組件只能做對應的事情。其中最明顯的限制就是UI操作。

token看著是屬于window機制的領域內(nèi)容,其實是context的知識范疇。我們知道context一共有三種最終實現(xiàn)類:Activity、Application、Servicecontext是區(qū)分一個類是普通Java類還是android組件的關鍵。context擁有訪問系統(tǒng)資源的權限,是各種組件訪問系統(tǒng)的接口對象。但是,三種context,只有Activity允許有界面,而其他的兩種是不能有界面的,也沒必要有界面。為了防止開發(fā)者亂用context造成混亂,那么必須對context的權限進行限制,這也就是token存在的意義。擁有token的context可以創(chuàng)建界面、進行UI操作,而沒有token的context如service、Application,是不允許添加view到屏幕上的(這里的view除了系統(tǒng)窗口)。

為什么說這不屬于window機制的知識范疇?從window機制中我們知道WMS控制每一個window,是通過viewRootImpl中的IWindowSession來進行通信的,token在這個過程中只充當了一個驗證作用,且當PhoneWindow顯示了DecorView之后,后續(xù)添加的View使用的token都是ViewRootImpl的IWindowSession對象。這表示當一個PhoneWindow可以顯示界面后,那么對于后續(xù)其添加的view無需再次進行權限判斷。因而,token真正限制的,是context是否可以顯示界面,而不是針對window

而我們了解完底層邏輯后,不是要去知道怎么繞過他的限制,動一些“大膽的想法”,而是要知道官方這么設計的目的。我們在開發(fā)的時候,也要針對不同職責的context來執(zhí)行對應的事務,不要使用Application或Service來做UI操作。

總結

文章采用思考問題的思路來表述,通過源碼分析,講解了關于token的創(chuàng)建、傳遞、驗證等內(nèi)容。同時,token在源碼設計上的思想進行了總結。

android體系中各種機制之間是互相聯(lián)系,彼此連接構成一個完整的系統(tǒng)框架。token涉及到window機制和context機制,同時對activity的啟動流程也要有一定的了解。閱讀源碼各種機制的源碼,可以從多個維度來幫助我們對一個知識點的理解。同時閱讀源碼的過程中,不要局限在當前的模塊內(nèi),思考不同機制之間的聯(lián)系,系統(tǒng)為什么要這么設計,解決了什么問題,可以幫助我們從架構的角度去理解整個android源碼設計。閱讀源碼切忌無目標亂看一波,要有明確的目標、驗證什么問題,針對性尋找那一部分的源碼,與問題無關的源碼暫時忽略,不然會在源碼的海洋里游著游著就溺亡了。
————————————————
版權聲明:本文為CSDN博主「一只修仙的猿」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權協(xié)議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/weixin_43766753/article/details/109060496

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 今天感恩節(jié)哎,感謝一直在我身邊的親朋好友。感恩相遇!感恩不離不棄。 中午開了第一次的黨會,身份的轉變要...
    余生動聽閱讀 10,798評論 0 11
  • 彩排完,天已黑
    劉凱書法閱讀 4,452評論 1 3
  • 表情是什么,我認為表情就是表現(xiàn)出來的情緒。表情可以傳達很多信息。高興了當然就笑了,難過就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 129,440評論 2 7

友情鏈接更多精彩內(nèi)容