Android:窗口管理器WindowManager
导言
本篇文章主要是对Android中与窗口(Window)有关的知识的介绍,主要涉及到的有:
- Window
- WindowManager
- WindowManagerService
主要是为了更进一步地向下地深入Android屏幕渲染的知识(虽然窗口可能并算不上)。
窗口(Window)
Q:什么是窗口
实际上Android上的窗口指的并不是具体的手机窗口而是一个抽象的概念,它本质上也是一个View,我会把窗口理解成一组有关联的View。
与ActivityManager
与ActivityManagerService
的关系类似,WindowManager
中方法的实现也是通过远程调用WindowManagerService
实现的:
窗口的属性
窗口的类型
Window的类型大体来分有三种,我们可以在源文件中找到具体的对应:
-
- 应用程序窗口:最常见的,顶层应用的显示窗口
-
- 子窗口:需要依附在其他窗口的窗口
-
- 系统窗口: Toast,系统输入法窗口,系统错误窗口等
另外,每种窗口还有其对应的TYPE
值,这个值主要是用来确定窗口的显示层次的,应用程序窗口的TYPE值在1-99范围内,子窗口在1000-1999,系统窗口在2000-2999。至于这个TYPE值会如何影响显示层次呢?这里我们可以简单的将这个TYPE值看做是一个z轴的坐标值,也就是垂直于手机屏幕的距离,数值越大,其离手机屏幕就越远,那么显示的优先级也会越高。
当然,实际的情况比这要复杂,会涉及到一些加权的计算,这里我们先简单这样理解即可。
窗口的标志
窗口的标志决定了窗口的一些响应特性,这里直接给出一些常用的flag理解一下:
Flag | 描述 |
---|---|
FLAG_ALLOW_LOCK_WHILE_SCREEN_ON | 只要窗口可见,就允许在开启状态的屏幕上锁屏 |
FLAG_NOT_FOCUSABLE | 窗口不能获得输入焦点,在设置该标志的同时也会将FLAG_NOT_TOUCH_MODAL设置 |
FLAG_NOT_TOUCHABLE | 窗口不接受任何触摸事件 |
FLAG_NOT_TOUCH_MODAL | 将该窗口区域之外的触摸事件传递给其他的Window,而自己只会处理窗口区域内的触摸事件 |
FLAG_KEEP_SCREEN | 只要窗口可见,就会一直保持长亮 |
FLAG_LAYOUT_NO_LIMITS | 允许窗口显示在手机屏幕之外 |
… |
Window的具体实现类PhoneWindow
这个PhoneWindow我们应该在Activity的setContentView方法
中有提及到,这里再简单回顾一下:
public void setContentView(@LayoutRes int layoutResID) {
initViewTreeOwners();
getDelegate().setContentView(layoutResID);
}
//Activity.java中
public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
initWindowDecorActionBar();
}
当我们调用Activity的setContentView方法时
首先会根据后面传入的xml布局文件初始化整棵视图树,之后会获取到Activity自身对应的Window
对象,也就是描述Activity该如何显示的一个View,之后再调用该Window的setContentView
方法,那这个Window对象是在何处被初始化的呢?答案是在Activity的attach
方法中,该方法是在ActivityThread中被调用的:
final void attach(...) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window, activityConfigCallback);
......
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
}
可以清楚的看到此处将Activity对应的PhoneWindow对象实例创建了出来,并将这个对象与一个WindowManager对象绑定起来,所以说上面Activity调用的setContentView
最终是由这个PhoneWindow
对象实例来完成的,最终就会在PhoneWindow中安装一个DecorView,DecorView作为整个PhoneWindow中的第一个View(实际上的根View),并把xml中的内容填充进DecorView的内容部分。
WindowManager(窗口管理者)
WindowManager接口
接下来我们从源码角度先分析一下WindowManager:
public interface WindowManager extends ViewManager
可以看到WindowManager本质上是一个继承了ViewManager接口的一个接口,因为ViewManager比较简单,我们先来看ViewManager接口:
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
首先这个类有一段注释,大概是说:这个接口是让你在Activity中添加或者移除子View的。
实际上这三个方法也很直白,addView
方法用于添加子View,updateViewLayout
用于更新子View,而removeView
方法用于移除子View。
从WindowManager继承了ViewManager这个角度我们也可以看出来Window实际上就是View,WindowManager只不过是在ViewManager接口的基础上添加了对窗口管理的逻辑,包括Window的类型,显示层级等处理。额外的逻辑中根据Window添加了两个方法:
public Display getDefaultDisplay()
(该方法已经废弃,用Context.getDisplay()
进行替代):得到WindowManager所管理的屏幕 (Display)public void removeViewImmediate(View view)
(同步方法,立即移除一个View,会触发View.onDetachFromWindow
回调)
Window绑定WindowManager
一开始给出的一个简单的示意图中我们已经明确了一点:Window是由WindowManager进行管理的,并且在上一段中我们知道Window是在ActivityThread调用的attach
方法之中通过mWindow.setWindowManager
方法来绑定的,这一小段之中我们就来稍微看一眼这个方法的逻辑:
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);
}
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mContext, parentWindow, mWindowContextToken);
}
private WindowManagerImpl(Context context, Window parentWindow,
@Nullable IBinder windowContextToken) {
mContext = context;
mParentWindow = parentWindow;
mWindowContextToken = windowContextToken;
}
可以看到这个方法主要会涉及到三个方法间的跳转,第一个方法中首先会通过Binder通信获取到系统服务之一的WindowService
,之后就会跳转到第二个方法中,创建并返回一个WindowManagerImpl
的实例。然后第三个方法创建这个示例的时候实际上就是对传入的数据进行了一个简单的封装,就是将需要绑定的Window对象
,上下文对象Context
,以及可以与WindowService进行通信的IBinder
对象进行了一个封装:
我觉得这样做的目的也很明显,这样一下WindowManagerImpl同时持有了需要被操作的Window和提供操作服务的WindowService的通信手段,这样一来就可以借助WindowService来操作Window对象了:
最后,我们可以来看一看WindowManagerImpl的addView
方法:
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyTokens(params);
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
可以看到WindowManagerImpl自身并不实现addView
方法,而是将其委托给mGlobal
实现,这个mGlobal
实际上是一个WindowManagerGlobal
对象,所有的WindowManagerImpl对象都是将其委托给WindowManagerGlobal
对象实现的,而WindowManagerGlobal
又是一个单例的对象,所以说实际上所有的WindowManagerImpl都是通过过一个对象来实现对View的操作的。
另外提一嘴,这里WindowManagerImpl将实现分为了抽象和具体两个部分,用到了桥接模式。
//Global是单例的
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
//DCL单例
public static WindowManagerGlobal getInstance() {
synchronized (WindowManagerGlobal.class) {
if (sDefaultWindowManager == null) {
sDefaultWindowManager = new WindowManagerGlobal();
}
return sDefaultWindowManager;
}
}
WindowManager关联类
实际上在上面介绍Window的过程中我们已经差不多已经把关联类介绍过了,此处借用进阶解密中的一张图来总结:
ViewRootImpl–WindowManager与Window的中转站
ViewRootImpl的职责
ViewRootImpl顾名思义就是名义上的View视图树的根节点,它有着多种职责:
- View树的根并且管理整颗视图树
- 触发View的测量,布局和绘制
- 输入事件的中转站
- 管理Surface
- 负责与WMS进行通信
关于ViewRootImpl与WMS的通信,具体是通过一个Session进行的,可以看以下这张图:
ViewRootImpl存储Window
当我们需要将之前创建的PhoneWindow添加到屏幕上时,显然就需要调用到WindowManager
的addView
方法了,具体我们也知道是会委托到WindowManagerGlobal
来执行相关的操作,我们直接跳进WindowManagerGlobal
来看看相关的逻辑:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
........一些错误检查
ViewRootImpl root;
View panelParentView = null;
//上锁
synchronized (mLock) {
//加载参数
//判断当前View是否重复添加
........
IWindowSession windowlessSession = null;
........
if (windowlessSession == null) {
//如果Session为空就新生成一个ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
} else {
//如果Session为空就新生成一个ViewRootImpl,并且把Session传入
root = new ViewRootImpl(view.getContext(), display,
windowlessSession);
}
//设置相关的布局参数
view.setLayoutParams(wparams);
//维护三个列表
//Views列表
mViews.add(view);
//ViewRootImpl列表
mRoots.add(root);
//布局参数列表
mParams.add(wparams);
try {
//调用ViewRootImpl的setView绑定Window
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
相关的重要注释已经在上面的代码处标注出来了,我们可以发现这个方法中动态地维护了WindowManagerGlobal中的三个列表:
@UnsupportedAppUsage
private final ArrayList<View> mViews = new ArrayList<View>();
@UnsupportedAppUsage
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
@UnsupportedAppUsage
private final ArrayList<WindowManager.LayoutParams> mParams =
new ArrayList<WindowManager.LayoutParams>();
可以看到他们都带有@UnsupportedAppUsage
说明是不支持其他非系统App调用的,第一个列表维护的是被添加的View,第二个列表维护的是生成的ViewRootImpl,第三个列表是Window的布局参数。
而在这个addView
的具体方法中,会先生成一个对应的ViewRootImpl
对象作为整颗视图树的根节点,之后还会将被添加的Window和这个根节点绑定起来,这样根节点就可以管理这整颗视图树了。
读到这里相信大家也知道我为什么称ViewRootImpl为WindowManager与Window之间的中转站了:ViewRootImpl作为根节点管理整个Window,当Window中有请求发出的时候第一时间给ViewRootImpl进行处理,然后ViewRootImpl再通过WindowManagerGlobal的Binder机制与WindowManagerService间接地进行通信。
题外话:在子线程真的不能更新UI吗
首先我们需要刷新UI的话首先也是需要通过ViewManager
接口中的updateViewLayout
方法发起的,在具体实现中是交给WindowManagerGlobal实现的:
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
//1-------1
view.setLayoutParams(wparams);
synchronized (mLock) {
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index);
mParams.remove(index);
mParams.add(index, wparams);
root.setLayoutParams(wparams, false);
}
}
这段方法中最重要的就是注释一处的view.setLayoutParams(wparams)
方法中,这个方法还会进行一次跳转,最终会执行到ViewRootImpl的scheduleTraversals方法中:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
可以看到在这里会通过Handler向ViewRoot的handler对象发送一个同步屏障和Runnable
任务,这个任务的具体内容如下:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
实际上就是执行performTraversals()
方法,这个方法我们可很熟悉,就是开启三大流程的方法,而这个过程中一旦涉及到performLayout
方法的执行就会进行一个线程的检查:
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
//检查线程
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
上面出现的checkThread()
方法就是导致我们平时无法在主线程更新UI的原因,具体逻辑如下:
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
这个方法是在ViewRootImpl中执行的,也就是说他检查的是ViewRootImp的mThread
线程是否是当前的线程,至于这个mThread
是在哪里被赋值的,实际上是在其构造函数中被赋值的;
所以说,并不是只有主线程不能更新UI,而是只有创建ViewRootImpl实例的线程才能更新UI。一般情况下ViewRootImpl的创建都是在ActivityThread,也就是主线程中进行的,所以说才会说只有主线程能更新UI。
那有没有别的方法可以让我们在子线程更新UI呢?实际上是有的,比如我们可以使用SurfaceView
或者TextureView
,这些特殊View的绘制过程与一般的View不同,并且他们可以单独持有一个Surface。
我们也可以自己在代码中添加View,然后让添加View和更新View的操作放在一个线程里跑就好了。