1. 程式人生 > 實用技巧 >Android之window機制token驗證

Android之window機制token驗證

前言

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

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

當我們想要在螢幕上展示一個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("我是內容")
    }
    dialog.show()
}

他的構造引數需要一個context物件,但是這個context不能是ApplicationContext等其他context,只能是ActivityContext(當然沒有ApplicationContext這個類,也沒有ActivityContext這個類,這裡這樣寫只是為了方便區分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就出問題了?他們之間有什麼區別?那麼這篇文章就圍繞這個token來展開討論一下。

文章採用思考問題的思路來展開講述,我會根據我學習這部分內容時候的思考歷程進行復盤。希望這種解決問題的思維可以幫助到你。
對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的地方丟擲了異常,是根據一個變數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原始碼設計。閱讀原始碼切忌無目標亂看一波,要有明確的目標、驗證什麼問題,針對性尋找那一部分的原始碼,與問題無關的原始碼暫時忽略,不然會在原始碼的海洋裡遊著遊著就溺亡了。

全文到此,感謝你的閱讀

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

另外歡迎光臨筆者的個人部落格:傳送門