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);
...
}
這裡的mWindow
和mWindowManager
是什麼?我們到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
方法最終會調用到了WindowManagerGlobal
的addView
方法,具體調用流程可以看我文章開頭的文章:
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的知識有一個整體上的感知,同時也當時前面內容的總結。先來看整體圖:
- token在創建ActivityRecord的時候一起被創建,他是一個IBinder對象,實現了接口IApplicationToken。
- token創建後會發送到WMS,在WMS中封裝成WindowToken,並存在一個HashMap<IBinder,WindowToken>。
- token會隨着ActivityRecord被發送到本地進程,ActivityThread根據AMS的指令執行Activity啟動邏輯。
- Activity啟動的過程中會創建PhoneWindow和對應的WindowManager,同時把token存在PhoneWindow中。
- 通過Activity的WindowManager添加view/彈出dialog時會把PhoneWindow中的token放在窗口LayoutParams中。
- 通過viewRootImpl向WMS進行驗證,WMS在LayoutParams拿到IBinder之後就可以在Map中獲取WindowToken。
- 根據獲取的結果就可以判斷該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源碼設計。閱讀源碼切忌無目標亂看一波,要有明確的目標、驗證什麼問題,針對性尋找那一部分的源碼,與問題無關的源碼暫時忽略,不然會在源碼的海洋里游着游着就溺亡了。
全文到此,感謝你的閱讀
原創不易,覺得有幫助可以點贊收藏評論轉發關注。
筆者能力有限,有任何想法歡迎評論區交流指正。
如需轉載請私信交流。另外歡迎光臨筆者的個人博客:傳送門