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 {
…
// 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呢?
因为contentProvider是伴随着应用的启动而启动的。最终Application的启动会来到ActivityThread的handleBindApplication方法:
/frameworks/base/core/java/android/app/ActivityThread.java
private void handleBindApplication(AppBindData data) {
…
// 获取到Application实例
Application app;
…
try {
app = data.info.makeApplication(data.restrictedBackupMode, null);
…
}
…
}
这里的data.info是LoadedApk对象,我们深入这个方法看一下:
LoadedApk.java(api29)
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
if (mApplication != null) {
return mApplication;
}
…
Application app = null;
…
try {
java.lang.ClassLoader cl = getClassLoader();
…
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
}
…
mActivityThread.mAllApplications.add(app);
mApplication = app;
if (instrumentation != null) {
try {
instrumentation.callApplicationOnCreate(app);
}
…
}
…
return app;
}
构建Application的逻辑也不复杂。首先判断Application如果存在则直接返回。后面通过Instrumentation来创建Application对象,最后再通过Instrumentation来回调Application的onCreate方法,我们分别看一下这两个方法:
Instrumentation.java(api29)
public Application newApplication(ClassLoader cl, String className, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
Application app = getFactory(context.getPackageName())
.instantiateApplication(cl, className);
app.attach(context);
return app;
}
public void callApplicationOnCreate(Application app) {
app.onCreate();
}
Application.java
final void attach(Context context) {
attachBaseContext(context);
mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
}
我们可以看到,直到Application回调onCreate方法,整个过程没有涉及token的赋值。Activity是在attach方法中传入了token参数,而这里Application并没有,所以Application并没有token,也没有初始化PhoneWindow和WindowManager。那么Application的getSystemService返回的是什么呢?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) {
…
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后
分享一份工作1到5年以上的Android程序员架构进阶学习路线体系,希望能对那些还在从事Android开发却还不知道如何去提升自己的,还处于迷茫的朋友!
-
阿里P7级Android架构师技术脑图;查漏补缺,体系化深入学习提升
-
**全套体系化高级架构视频;**七大主流技术模块,视频+源码+笔记
有任何问题,欢迎广大网友一起来交流
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!**
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后
分享一份工作1到5年以上的Android程序员架构进阶学习路线体系,希望能对那些还在从事Android开发却还不知道如何去提升自己的,还处于迷茫的朋友!
-
阿里P7级Android架构师技术脑图;查漏补缺,体系化深入学习提升
[外链图片转存中…(img-N9jCGRrZ-1713444918365)]
-
**全套体系化高级架构视频;**七大主流技术模块,视频+源码+笔记
[外链图片转存中…(img-4yOLy1St-1713444918367)]
有任何问题,欢迎广大网友一起来交流
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!