WindowManagerService,简称wms服务,见名知意首先是管理window窗口的,它在管理当前系统中所有的window窗口,由此可以引申出另一个重要作用,它主管分配设备产生的touch事件、keydown事件,因为wms能知道当前系统中哪个window最适合来处理某个事件(它在管理所有的window,可以遍历来挑选最优window),硬件产生的某个事件最终是要被软件应用所消费的,那些view、button的按键事件就是通过window再传递给View体系来响应按键的。
与ams、ims类似都是由SystemServer进程启动,在startOtherServices函数中调用wms的main函数,SystemServer很熟悉就不用展示源码了
1、SystemServer在源码位置:/* framework/base/service/java/com/android/server/SystemServer.java*/
2、ams源码位置:/* framework/base/service/core/java/com/android/server/am*/
3、wms源码位置:/* framework/base/service/core/java/com/android/server/wm*/
wms以AIDL的方式来构建binder服务架构,在源码未编译前只有IWindowManager.aidl文件,其提供的所有功能都在此文件中,下面罗列出几个接口函数:
wms运行在SystemServer进程,应用程序访问wms还是用binder驱动来实现IPC通信,wms本身是实名binder服务,可以通过serviceManager获取,然后通过openSession函数建立session连接(由wms提供的匿名服务,IWindowSession),反过来wms如果需要和某个应用程序通信(window),是根据应用进程传递的匿名binder服务,也就是说应用(window)自己提供一个匿名的binder server(IWinder)供wms反向调用。
先看打开session源码例子:
/*framework/base/core/java/com/android/view/ViewRootImpl.java*/
public ViewRootImpl(Context context, Display display) {
this(context, display, WindowManagerGlobal.getWindowSession(),
false /* useSfChoreographer */);
}
/*framework/base/core/java/com/android/view/WindowManagerGlobal.java*/
public static IWindowSession getWindowSession() {
synchronized (WindowManagerGlobal.class) {
if (sWindowSession == null) {
try {
// Emulate the legacy behavior. The global instance of InputMethodManager
// was instantiated here.
// TODO(b/116157766): Remove this hack after cleaning up @UnsupportedAppUsage
InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
IWindowManager windowManager = getWindowManagerService();
sWindowSession = windowManager.openSession(new IWindowSessionCallback.Stub());
...
而IWinder则是:static class W extends IWindow.Stub (源码在/framwork/base/core/java/com/android/view/ViewRootImpl.java)
当一个activity被启动时,首先要在ams中注册,amsnew出来ActivityRecord来记录这个Activity;另外Activity是专门用来显示UI,每个activity对应一个window窗口,所以wms也需要记录联动–以WindowState来保存一个窗口相关的信息。wms还用AppWindowToken来对应ams中的ActivityRecord。
虽然Android系统中的窗口非常多,不过从用途来考虑,我们可以将其分为三大类:
细分的若干子类型都能从WindowManager.java中找到,接下来用表格详细展示window窗口的属性
普通应用程序的窗口都属于这一类
Type | Description |
---|---|
FIRST_APPLICATION_WINDOW=1 | 应用程序窗口类型的起始值 |
TYPE_BASE_APPLICATION=1 | 应用程序窗口类型的基础柱,其他窗口类型一次为基础 |
TYPE_APPLICATION=2 | 普通应用程序窗口类型 |
TYPE_APPLICATION_STARTING=3 | 应用程序的启动窗口类型,应用程序本身不可使用,Android系统自动为应用程序启动前展示的窗口,当应用程序启动后就会消失 |
LAST_APPLICATION_WINDOW=99 | 应用程序窗口类型的最大值 |
见名知意,子窗口,附着在某些窗口中,包括下列一些子类型
Type | Description |
---|---|
FIRST_SUB_WINDOW=1000 | 子窗口类型的起始值 |
TYPE_BAPPLICATION_PANEL=FIRST_SUB_WINDOW | 应用程序的panel子窗口,在它的父窗口之上显示 |
TYPE_APPLICATION_MEDIA=FIRST_SUB_WINDOW+1 | 用于显示多媒体内容的子窗口,位于父窗口之下 |
TYPE_APPLICATION_SUB_PANEL=FIRST_SUB_WINDOW+2 | 也是panel子窗口,位于父窗口以及所有TYPE_APPLICATION_PANEL子窗口之上 |
TYPE_APPLICATION_ATTACHED_DIALOG=FIRST_SUB_WINDOW+3 | Dialog子窗口,类似menu类型 |
TYPE_APPLICATION_MEDIA_OVERLAY=FIRST_SUB_WINDOW+4 | 多媒体窗口的覆盖层,位于TYPE_APPLICATION_MEDIA和应用程序窗口之间,通常透明才有意义,未开放 |
TYPE_SUB_WINDOW=1999 | 子窗口类型的结束值 |
Type | Description |
---|---|
FIRST_SYSTEM_WINDOW=2000 | 系统窗口类型的起始值 |
TYPE_STATUS_BAR=FIRST_SYSTEM_WINDOW | 系统状态栏窗口 |
TYPE_SEARCH_BAR=FIRST_SYSTEM_WINDOW+1 | 搜索条窗口 |
TYPE_PHONE=FIRST_SYSTEM_WINDOW+2 | 通话窗口,一般来电通话位于系统状态栏之下,其他应用窗口之上 |
TYPE_SYSTEM_ALERT=FIRST_SYSTEM_WINDOW+3 | Alert窗口,如电量不足的警告窗口,一般位于所有其他应用程序之上 |
TYPE_KEYGUAED=FIRST_SYSTEM_WINDOW+4 | 屏保窗口 |
TYPE_TOAST=FIRST_SYSTEM_WINDOW+5 | 短暂提示框窗口 |
TYPE_SYSTEM_OVERLAY=FIRST_SYSTEM_WINDOW+6 | 系统覆盖层窗口,不可接受input焦点否则会与屏保冲突 |
TYPE_PRIORITY_PHONE=FIRST_SYSTEM_WINDOW+7 | 电话优先窗口,屏保状态下显示来电窗口 |
TYPE_SYSTEM_DIALOG=FIRST_SYSTEM_WINDOW+8 | RecentAppsDialog就是这种类型窗口 |
TYPE_KEYGUARD_DIALOG=FIRST_SYSTEM_WINDOW+9 | 屏保时显示的对话框 |
TYPE_SYSTEM_ERROR=FIRST_SYSTEM_WINDOW+10 | 系统错误窗口 |
TYPE_INPUT_METHOD=FIRST_SYSTEM_WINDOW+11 | 输入法窗口 |
TYPE_INPUT_METHOD_DIALOG=FIRST_SYSTEM_WINDOW+12 | 输入法窗口之上的对话框窗口 |
TYPE_WALLPAPER=FIRST_SYSTEM_WINDOW+13 | 壁纸窗口 |
TYPE_STATUS_BAR_PANEL=FIRST_SYSTEM_WINDOW+14 | 滑动状态栏出现的窗口 |
TYPE_SECURE_SYSTEM_OVERLAY=FIRST_SYSTEM_WINDOW+15 / TYPE_DRAG=FIRST_SYSTEM_WINDOW+16 / TYPE_STATUS_BAR_SUB_PANEL=FIRST_SYSTEM_WINDOW+17 / TYPE_POINTER=FIRST_SYSTEM_WINDOW+18 | 暂未开放 |
TYPE_NAVIGAGTION_BAR=FIRST_SYSTEM_WINDOW+19 | 导航条 |
TYPE_VOLUME_OVERLAY=FIRST_SYSTEM_WINDOW+20 | 系统音量条 |
TYPE_BOOT_PROGRESS=FIRST_SYSTEM_WINDOW+21 | 启动时的进度条窗口 |
TYPE_HIDDEN_NAV_CONSUMER=FIRST_SYSTEM_WINDOW+22 | 导航条隐藏时用于消费触摸事件的伪窗口 |
TYPE_DREAM=FIRST_SYSTEM_WINDOW+23 / TYPE_NAVIGATION_BAR_PANEL=FIRST_SYSTEM_WINDOW+24 | 暂未开放 |
TYPE_UNIVERSE_BACKGROUND=FIRST_SYSTEM_WINDOW+25 | 多用户系统中,将显示给所有用户 |
TYPE_DISPLAY_OVERLAY=FIRST_SYSTEM_WINDOW+26 | 用于模拟第二个显示设备 |
TYPE_MAGNIFICATION_OVERLAY=FIRST_SYSTEM_WINDOW+27 | 类似放大镜的效果,用于放大显示某部分内容 |
TYPE_RECENTS_OVERLAY=FIRST_SYSTEM_WINDOW+28 | 与TYPE_SYSTEM_DIALOG基本类似,区别在于TYPE_RECENTS_OVERLAY只显示在一个用户的屏幕上 |
LAST_SYSTEM_WINDOW=2999 | 系统窗口结束 |
每种窗口类型都有一个数值,三类窗口间隔是:
当某个应用进程向wms申请窗口时,需要指定所需的窗口类型(当然应用程序基本上不允许创建系统窗口,会在权限检查时被拒绝),然后wms根据用户申请的窗口类型以及当前系统中已有窗口的情况给这个窗口最终分配一个"层级值",系统在运行期间,很可能会出现同一种窗口类型当前有很多个窗口,比如当前有三个应用程序在执行,那类型为TYPE_APPLICATION的应用程序窗口就会有3个,因此wms需要根据具体情况调整他们的最终层级值。数值越大的窗口,在wms中优先级就越高,最终在屏幕显示时就越靠近用户
相同类型的window,要加上窗口间的层级间隔 WINDOW_LAYER+MULTIPLIER = 5,wms的层级值layers是按照WindowType来转换的,有个映射转换表,源码则是在(framework/base/services/core/java/com/android/server/wm/WindowState.java位置的WindowState构造函数中)
/*framework/base/services/core/java/com/android/server/wm/WindowState.java*/
mBaseLayer = mPolicy.getWindowLayerLw(parentWindow)
* TYPE_LAYER_MULTIPLIER + TYPE_LAYER_OFFSET;
mSubLayer = mPolicy.getSubWindowLayerFromTypeLw(a.type);
注:WindowType就是2.1、2.2、2.3里的各种窗口Type。
接下来看WindowType到Layers的映射转换表
WindowType | Layer | value |
---|---|---|
[FIRST_APPLICATION_WINDOW,LAST_APPLICATION_WINDOW] | LAST_APPLICATION_WINDOW | 2 |
TYPE_WALLPAPER | WALLPAPER_LAYER | 2 |
TYPE_PHONE | PHONE_LAYER | 3 |
TYPE_SEARCH_BAR | TYPE_SEARCH_LAYER | 4 |
TYPE_SYSTEM_DIALOG | SYSTEM_DIALOG_LAYER | 5 |
TYPE_TOAST | TOAST_LAYER | 6 |
TYPE_PRIORITY_PHONE | PRIORITY_PHONE_LAYER | 7 |
TYPE_SYSTEM_ALERT | SYSTEM_ALERT_LAYER | 8 |
TYPE_INPUT_METHOD | INPUT_METHOD_LAYER | 9 |
TYPE_INPUT_METHOD_DIALOG | INPUT_METHOD_DIALOG_LAYER | 10 |
TYPE_KEYGUARD | KEYGUARD_LAYER | 11 |
TYPE_KEYGUARD_DIALOG | KEYGUARD_DIALOG_LAYER | 12 |
TYPE_DREAM | SCREENSAVER_LAYER | 13 |
TYPE_STATUS_BAR_SUB_PANEL | STATUS_BAR_SUB_PANEL_LAYER | 14 |
TYPE_STATUS_BAR | STATUS_BAR_LAYER | 15 |
TYPE_STATUS_BAR_PANEL | STATUS_BAR_PANEL_LAYER | 16 |
TYPE_VOLUME_OVERLAY | VOLUME_OVERLAY_LAYER | 17 |
TYPE_SYSTEM_OVERLAY | SYSTEM_OVERLAY_LAYER | 18 |
TYPE_NAVIGATION_BAR | NAVIGATION_BAR_LAYER | 19 |
TYPE_NAVIGATION_BAR_PANEL | NAVIGATION_BAR_PANEL_LAYER | 20 |
TYPE_SYSTEM_ERROR | SYSTEM_ERROR_LAYER | 21 |
TYPE_DRAG | DRAG_LAYER | 22 |
TYPE_SECURE_SYSTEM_OVERLAY | SECURE_SYSTEM_OVERLAY_LAYER | 23 |
TYPE_BOOT_PROGRESS | BOOT_PROGRESS_LAYER | 24 |
TYPE_POINTER | POINTER_LAYER | 25 |
TYPE_HIDDEN_NAVCONSUMER | HIDDEN_NAVCONSUMER_LAYER | 26 |
mBaseLayer = mPolicy.getWindowLayerLw(parentWindow)
* TYPE_LAYER_MULTIPLIER + TYPE_LAYER_OFFSET;
mSubLayer = mPolicy.getSubWindowLayerFromTypeLw(a.type);
对于SubWindow来说,窗口类型取决于父窗口类型,
上一节我们说到了mPolicy.getWindowLayerLw(parentWindow),这个mPolicy是什么?WindowManagerPolicy窗口策略,我们知道Android不是单一的产品设备系统,手机、平板、现在都要进军电脑了,那UI窗口的样式必定要有差异,例如手机设备一般是有StatusBar,平板pad电脑一般就没有,它是CombinedBar,这种差异化的窗口属性组成了某种设备的UI属性,Google试图在一套源码上实现各式各样的差异,动态的配置就注定需要一个manager来管理,这就是WindowManagerPolicy的设计理念。
在Android源码实现中,与window policy相关的类主要是4个,WindowManagerPolicy,PhoneWindowManager,PolicyManager和Policy。
PolicyManager:
1、makeNewWindow–new一个window,默认是PhoneWindow
2、makeNewWindowManagerPolicy–产生一个管理Window的Policy,
WindowManagerPolicy是窗口管理策略的接口类,
除了WindowType,wms中还有很多其他的窗口属性,在APP开发时我们会遇到大量的View控件,后者天然存在各种LayoutParams,各种属性都存在于WindowManager:
Flags | Description |
---|---|
FLAG_ ALLOW_LOCK_WHILE_SCREEN_ON | 当此窗口可见,即使屏幕开启时也允许锁屏 |
FLAG_DIM_BEHIND | 在窗口后面的所有东西都将变暗dimmed |
FLAG_NOT_FOCUSABLE / FLAG_NOT_TOUCHABLE / FLAG_NOT_TOUCHABLE_MODAL | 窗口不获取焦点和事件,事件会直接传递给窗口后的下一级,一般前两者同时设置 |
FLAG_TOUCHABLE_WHEN_WAKING | 当设备进入睡眠状态,设置此标志可以使此窗口获得第一次的触摸事件,因为此时用户不知道他们当下点击的到底是哪一个窗口,因此都是由系统来指定 |
FLAG_KEEP_SCREEN_ON | 只要这个窗口可见,屏幕就一直亮不会锁屏 |
FLAG_LAYOUT_IN_SCREEN | 窗口显示时不考虑系统窗口,例如Status Bar |
FLAG_LAYOUT_NO_LIMITS | 允许窗口超过屏幕区域 |
FLAG_FULLSCREEN | 隐藏所有屏幕装饰窗口,例如Status Bar,在视频全屏播放中很常用 |
FLAG_FORCE_NOT_FULLSCREEN | 正好和FLAG_FULLSCREEN 相反 |
FLAG_SECURE | 窗口内容被认为是保密的,不允许截屏 |
FLAG_SCALED | 按照用户提供参数进行伸缩调整 |
FLAG_IGNORE_CHEEK_PRESSES | 有时候用户和屏幕会贴的很近,例如打电话的时候,这种情况下出现的某些事件不应该去响应的 |
FLAG_LAYOUT_INSET_DECOR | 只能和FLAG_LAYOUT_IN_SCREEN 同时使用,当设置全屏显示布局时,应用窗口部分内容可能还会被系统窗口覆盖,设置此Flag后,系统就会计算系统装饰窗口占的区域,避免这种问题 |
FLAG_SHOW_WHEN_LOCKED | 使窗口能在锁屏窗口之上显示 |
FLAG_WALLPAPER | 让壁纸在这个窗口之后显示,例如Launcher APP,当窗口是透明、半透明时就可以看到后面的壁纸背景 |
FLAG_TURN_SCREEN_ON | 窗口显示时将屏幕点亮 |
FLAG_DISMISS_KEYGUARD | 只要不是secure lock,设置这个Flag可以解锁屏幕, |
FLAG_HARDWARE_ACCELERATED | 指明窗口是否需要硬件加速 |
可以通过以下方法来设置窗口属性
Window w = activity.getWindow();
w.setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED |
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON )
名称就能看出来此变量表示的是系统UI的可见性,但SystemUIVisibility的可选Flags值定义在View里,而不是WindowManager
Flags | Description |
---|---|
SYSTEM_UI_FLAG_VISIBLE | View视图请求显示System UI |
SYSTEM_UI_FLAG_LOw_PROFILE | View视图请求进入 low profile 模式,代表Status Bar/Navigation icons有可能被调暗,一般游戏、电子书阅读器等应用可能用得上 |
SYSTEM_UI_FLAG_HIDE_NAVIGATION | 用于隐藏Navigation导航栏的隐藏,这个标志经常和FLAG_LAYOUT_IN_SCREEN和FLAG_LAYOUT_IN_SCREEN一起使用,这样才能真正的全屏显示 |
SYSTEM_UI_FLAG_FULLSCREEN | 整个View将进入全屏显示,它与WindowManager.LayoutParams.FLAY_FULLSCREEN视觉效果基本相同,不过和Window提供的标志不同的是,对于使用Window.FEATURE_ACTION_BAR_OVERLAY的ActionBar,SystemUi是这个标志也能把它隐藏掉。一般来说如果是暂时的全屏显示需求,可以用SYSTEM_UI_FLAG_FULLSCREEN,而如果是长时间的全屏显示比如游戏界面中,就去用Window的标志 |
SYSTEM_UI_FLAG_LAYOUT_STABLE | 系统将尽力保证UI布局稳定 |
因为这个Flags是作用在view上的,因此当我们希望Activity中整个View都使用同一个systemUIVisibility,可以给DecorView设置,其实我们看上述的那些Flag,基本是影响了整个窗口,如果只给某个Button设置也不像话。下面看使用示例:
int newViewFlags = View.SYSTEM_UI_FLAG_LOw_PROFILE|SYSTEM_UI_FLAG_HIDE_NAVIGATION|SYSTEM_UI_FLAG_FULLSCREEN;
getWindow().getDecorView().setSystemUiVisibility(newViewFlags);
Android中展示的窗口虽然很多,从使用角度分类,其实能把它分成应用程序使用的窗口和系统使用的窗口,分别叫成“应用窗口”、“系统窗口”,我们分别追踪两者添加过程。
使用SystemUI应用中StatusBar举例说明:
/*/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java*/
private void addStatusBarWindow() {
makeStatusBarView();
mStatusBarWindowManager = Dependency.get(StatusBarWindowManager.class);
mStatusBarWindowManager.add(mStatusBarWindow, getStatusBarHeight());
}
/*frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java*/
public void add(View statusBarView, int barHeight) {
mLp = new WindowManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
barHeight,
WindowManager.LayoutParams.TYPE_STATUS_BAR,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
| WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
| WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
PixelFormat.TRANSLUCENT);
mLp.token = new Binder();
mLp.gravity = Gravity.TOP;
mLp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
mLp.setTitle("StatusBar");
mLp.packageName = mContext.getPackageName();
mLp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
mStatusBarView = statusBarView;
mBarHeight = barHeight;
mWindowManager.addView(mStatusBarView, mLp);//Step1
mLpChanged = new WindowManager.LayoutParams();
mLpChanged.copyFrom(mLp);
}
上面就是StatusBar能够显示在屏幕上最关键的部分,mWindowManager.addView(),其作用是加入到wms中,变量mWindowManager是一个WindowManager对象
/*frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java*/
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
WindowManager是一个接口类,继承ViewManager,WindowManager的实现类是WindowManagerImpl,伪代码:mWindowManager = new WindowManagerImpl;于是下一步转向WindowManagerImpl.addView();
/*framework/base/core/view/WindowManagerImpl.java*/
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
于是又转向WindowManagerGlobal.addView;
/*framework/base/core/view/WindowManagerGlobal.java*/
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
root = new ViewRootImpl(view.getContext(), display);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
root.setView(view, wparams, panelParentView);//Step1
...
}
继续转向ViewRootImpl.setView,
/*framework/base/core/view/ViewRootImpl.java*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
//Step1,设置响应Vsync垂直同步刷新,就是由硬件产生通过
//SurfaceFlinger传输的vsync_app信号
requestLayout();
/*mWindowSession,应该要很熟悉了,开篇讲wms所提供的接口中
第一个接口就是openSession()
*/
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
...
}
//mWindowSession是从哪里来的??
mWindowSession = WindowManagerGlobal.getWindowSession();
/*framework/base/core/view/WindowManagerGlobal.java*/
public static IWindowSession getWindowSession() {
synchronized (WindowManagerGlobal.class) {
if (sWindowSession == null) {
try {InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
IWindowManager windowManager = getWindowManagerService();
//这里我们看到了openSession
sWindowSession = windowManager.openSession(
new IWindowSessionCallback.Stub() {
@Override
public void onAnimatorScaleChanged(float scale) {
ValueAnimator.setDurationScale(scale);
}
});
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
return sWindowSession;
}
}
mWindowSession.addToDisplay()就是应用窗口与wms的通信了,类型是IWindowSession ,第一个参数mWindow,IWindow类型的匿名Binder服务,用来让wms主动和当前窗口通信用的,我们能发现没有给wms传递View树相关的参数,这是因为wms不关系你这个窗口里具体展示的是哪些view,wms只管这个窗口的size大小、窗口层级值等等(都放在WindowManager.LayoutParams属性中),接下来经过Binder驱动的跨进程,流程就从应用进程到了wms的进程中,IWindowSession在wms服务端的实现是Session,函数addToDisplay(早期版本这个函数叫add,能感觉出来Android在为多显示屏不停的做努力)又调用到了WMS的addWindow:
/*framework/base/service/core/java/com/android/server/wm/Session.java*/
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);
}
/*framework/base/service/core/java/com/android/server/wm/WindowManagerService.java*/
public int addWindow(Session session, IWindow client, int seq,
LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
Rect outContentInsets, Rect outStableInsets,
DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
InsetsState outInsetsState, InsetsSourceControl[] outActiveControls,
int requestUserId) {
int[] appOp = new int[1];
int res = mPolicy.checkAddPermission(attrs, appOp);/*Step1.权限检查*/
final int type = attrs.type;//窗口类型
synchronized (mGlobalLock) {
...
if (mWindowMap.containsKey(client.asBinder())) {/*Step2.避免重复添加*/
Slog.w(TAG_WM, "Window " + client + " is already added");
return WindowManagerGlobal.ADD_DUPLICATE_ADD;
}
if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {/*子窗口*/
//寻找父窗口
parentWindow = windowForClientLocked(null, attrs.token, false);
...
}
//Step4
token = new WindowToken(this, binder, type, false, displayContent,
session.mCanAddInternalSystemWindow, isRoundedCornerOverlay);
} else if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
atoken = token.asAppWindowToken();
if (atoken == null) {
return WindowManagerGlobal.ADD_NOT_APP_TOKEN;
} else if (atoken.removed) {
return WindowManagerGlobal.ADD_APP_EXITING;
} else if (type == TYPE_APPLICATION_STARTING && atoken.startingWindow != null) {
return WindowManagerGlobal.ADD_DUPLICATE_ADD;
} ...
else if (token.asAppWindowToken() != null) {
attrs.token = null;
token = new WindowToken(this, client.asBinder(), type, false, displayContent,
session.mCanAddInternalSystemWindow);
}
}
...
/*Step5.*/
final WindowState win = new WindowState(this, session, client, token, parentWindow,
appOp[0], seq, attrs, viewVisibility, session.mUid,
session.mCanAddInternalSystemWindow);
if (win.mDeathRecipient == null) {//Step6.当客户端死亡就不执行
return WindowManagerGlobal.ADD_APP_EXITING;
}
...
final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy();
displayPolicy.adjustWindowParamsLw(win, win.mAttrs, Binder.getCallingPid(),
Binder.getCallingUid());/*Step7.调整window的属性*/
win.setShowToOwnerOnlyLocked(mPolicy.checkShowToOwnerOnly(attrs));
res = displayPolicy.prepareAddWindowLw(win, attrs);
...
if (displayPolicy.getLayoutHintLw/*Step8、ContentInset计算*/(win.mAttrs, taskBounds, displayFrames, floatingStack,
outFrame, outContentInsets, outStableInsets, outOutsets, outDisplayCutout)) {
res |= WindowManagerGlobal.ADD_FLAG_ALWAYS_CONSUME_SYSTEM_BARS;
}
...
win.getParent().assignChildLayers();//Step9、分配最终的层级值
...
}
Step1、进行权限检查,type
Step2、变量mWindowMap是一个< IBinder,WindowState>类型的map,IBinder是IWindow,因此mWindowMap是wms与IWindow相对应的WindowState的映射。下面还有< IBinder,WindowToken>类型的mTokenMap,两者是有区别的,一个IWindow只允许添加唯一的窗口(在wms看来,就是WindowState,而对于应用程序来说是可以持有多个ViewRoot,其ViewTree没有理论的上限)
Step3、如果要添加的窗口是子窗口,就得找出它的父窗口,要注意这个父窗口不能同时是其他窗口的子窗口,不然会添加失败
Step4:
Step5、一切顺利后,wms就会为这个窗口创建WindowState,WindowState win = new WindowState(this, session, client, token, parentWindow,appOp[0], seq, attrs, viewVisibility, session.mUid,session.mCanAddInternalSystemWindow);
WindowState构造函数中的几个参数:this是WMS自身;session是wms提供给窗口使用的IWindowSession;client是IWindow,窗口提供给wms的调用通道;token是WindowToken;parentWindow是父窗口;。。。
Step6、当客户端死亡就不执行 例如应用进程crash
这样一个System window就add完毕
其实对于wms来说,不论你是应用窗口也好,系统窗口也好,它的整体流程基本一样,只不过层级和权限有差异。当启动一个activity,ams首先判断前者所属进程是否在运行,有的话就发启动命令给这个进程,否则先让zygote来fork应用进程,创建ActivityThread主线程,然后在H这个handler中处理启动activity的操作。
activity天生拥有显示UI界面的能力,且需要wms服务参与支持,当ams发现一个activity需要启动时,它需要将对应信息告知wms,那么activity进程到底是什么时候调用addView,进而由wms处理addWindow呢?
/*framework/base/core/java/android/app/ActivityThread.java*/
public void handleResumeActivity(handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason){
//activity的onResume函数将被调用
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
...
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();//Step1
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
ViewRootImpl impl = decor.getViewRootImpl();
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);//Step2
}
handlerResumeActivity这个函数里的逻辑我们应该要感觉很熟悉:
Step1、与系统窗口添加的逻辑相同,ViewManager是接口类,最终实现类是WindowManagerImpl,Step2、其addView和系统窗口添加时的addView完全相同,会走到wms服务端的Session.java中,再去调用wms.addWindow,这些流程完全一样,接下来我们看看应用窗口和系统窗口的差异究竟在哪?
根据上一个小节我们知道wms在添加应用窗口或者系统窗口的主体框架上完全相同,实质上wms就是等某个用户使用openSession建立一个session通道,然后用这个session调用addToDisplay经过Binder驱动把窗口的参数传递到wms,就这么简单,一般wms都会为用户生成一个WindowState来管理窗口,然而,后续将用于窗口添加到全局窗口列表中的处理流程就会产生差异,主要是应用窗口的层级值一般会比系统窗口低,比如StatusBar在默认的时候它一直会显示出来而且会overlay应用窗口的一部分,也就是前面说的权限和层级值不同,一般系统窗口会更重要一些,详细逻辑可以翻源码去查看
我们能看到wms只关心window的size、attr和layer,SurfaceFlinger才是把众多window数据合成起来的核心系统服务,显然当wms将window的size或者Z-order等layer属性调整后必须要通知SurfaceFlinger,回想SurfaceFlinger体系(SurfaceFlinger传送门),有两种本地窗口,一种是用于在本地设备适配OpenGL ES渲染后的窗口-FrameBufferNativeWindow(提供给Gralloc这个hal使用);一种是用于封装SurfaceFlinger管理的BufferQueue-Surface(提供给应用层使用),而UI界面的绘制必须要有画板,应用的view才能在其上作画,因此无论是系统窗口还是应用窗口,都必须向SurfaceFlinger申请layer拿到FrameBuffer才能进行。
SurfaceFlinger服务端(BufferQueue)生产的就是surface,只不过它属于native层(c++),这些知识点有了才能整体将Android显示系统串联起来,此小节的作用就是阐述应用层是怎么拿到BufferQueue提供的本地surface
此时应当要有个疑惑点 :Surface和Window的区别是什么?先预留
应用层Surface是从哪里开始申请init?还是从ViewRootImpl开始,当Gralloc模块的场频信号产生sync_app通过SurfaceFlinger上报到ViewRootImpl的Choreographer,mTraversalRunnable回调执行,doTraversal->performTraversals->relayoutWindow,从relayoutWindow能看出来,当目前的FrameBuffer不符合(或者首次调用performTraversals),它需要重新申请buffer,详细来看relayoutWindow函数:
/*framework/base/core/view/ViewRootImpl.java*/
public final Surface mSurface = new Surface();//Step1
private final SurfaceControl mSurfaceControl = new SurfaceControl();
private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
boolean insetsPending) throws RemoteException {
int relayoutResult = mWindowSession.relayout(mWindow, params,
(int) (mView.getMeasuredWidth() * appScale + 0.5f),
(int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility,
insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets,
mTempControls, mSurfaceSize);//Step2
...
mSurface.copyFrom(mSurfaceControl);//Step3
...
}
mWindowSession根据上小节应该很熟悉了,与wms通信的session,显然relayout是通过binder调用到wms服务端的函数;mSurfaceControl是封装JNI调用的类,它只是个中介,最终为了赋值给mSurface;而mSurface就是应用层使用的画板。
这里的逻辑很容易看懂,应用进程通过IwindowSession来调用relayout函数,让wms服务端去跟SurfaceFlinger申请画板,最后copyFrom赋值给应用层的Surface来使用,要注意在wms申请之前应用层的Surface是个空壳子,只有copyFrom赋值后才是有效的FrameBuffer(应用层就是Surface)。
首先思考下wms申请的画板,应用进程要如何来用呢?毕竟Linux或者说Android系统是沙盒进程,还是使用binder来跨进程传递,回想下SurfaceFlinger,后者的服务端GraphicBuffer产生一个buffer,怎么通过binder跨进程传递给client进程,wms就怎么做的,只不过SurfaceFlinger是用c++,wms是java语言,用aidl来做,来复习下aidl语言。relayout函数在wms服务的Session匿名binder服务中
/*framework/base/core/java/android/IWindowSession.aidl*/
int relayout(IWindow window, in WindowManager.LayoutParams attrs,
int requestedWidth, int requestedHeight, int viewVisibility,
int flags, long frameNumber, out ClientWindowFrames outFrames,
out MergedConfiguration outMergedConfiguration, out SurfaceControl outSurfaceControl,
out InsetsState insetsState, out InsetsSourceControl[] activeControls,
out Point outSurfaceSize);
/*frameworks/base/service/core/java/com/android/server/wm/Session.java*/
public int relayout(IWindow window, WindowManager.LayoutParams attrs,
int requestedWidth, int requestedHeight, int viewFlags, int flags, long frameNumber,
ClientWindowFrames outFrames, MergedConfiguration mergedConfiguration,
SurfaceControl outSurfaceControl, InsetsState outInsetsState,
InsetsSourceControl[] outActiveControls, Point outSurfaceSize) {
int res = mService.relayoutWindow(this, window, attrs,
requestedWidth, requestedHeight, viewFlags, flags, frameNumber,
outFrames, mergedConfiguration, outSurfaceControl, outInsetsState,
outActiveControls, outSurfaceSize);//mService应该很熟悉,wms
}
其实这里主要想说下aidl语法中的 in out,看到SurfaceControl 形参了没,前面带个out,就代表它是一个函数出参,aidl工具在生成java文件的时候,会为客户端的proxy和服务端的stub适配参数的parcable,详细来说就是服务端的stub会用writeToParcel写,而客户端的proxy会用readFormParcel读,binder跨进程中的对象就是这么传递的(或者说这样把一个对象的内容还原出来的)
继续看relayoutWindow逻辑(看wms中relayoutWindow函数逻辑),可以联想activity跳转逻辑,一个Activity显示,必然伴随着上一个Activity destroy,可想而知在这个过程中Surface必然也会存在创建与销毁,也就是viewVisibility和Gone,我们先来分析Visibility时申请Surface的情况
/*framework/base/service/core/java/com/android/server/wm/WindowManagerService.java*/
public int relayoutWindow(Session session, IWindow client, LayoutParams attrs,
int requestedWidth, int requestedHeight, int viewVisibility, int flags,
long frameNumber, ClientWindowFrames outFrames, MergedConfiguration mergedConfiguration,
SurfaceControl outSurfaceControl, InsetsState outInsetsState,
InsetsSourceControl[] outActiveControls, Point outSurfaceSize) {
synchronized (mGlobalLock) {
//Step1、寻找对应窗口的WindowState
final WindowState win = windowForClientLocked(session, client, false);
...
// We should only relayout if the view is visible, it is a starting window, or the
// associated appToken is not hidden.
//用很大篇幅来判断和配置surface
final boolean shouldRelayout = viewVisibility == View.VISIBLE &&
(win.mActivityRecord == null || win.mAttrs.type == TYPE_APPLICATION_STARTING
|| win.mActivityRecord.isClientVisible());
//真正开始创建surface
if (shouldRelayout) {
result = win.relayoutVisibleWindow(result, attrChanges);
try {
result = createSurfaceControl(outSurfaceControl, result, win, winAnimator);
} catch (Exception e) {
return 0;
}
if (viewVisibility == View.VISIBLE && winAnimator.hasSurface()) {
winAnimator.mSurfaceController.getSurfaceControl(outSurfaceControl);
} else {
try {
outSurfaceControl.release();///生成Surface失败
} finally {
...
}
}
if (focusMayChange) {
if (updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL, true /*updateInputWindows*/)) {//调整分配层级
imMayMove = false;
}
}
// updateFocusedWindowLocked() already assigned layers so we only need to
//reassign them at this point if the IM window state gets shuffled
boolean toBeDisplayed = (result & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0;
if (toBeDisplayed) {//重新调整
// Little hack here -- we -should- be able to rely on the function to return
// true if the IME has moved and needs its layer recomputed. However, if the IME
// was hidden and isn't actually moved in the list, its layer may be out of data
// so we make sure to recompute it.
displayContent.assignWindowLayers(false /* setLayoutNeeded */);
}
}
}
private int createSurfaceControl(SurfaceControl outSurfaceControl, int result, WindowState win,
WindowStateAnimator winAnimator) {
if (!win.mHasSurface) {
result |= RELAYOUT_RES_SURFACE_CHANGED;
}
WindowSurfaceController surfaceController;
try {
//wms并不直接生成Surface,而是由WindowStateAnimator提供createSurfaceLocked
surfaceController = winAnimator.createSurfaceLocked(win.mAttrs.type, win.mOwnerUid);
} finally {
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
if (surfaceController != null) {
surfaceController.getSurfaceControl(outSurfaceControl);
} else {
outSurfaceControl.release();
}
return result;
}
/*frameworks/base/service/core/java/com/android/server/wm/WindowStateAnimator.java*/
WindowSurfaceController createSurfaceLocked(int windowType, int ownerUid) {
final WindowState w = mWin;
if (mSurfaceController != null) {
return mSurfaceController;
}
mChildrenDetached = false;
if ((mWin.mAttrs.privateFlags & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0) {
windowType = SurfaceControl.WINDOW_TYPE_DONT_SCREENSHOT;
}
w.setHasSurface(false);
resetDrawState();
mService.makeWindowFreezingScreenIfNeededLocked(w);
int flags = SurfaceControl.HIDDEN;
final WindowManager.LayoutParams attrs = w.mAttrs;
if (mService.isSecureLocked(w)) {
flags |= SurfaceControl.SECURE;
}
calculateSurfaceBounds(w, attrs, mTmpSize);
final int width = mTmpSize.width();
final int height = mTmpSize.height();
mLastClipRect.set(0, 0, 0, 0);
// Set up surface control with initial size.
try {
final boolean isHwAccelerated = (attrs.flags & FLAG_HARDWARE_ACCELERATED) != 0;
final int format = isHwAccelerated ? PixelFormat.TRANSLUCENT : attrs.format;
if (!PixelFormat.formatHasAlpha(attrs.format)
// Don't make surface with surfaceInsets opaque as they display a
// translucent shadow.
&& attrs.surfaceInsets.left == 0
&& attrs.surfaceInsets.top == 0
&& attrs.surfaceInsets.right == 0
&& attrs.surfaceInsets.bottom == 0
// Don't make surface opaque when resizing to reduce the amount of
// artifacts shown in areas the app isn't drawing content to.
&& !w.isDragResizing()) {
flags |= SurfaceControl.OPAQUE;
}
mSurfaceController = new WindowSurfaceController(mSession.mSurfaceSession,
attrs.getTitle().toString(), width, height, format, flags, this,
windowType, ownerUid);
mSurfaceController.setColorSpaceAgnostic((attrs.privateFlags
& WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC) != 0);
setOffsetPositionForStackResize(false);
mSurfaceFormat = format;
w.setHasSurface(true);
} catch (OutOfResourcesException e) {
mService.mRoot.reclaimSomeSurfaceMemory(this, "create", true);
mDrawState = NO_SURFACE;
return null;
} catch (Exception e) {
mDrawState = NO_SURFACE;
return null;
}
mLastHidden = true;
return mSurfaceController;
}
能看到这里是创建Surface(WindowSurfaceController对象是操作surface的controller,找到这个类随意看一眼就能理顺逻辑,源码行数很短),这里我们也没有找到surface和SurfaceFlinger通信的地方,继续来追踪流程
Android显示系统中,与Surface相关的类非常多,应用层、native层:Surface.java、Surface.cpp、Isurface、IGraphicBufferProducer等等,需要针对这些做好区分认知。
对于java层应用来说,Surface.java是其直接使用的绘图画板,但这个Surface是需要底层的Surface赋值,而后者buffer的来源是从SurfaceFlinger体系中的BufferQueue来quest一个buffer。
/*frameworks/base/core/jni/android_view_SurfaceControl.cpp*/
```cpp
static jlong nativeCreate(JNIEnv* env, jclass clazz, jobject sessionObj,
jstring nameStr, jint w, jint h, jint format, jint flags, jlong parentObject,
jobject metadataParcel) {
ScopedUtfChars name(env, nameStr);
sp<SurfaceComposerClient> client;//Step1、出现了在SurfaceFlinger体系中的代表SurfaceFlingerComposerClient
if (sessionObj != NULL) {//Step2、
client = android_view_SurfaceSession_getClient(env, sessionObj);
} else {
client = SurfaceComposerClient::getDefault();
}
SurfaceControl *parent = reinterpret_cast<SurfaceControl*>(parentObject);
sp<SurfaceControl> surface;
LayerMetadata metadata;
Parcel* parcel = parcelForJavaObject(env, metadataParcel);
if (parcel && !parcel->objectsCount()) {
status_t err = metadata.readFromParcel(parcel);
}
status_t err = client->createSurfaceChecked(
String8(name.c_str()), w, h, format, &surface, flags, parent, std::move(metadata));
if (err == NAME_NOT_FOUND) {
return 0;
} else if (err != NO_ERROR) {
jniThrowException(env, OutOfResourcesException, NULL);
return 0;
}
surface->incStrong((void *)nativeCreate);
return reinterpret_cast<jlong>(surface.get());
}
Step1、能看到出现了SurfaceFlinger体系中熟悉的SurfaceFlingerComposerClient,surface在前者的帮助下有序申请和使用buffer,
Step2、当sessionObj 不等于null时能看到直接去获取SurfaceFlingerComposerClient,也就是说在java层就创建好了,通过jni传递下来,它是从哪来的?
来源是这样的:
当addWindow被调用时,wms会为窗口建立WindowState,并将Session保存到其成员变量mSession中,接着WindowState.attach被调用,mSession就通过windowAddedLocked进一步生成mSurfaceSession,WindowState构造时,同时生成内部mWinAnimator,后者构造时也将Session保存在mSession成员中,,其成员变量在createSurfaceLocked中传递给Surface的构造函数,当ViewRootImpl的Surface开始调用nativeReadFromParcel函数后,后者会创建出sp< IGraphicBufferProducer> gbp,然后将gbp用构造参数的形式jni返回给java层的Surface,因此上述java层会有SurfaceFlingerComposerClient。
这样上层的surface就和surfaceflinger所管理的BufferQueue建立了关联,由view->activity->wms->surfaceflinger这条线就能完整串起来了
以activity窗口来说,除了应用窗口至少是有statusBar+输入法窗口,输入法窗口在AndroidManifast.xml中可以指定配置(windowSoftInputMode属性,可叠加)-以state开头的属性描述的是当前activity有焦点的时候,软键盘是隐藏还是显示;以adjust开头的部分属性描述的是如何调整activity窗口用来容纳软键盘,当然也可能有导航栏等等其他窗口,因此窗口大小的计算其实就是计算这些(屏幕大小是固定的,系统需要显示的宽高确定下来以后,再重新计算应用窗口大小),根本上来说窗口只需要w、h两个参数,但现实情况会很复杂,因此在ViewRootImpl中包括了以下相关变量
Member | Description |
---|---|
int mWidth Height | 当前真实宽高 |
Rect mWinFrame | 当Activity的窗口大小需要改变时,WMS通过W.resized接口通知客户端,mWinFrame就用于记录WMS提供的宽高 |
Rect mPendingVisibleInsets | 同上,用于表示可见区域 |
Rect mPendingContentInsets | 同上,用于表示内容区域 |
int desiredWindowWidth desiredWindowHeight | 中间值,用于表示期望的宽高值,不一定是最终值 |
contentInsets内容区域,visibleInsets可见区域,一般来说俩者一样大,但有时候输入法窗口会覆盖在窗口之上,此时可见区域就比内容区域小得多了(因为输入法窗口占了一大部分窗口的size)。
performTraversals是整个窗口计算的起点,如果窗口尺寸发生变化则(或者是首次调用)调用relayoutWindow(),进而调用relayout函数,其后者通知wms去计算的binder函数,这些在前边小节已有介绍,WMS服务的relayoutWindow接口函数中对窗口大小的计算流程是:performLayoutAndPlaceSurfacesLocked()-> performLayoutAndPlaceSurfaceLockedLoop()-> performLayoutAndPlaceSurfacesLockedInner()->performLayoutLockedInner(),这个函数有三个重要节点(接口函数都在PhoneWindowManager类中):beginLayoutLw()–初始化、layoutWindowLw()–执行计算过程、finishLayoutLw()–完成清理工作。layoutWindowLw()–执行计算过程中会根据各自系统窗口产生父窗口大小、屏幕大小、内容区域、可见区域,然后由win.computeFrameLw来计算最终结果,高版本上上述函数可能名称上有差异,或者所存在的位置不同,但梳理流程逻辑的本质是一样的。经过这一系列调用,WindowManagerService.relayoutWindow才算是真正结束了,wms将其计算出的结果值通过函数几个出参(outContentInsets、outVisibleInsets、outConfig、outSurface等)返回给ViewRootImpl,进而来影响UI的显示。
我们应该或多或少都接触过,新activity启动时,在应用进程尚未真正显示之前,由系统显示一个启动窗口,直到activity主界面显示,启动窗口才会消失,也叫APP的冷启动优化,这个启动窗口完全是由系统来管理其生命周期的,可以猜测必然是AMS和WMS交替完工,
当AMS在处理startActivity时,会走到Task.startActivityLocked,如果发现目标进程未启动或者是一个新的task,此时就会准备启动一个Preview window,受两个变量影响
两者同时为true,在Task.startActivityLocked中,ActivityRecord@showStartingWindow()-> ActivityRecord@addStartingWindow(),到wms服务中,下一步由PhoneWindowManager来makeNewWindow,由WindowManagerImpl实现(addView函数),跳到ViewRootImpl@setView,最后通知IWindowSession.add(),这里能看出来添加一个启动窗口和普通应用窗口在这些流程上是没有区别的,这个window(窗口)的属性如下:
启动窗口的销毁就正好和创建反着来,一旦应用程序的主窗口显示,与之相关的启动窗口就会被销毁(系统在某些特殊情况下也会移除窗口),发送消息让WMS来处理(remove_starting或者是finished_starting)->PhoneWindowManager@removeStartingWindow->WindowManagerImpl@removeView->ViewRootImpl@die->doDie->dispatchDetachedFromWindow->Surface@release->Session@remove()。
流程看着复杂,可以分解:
wms的服务大体上描述的差不多,后边源码需要看官们亲自去翻,有问题欢迎留言讨论。