主頁 >  其他 > Android之window機制token驗證

Android之window機制token驗證

2020-10-15 15:49:06 其他

前言

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

這篇文章講解關于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原始碼設計,閱讀原始碼切忌無目標亂看一波,要有明確的目標、驗證什么問題,針對性尋找那一部分的原始碼,與問題無關的原始碼暫時忽略,不然會在原始碼的海洋里游著游著就溺亡了,

全文到此,感謝你的閱讀

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

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/172425.html

標籤:其他

上一篇:Runtime-iOS運行時應用

下一篇:名片識別,史上最簡單的集成攻略來啦!附有SDK包

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more