Android之window機制token驗證

前言

很高興碰見你~ 歡迎閱讀個人文章java

這篇文章講解關於window token的問題,同時也是Context機制Window機制這兩篇文章的一個補充。若是你對Android的Window機制和Context機制目前位瞭解過,強烈建議你先閱讀前面兩篇文章,能夠幫助理解整個源碼的解析過程以及對token的理解。同時文章涉及到Activty啓動流程源碼,讀者可先閱讀Activity啓動流程這篇文章。文章涉及到這些方面的內容默認讀者已經閱讀且瞭解,不會對這方面的內容過多闡述,若是遇到一些內容不理解,能夠找到對應的文章看一下。那麼,咱們開始吧。android

當咱們想要在屏幕上展現一個Dialog的時候,咱們可能會在Activity的onCreate方法裏這麼寫:git

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

他的構造參數須要一個context對象,可是這個context不能是ApplicationContext等其餘context,只能是ActivityContext(固然沒有ApplicationContext這個類,也沒有ActivityContext這個類,這裏這樣寫只是爲了方便區分context類型,下同)。這樣的代碼運行時沒問題的,若是咱們使用Application傳入會怎麼樣呢?api

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:session

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就出問題了?他們之間有什麼區別?那麼這篇文章就圍繞這個token來展開討論一下。app

文章採用思考問題的思路來展開講述,我會根據我學習這部份內容時候的思考歷程進行復盤。但願這種解決問題的思惟能夠幫助到你。
對token有必定了解的讀者能夠看到最後部分的總體流程把握,再選擇想閱讀的部分仔細閱讀。框架

什麼是token

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

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的地方拋出了異常,是根據一個變量res來判斷的,這個res來自方法addToDisplay,那麼token的判斷確定在這個方法裏面了,res只是一個 判斷的結果,那麼咱們須要進到這個addToDisplay裏去看一下。mWindowSession的類型是IWindowSession,他是一個接口,那他的實現類是什麼?找不到實現類就沒法知道他的具體代碼。這裏涉及到window機制的相關內容,簡單講一下:

WindowManagerService是系統服務進程,應用進程跟window聯繫須要經過跨進程通訊:AIDL,這裏的IWindowSession只是一個Binder接口,他的具體實現類在系統服務進程的Session類。因此這裏的邏輯就跳轉到了Session類的addToDisplay方法中。關於window機制更加詳細的內容,讀者能夠閱讀Android全面解析之Window機制這篇文章進一步瞭解,限於篇幅這裏不過多講解。

那咱們繼續到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都是運行在系統進程,因此不須要跨進程通訊,直接調用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_TOKEN和WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN,咱們只須要找到其中一個就能夠找到token的判斷位置,從代碼中能夠看到,當token==null的時候,會進行各類判斷,第一個返回的就是WindowManagerGlobal.ADD_BAD_APP_TOKEN,這樣咱們就順利找到token的類型:WindowToken。那麼根據咱們這一路跟過來,終於找到token的類型了。再看一下這個類:

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

官方告訴咱們裏面的token變量纔是真正的token,而這個token是IBinder對象。

好了到這裏關於token是什麼已經弄清楚了:

  • 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驗證的?源碼內容不少,咱們須要先看一下token是封裝在哪一個參數被傳輸到了WMS,肯定了參數咱們的搜索範圍就減少了,咱們回到WMS的代碼:

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

咱們能夠看到token和一個attrs.token關係很是密切,而這個attrs從調用棧一路往回走到了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);
    ...
}

這裏的mWindowmWindowManager是什麼?咱們到Dialog的構造函數一看究竟:

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聯繫起來
    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是調用ContextImpl的方法,返回的是應用服務windowManager。這兩個有什麼不一樣,咱們暫時不知道,先留意着,再繼續把源碼看下去尋找答案。咱們回到前面的方法,看到mWindowManager.addView(mDecor, l);咱們知道一個PhoneWindow對應一個WindowManager,這裏使用的WindowManager並非Dialog本身建立的WindowManager,而是參數context的windowManager,也意味着並無使用本身建立的PhoneWindow。Dialog建立PhoneWindow的目的是爲了使用DecorView模板,咱們能夠看到addView的參數裏並非window而只是mDecor。

咱們繼續看代碼,,同時要注意這個l參數,最終token就是封裝在裏面。addView方法最終會調用到了WindowManagerGlobaladdView方法,具體調用流程能夠看我文章開頭的文章:

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參數,parentWindow是與windowManagerPhoneWindow,因此這裏確定不是null,進入到adjustLayoutParamsForSubWindow方法進行調整參數。最後調用ViewRootImpl的setView方法。到這裏WindowManager.LayoutParams這個參數依舊沒有被設置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) {
        // 系統窗口token獲取邏輯
        ...
    } else {
        // 應用窗口token獲取邏輯
        if (wp.token == null) {
            wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
        }
        ...
    }
    ...
}

終於看到了token的賦值了,這裏分爲三種狀況:應用層窗口、子窗口和系統窗口,分別進行token賦值。

應用窗口直接獲取的是與WindowManager對應的PhoneWindow的mAppToken,而子窗口是拿到DecorView的token,系統窗口屬於比較特殊的窗口,使用Application也能夠彈出,可是須要權限,這裏不深刻討論。而這裏的關鍵就是:這個dialog是什麼類型的窗口?以及windowManager對應的PhoneWindow中有沒有token?

而這個判斷跟咱們前面賦值的不一樣WindowManagerImpl有直接的關係。那麼這裏,就必須到Activity和Application建立WindowManager的過程一看究竟了。

Activity與Application的WindowManager

首先咱們看到Activity的window建立流程。這裏須要對Activity的啓動流程有必定的瞭解,有興趣的讀者能夠閱讀Activity啓動流程。追蹤Activity的啓動流程,最終會到ActivityThread的performLaunchActivity

ActivityThread.class(api29)
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ...
	// 最終會調用這個方法來建立window
    // 注意r.token參數
    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);
    ...
}

這個方法調用了activity的attach方法來初始化window,同時咱們看到參數裏有了r.token這個參數,這個token最終會給到哪裏,咱們趕忙繼續看下去:

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) {
    ...
	// 建立window
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    ...
	// 建立windowManager
    // 注意token參數
    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    mWindowManager = mWindow.getWindowManager();
    ...
}

attach方法裏建立了PhoneWindow以及對應的WindowManager,再把建立的windowManager給到activity的mWindowManager屬性。咱們看到建立WindowManager的參數裏有token,咱們繼續看下去:

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建立了WindowManager,同時把token保存在了PhoneWindow內。到這裏咱們知道Activity的PhoneWindow是擁有token的。那麼Application呢?


Application調用的是ContextImpl的getSystemService方法,而這個方法返回的是應用服務的windowManager,Application自己並無建立本身的PhoneWindow和WindowManager,因此也沒有給PhoneWindow賦值token的過程。

所以,Activity擁有本身PhoneWindow以及WindowManager,同時它的PhoneWindow擁有token;而Application並無本身的PhoneWindow,他返回的WindowManager是應用服務windowManager,並無賦值token的過程

那麼到這裏結論已經快要出來了,還差最後一步,咱們回到賦值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的DecorView已是添加到屏幕上了,也就是咱們的Activity是有界面了,這個狀況下,他就是屬於子窗口的類型被添加到PhoneWindow中,而他的token就是DecorView的token,此時DecorView已經被添加到屏幕上,他自己是擁有token的;

這裏補充一點。當一個view(view樹)被添加到屏幕上後,他所對應的viewRootImpl有一個token對象,這個token來自WindowManagerGlobal,他是一個IWindowSession 對象。從源碼中能夠看到,當咱們的PhoneWindow的DecorView展現到屏幕後,後續添加的子window的token,就都是這個IWindowSession 對象了。

而若是是第一次添加,也就是應用界面,那麼他的token就是Activity初始化傳入的token。

可是若是使用的是Application,由於它內部並無token,那麼這裏獲取到的token就是null,後面到WMS也就會拋出異常了。而這也就是爲何使用Activity能夠彈出Dialog而Application不能夠的緣由。由於受到了token的限制。

WMS是如何驗證token的

到這裏咱們已經知道。咱們從WMS的token判斷找到了token的類型以及token的載體:WindowManager.LayoutParams,而後咱們再從dialog的建立流程追到了賦值token的時候會由於windowManager的不一樣而不一樣。所以咱們再去查看了二者不一樣的windowManager,最終獲得結論Activity的PhoneWindow擁有token,而Application使用的是應用級服務windowManager,並無token

那麼此時仍是會有疑問:

  • token究竟是在何時被建立的?
  • WMS怎麼知道我這個token是合法的?

雖然到目前咱們已經弄清緣由,可是知識卻少了一塊,秉着探索知識的好奇心咱們繼續研究下去。

咱們從前面Activity的建立window過程知道token來自於r.token,這個r是ActivityRecord,是AMS啓動Activity的時候傳進來的Activity信息。那麼要追蹤這個token的建立就必須順着這個r的傳遞路線一路回溯。一樣這涉及到Activity的完整啓動流程,我不會解釋詳細的調用棧狀況,默認你清楚activity的啓動流程,若是不清楚,能夠先去閱讀Activity的啓動流程。首先看到這個ActivityRecord是在哪裏被建立的:

/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實現的接口,具體邏輯回到ActivityThread
    client.handleLaunchActivity(r, pendingActions, null /* customIntent */);
    Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
}

這樣咱們須要繼續往前回溯,看看這個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);
    ...
}

能夠看到咱們的token在ClientTransaction對象獲取到。ClientTransaction是AMS傳來的一個事務,負責控制activity的啓動,裏面包含兩個item,一個負責執行activity的create工做,一個負責activity的resume工做。那麼這裏咱們就須要到ClientTransaction的建立過程一看究竟了。下面咱們的邏輯就要進入系統進程了:

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

這個方法建立了ClientTransaction,可是token並非在這裏被建立的,咱們繼續往上回溯(注意代碼的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的建立,咱們進去構造方法中看看有沒有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建立。而這個token看接口就知道是個Binder對象,他持有ActivityRecord的弱引用,這樣能夠訪問到activity的全部信息。到這裏token的建立咱們也找到了。那麼WMS是怎麼知道一個token是否合法呢?每一個token建立後,會在後續發送到WMS ,WMS對token進行緩存,然後續對於應用發送來的token只須要在緩存拿出來匹配一下就知道是否合法了。那麼WMS是怎麼拿到token的?


activity的啓動流程後續會走到一個方法:startActivityLocked,這個方法在我前面的activity啓動流程並無講到,由於它並不屬於「主線」,可是他有一個很是重要的方法調用,以下:

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

這個方法就把token送到了WMS 那裏,咱們繼續看下去:

ActivityRecord.class(api28)
void createWindowContainer() {
    ...
    // 注意參數有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);
	...
}

注意參數有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的子類。那麼咱們繼續追下去:

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方法調用了AppWindow的構造器,而後再調用了父類WindowToken的構造器,咱們能夠看到這裏最終對token進行了緩存,並調用了一個方法,咱們看看這個方法作了什麼:

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以及後來建立的windowToken二者的關係。這裏的邏輯其實已經在WMS中了,因此這個也是保存在WMS中。AMS和WMS都是運行在系統服務進程,於是他們之間能夠直接調用方法,不存在跨進程通訊。WMS就能夠根據IBinder對象拿到windowToken進行信息比對了。至於怎麼比對,代碼位置在一開始的時候已經有涉及到,讀者可自行去查看源碼,這裏就不講了。

那麼,到這裏關於整個token的知識就所有走了一遍了,AMS怎麼建立token,WMS怎麼拿到token的流程也根據咱們回溯的思路走了一遍。

總體流程把握

前面根據咱們思考問題的思惟走完了整個token流程,可是彷佛仍是有點亂,那麼這一部分,就把前面講的東西整理一下,對token的知識有一個總體上的感知,同時也當時前面內容的總結。先來看總體圖:

  1. token在建立ActivityRecord的時候一塊兒被建立,他是一個IBinder對象,實現了接口IApplicationToken。
  2. token建立後會發送到WMS,在WMS中封裝成WindowToken,並存在一個HashMap<IBinder,WindowToken>。
  3. token會隨着ActivityRecord被髮送到本地進程,ActivityThread根據AMS的指令執行Activity啓動邏輯。
  4. Activity啓動的過程當中會建立PhoneWindow和對應的WindowManager,同時把token存在PhoneWindow中。
  5. 經過Activity的WindowManager添加view/彈出dialog時會把PhoneWindow中的token放在窗口LayoutParams中。
  6. 經過viewRootImpl向WMS進行驗證,WMS在LayoutParams拿到IBinder以後就能夠在Map中獲取WindowToken。
  7. 根據獲取的結果就能夠判斷該token的合法狀況。

這就是整個token的運做流程了。而具體的源碼和細節在上面已經解釋完了,讀者可自行選擇重點部分再次閱讀源碼。

從源碼設計看token

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

token看着是屬於window機制的領域內容,實際上是context的知識範疇。咱們知道context一共有三種最終實現類:Activity、Application、Service,context是區分一個類是普通Java類仍是android組件的關鍵。context擁有訪問系統資源的權限,是各類組件訪問系統的接口對象。可是,三種context,只有Activity容許有界面,而其餘的兩種是不能有界面的,也不必有界面。爲了防止開發者亂用context形成混亂,那麼必須對context的權限進行限制,這也就是token存在的意義。擁有token的context能夠建立界面、進行UI操做,而沒有token的context如service、Application,是不容許添加view到屏幕上的(這裏的view除了系統窗口)。

爲何說這不屬於window機制的知識範疇?從window機制中咱們知道WMS控制每個window,是經過viewRootImpl中的IWindowSession來進行通訊的,token在這個過程當中只充當了一個驗證做用,且當PhoneWindow顯示了DecorView以後,後續添加的View使用的token都是ViewRootImpl的IWindowSession對象。這表示當一個PhoneWindow能夠顯示界面後,那麼對於後續其添加的view無需再次進行權限判斷。於是,token真正限制的,是context是否能夠顯示界面,而不是針對window

而咱們瞭解完底層邏輯後,不是要去知道怎麼繞過他的限制,動一些「大膽的想法」,而是要知道官方這麼設計的目的。咱們在開發的時候,也要針對不一樣職責的context來執行對應的事務,不要使用Application或Service來作UI操做

總結

文章採用思考問題的思路來表述,經過源碼分析,講解了關於token的建立、傳遞、驗證等內容。同時,token在源碼設計上的思想進行了總結。

android體系中各類機制之間是互相聯繫,彼此鏈接構成一個完整的系統框架。token涉及到window機制和context機制,同時對activity的啓動流程也要有必定的瞭解。閱讀源碼各類機制的源碼,能夠從多個維度來幫助咱們對一個知識點的理解。同時閱讀源碼的過程當中,不要侷限在當前的模塊內,思考不一樣機制之間的聯繫,系統爲何要這麼設計,解決了什麼問題,能夠幫助咱們從架構的角度去理解整個android源碼設計。閱讀源碼切忌無目標亂看一波,要有明確的目標、驗證什麼問題,針對性尋找那一部分的源碼,與問題無關的源碼暫時忽略,否則會在源碼的海洋裏遊着遊着就溺亡了。

全文到此,感謝你的閱讀

原創不易,以爲有幫助能夠點贊收藏評論轉發關注。
筆者能力有限,有任何想法歡迎評論區交流指正。
如需轉載請私信交流。

另外歡迎光臨筆者的我的博客:傳送門

相關文章
相關標籤/搜索