Android----Window 窗口机制
2018-10-13 本文已影响34人
海盗的帽子
一.前言
Window 在 Android 有两种含义,一种是狭义的,一种是广义的。狭义的 Window 指的是 Window 这个抽象类,它的唯一的实现是 PhoneWindow 。广义的 Window 指的是 Android 中表示的窗口的概念,所谓的 Window 窗口机制实际上说的是广义的 Window ,在 Android 中窗口具体可以划分为 三种:
- 应用程序窗口:对应 Activity 显示的视图
- 子窗口:对应 Dialog ,PopupWindow ,Menu 等
- 系统窗口:对应 StatusBar ,Toast ,输入法键盘等
在 Window 窗口机制中,这三种类型就统称为 Window ,因为它们的作用就是显示某种视图,因此一般说的 Window 没有说明都指的是广义的 Window。
二.Window 的管理
Window 的管理涉及到应用进程和系统服务进程之间的通信,这里分为两个方面进行说明。
(一).在应用进程涉及的几个类
1.ViewManager
这是一个接口,里面只有三个方法,定义了对 View ( 也就是所说的视图 )的基本操作,添加,刷新和移除
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
2.WindowManager
这也是一个接口,继承 ViewManager,负责对 Window 进行管理,和操作。每一个 Window 都持有这个对象,用于对自己窗口内部的 View 进行操作。这里还有一个静态的内部类 LayoutParams, 这个类定义 Window 的一些属性,窗口的类别和显示顺序。属性指的是窗口大小,位置,状态等信息,而窗口的类别就是前言中说的三种,这是通过设置 type 值显示的,每个类别有一个范围的 type 值,而显示顺序也是根据 type 值来确定的, type 值越高就越前,也就是越靠近用户。
- 应用程序窗口的 type :1 ~ 99
- 子窗口的 type : 1000 ~ 1999
- 系统窗口 type : 2000 ~ 2999
@SystemService(Context.WINDOW_SERVICE)
public interface WindowManager extends ViewManager {
...
public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
...
// 应用程序窗口
public static final int FIRST_APPLICATION_WINDOW = 1;
public static final int TYPE_BASE_APPLICATION = 1;
public static final int TYPE_APPLICATION = 2;
public static final int TYPE_APPLICATION_STARTING = 3;
public static final int TYPE_DRAWN_APPLICATION = 4;
public static final int LAST_APPLICATION_WINDOW = 99;
//子窗口
public static final int FIRST_SUB_WINDOW = 1000;
public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;
public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1;
public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW + 2;
public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW + 3;
public static final int TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW + 4;
public static final int TYPE_APPLICATION_ABOVE_SUB_PANEL = FIRST_SUB_WINDOW + 5;
public static final int LAST_SUB_WINDOW = 1999;
//系统窗口
public static final int FIRST_SYSTEM_WINDOW = 2000;
public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;
public static final int TYPE_SEARCH_BAR = FIRST_SYSTEM_WINDOW+1;
public static final int TYPE_PHONE = FIRST_SYSTEM_WINDOW+2;
...
public static final int LAST_SYSTEM_WINDOW = 2999;
}
}
3.WindowManagerImpl
WindowManager 的实现类,持有 Window 类型的变量,因而对 Window 的操作会在这里调用,但是对 View 的三个操作并没有在这里实现,而是由成员变量 WindowManagerGlobal 类型的 mGlobal 去执行。
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
...
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
...
mGlobal.updateViewLayout(view, params);
}
@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}
4.WindowManagerGlobal
这个类虽然没有实现 ViewManager ,但是它在这里同样定义了这三种操作,同时它还有几个重要的变量。而对于具体的 View 的操作,这里也没有实现而是交由 ViewRootImpl 去实现。
public final class WindowManagerGlobal {
...
private final ArrayList<View> mViews = new ArrayList<View>(); 所有 Window 对应的 View
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>(); //所有 Window 对应的 ViewRootImpl 。
private final ArrayList<WindowManager.LayoutParams> mParams =
new ArrayList<WindowManager.LayoutParams>(); 所有 Window 的属性参数
private final ArraySet<View> mDyingViews = new ArraySet<View>(); //已经执行 remove 方法但是还未真正完成删除的 View 。
...
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
}
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
...
}
public void removeView(View view, boolean immediate) {
...
}
}
5.ViewRootImpl
ViewRootImpl 是连接 Window 和 View 关系的桥梁。有三个方面的作用:
- 一是管理 View 树,并完成 View 的绘制三大流程
- 二是管理 Surface 并对输入事件的的转发。
- 三是与 WMS ( WindowManagerServer )进行进程间的通信。
它有两个重要的变量:
- IWindowSession mWindowSession : Binder 对象,服进程在 应用程序进程中的代理对象,用于调用服务进程中的方法。在 WMS 中的是是实现为 Session 。
- W mWindow : Binder 对象,用于服务进程的调用应用进程的方法。主要是 WMS 添加Window 成功后在 Window 进行显示或者其他操作。
6.DecorView ,mNextView,mDecor 等
DecorView 对于大家可能比较熟悉,这是 Activity 视图中的“根视图”,而其他两个则是 Toast 和 Dialog 的根视图。这看起来好像每种窗口类型都有自己的“根视图”,事实上的确是这样的,我们都知道 Window 实际上就是为了显示各种各样的视图,因此一个 Window 中就可能会有多个 View, 而使用“根视图”则提供了结构上方便,所有的 View 都是以“根视图”来进行添加删除等的,因此便于 WindowManager 对窗口进行管理。在 Activity 中 DecorView 是PhoneWindow 的一个变量,但是并不是所有的窗口都会使用 PhoneWindow 和 抽象接口 Window。
整体的体系如图:
点击查看大图
Window机制.png(二).创建 Window ( 在应用进程 )
在了解了上面几个概念后,下面就以实际的例子进行说明,虽然不同类型的窗口在创建 Window 的时候整体机制是一样的,即通过 WindowManager 进行管理,但是在一些具体的方面还有有差别的。
1.Activity 的窗口创建和 setContentView
在 Activity 启动过程中的最后一步 Activity 实例创建完后会需要调用 attach 方法关联上下文,在这里会创建 Window 对象。
final void attach(...){
...
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
...
}
在创建完 Window 后就需要往这个 Window 添加一个最低层的 View 用于管理以后的所有的 View,没错这个 View 就是 DecorView。DecorView 实际上是一个 FrameLayout ,它里面有一个 LinearLayout , LinearLayout 有两个子元素,一个是 </ViewStub> 的 actionbar , 一个就是 FrameLayout 的 content 。setContentView 就是在这个 FrameLayout 里添加我们的 LayoutView.在这之前需要先创建 DecorView 。
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
首先这里 mContentParent 就是 FrameLayout 的 content 。因为是第一次创建为 null, 就要初始化 DecorView
private void installDecor() {
...
if (mDecor == null) {
mDecor = generateDecor(-1);
...
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
}
...
}
protected ViewGroup generateLayout(DecorView decor) {
...
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
...
return contentParent;
}
在初始化DecorView 后就会加载 Content View ,并添加到 DecorView .这里的 ID_ANDROID_CONTENT 实际上就是 com.android.internal.R.id.content,在 layout 文件中可以看到
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
在上述步骤中只是创建了 Window 实例, 只是将 View 添加到 DecorView ,但是真正的 Window 的添加还未实现,而具体的实现在之前说过是由 WindowManager 进行操作的,这一步在 hanldeResumeActivity 中。
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
在 WindowManager 的 addView 方法中就会见到之前说的几个对象的层层调用,一直到 ViewRootImpl, 这个过程如图所示:
addView.png在 ViewRootImpl 会先调用 requestLayout 对View 进行绘制,然后调用 addToDisplay 添加到 Window 中。这两个方法都涉及与 WMS 的通信,具体则是由 WindowSession 实现,下面就介绍 Window 创建过程中和 WMS 的通信的部分。
(三).在 SystemServer 进程涉及的几个类
1.WindowManagerServer
WindowManagerServer 是 Android 系统中负责处理 Window 和 View 相关的 系统服务,主要有如下几个职责:
- 对所有的 Window 进行添加,删除等管理。这部分主要涉及 DisplayContent,WindowToken 和 WindowState 这几个对象。
- 对View 进行绘制,这里需要为每个窗口分配 Surface
一个 Surface 就是一个对象,该对象持有一群像素(pixels),这些像素是要被组合到一起显示到屏幕上的。每一个 window 都有唯一一个自己的 surface,window 将自己的内容绘制到该 surface 中。 Surface Flinger 根据各个 surface 在 Z 轴上的顺序 (Z-order) 将它们渲染到最终的显示屏上,z 轴就是以屏幕为平面,由里到外的一个轴向,而屏幕就是 x 和 y 轴组成的平面。
- 对从输入系统 InputManagerService 传过来的触摸事件寻找合适的窗口进行处理。
- 对 Window 的动画效果的管理,这主要是由 WindowAnimator 对象进行管理。
2.WindowState
用于保存窗口的信息,窗口的信息是可以随时改变的,比如窗口的位置,大小等变化,通常窗口信息的改变,就会进行相应的 View 的改变。WindowState 是保持在一个 Map 里面,这个 Map 保存着系统所有的窗口。
3.AppWindowToken/WindowToken
AppWindowToken 是 WindowToken 的 子类,可以译为令牌,一个 Activity 对应一个 WindowToken, 当应用进程向 WMS 发出创建 Window 的申请的时候需要出示正确的令牌。这里需要注意子窗口通常需要依赖于父窗口才能添加,比如在 Activity 上显示 Dialog 或者 Menu ,因此子窗口通常使用的是父窗口的 WindowToken ,这就是为什么创建 Dialog 的时候关联的上下文不能使用 ApplicationContext ,因为 application 没有 WindowToken.
(四).创建 Window ( 在 Server 进程 )
在了解了服务端几个类后,下面就是 Window 的创建在 Server 端的过程,首先回到之前的在 ViewRootImpl 的两个方法,requestLayout 方法调用后在 SystemServer 会进行一个 Surface 的创建和绘制,之后就会 view 的绘制。这里属于 Surface 的创建过程,这里就不进行展开,在绘制完成后就会执行 addWindow 方法.
addWindow 的源码很长,主要是做一下几个部分:
1.对窗口的参数进行检查,比如窗口的类型,窗口的 WindowToken.输入法这种没有 WindowToken 的系统自己会创建一个 windowToken。
2.创建 WindowState 对象,将 WindowToken 和 WindowState 关联起来并添加到 HashMap。
3.将窗口按照 z 轴的位置添加到 DisPlayContent 的合适的位置。
4.最后就是窗口的位置,动画等信息保存下来。这样一个窗口创建成功。
(五).Dialog 的 Window 创建
Dialog 是一个子窗口,需要依赖于父窗口才能显示。
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
...
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
mWindow = w;
...
w.setWindowManager(mWindowManager, null, null);
}
@Override
public Object getSystemService(@ServiceName @NonNull String name) {
if (WINDOW_SERVICE.equals(name)) {
return mWindowManager;
}
return super.getSystemService(name);
}
Dialog 会创建自己的 Window 对象,然后就会在 Activity 的 getSystemService 方法中可以拿到 Activity 的 mWindowManager 对象,这样 Dialog 就会和 Activity 有一样的 Token 。接着看 show 方法。
public void show() {
...
mDecor = mWindow.getDecorView();
...
mWindowManager.addView(mDecor, l);
}
之前说过所有的 Window 都有一个“根视图”用于对 所有的View 进行管理,在 Dialog 中就对应 mDecor,可以看到同样的,mWindowManager 会调用 addView 执行窗口的添加工作。 后面的过程基本和上述的 setContentView 的过程相似。
(六).Toast 的 Window 创建
Toast 是一个系统级的窗口,因此在创建的过程中就有些不同。这里涉及到了几个新的对象
(1)涉及的几个对象
1.NotificationManagerService ( NMS )
这是一个系统服务,用于管理系统通知,因此也负责 Toast 的管理,其中有几个重要的成员
- ArrayList<ToastRecord> mToastQueue ,这是保存系统中所有 Toast 的队列,每个非系统应用的 Toast 在这个队列的数量不能超过 50 个,这样做是避免网络攻击。
拒绝服务攻击 简称:DoS,也叫洪水攻击,是一种网络攻击手法,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。
- WorkerHandler 用于处理 Toast 的显示时间,即在 Toast 显示过后发送一个 延迟的消息进行取消。
1. TN
这是 Toast 的内部类,继承自 ITransientNotification.Stub ,因此可以跨进程通信,用于处理 Toast 窗口的创建和 View 的添加。
(2)Toast 的 Window 创建过程
首先看 makeText 方法
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
Toast result = new Toast(context, looper);
LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);
result.mNextView = v;
result.mDuration = duration;
return result;
}
这里的 mNextView 就是 Toast "根视图",可以看到这个 mNextView 有一个 TextView 用于显示 Toast 的文本信息。
接着看 show 方法
public void show() {
...
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
在 Toast 方法中会调用 NMS 的 enqueueToast ,这就到 NMS 服务中。在 NMS 服务中会将 Toast 封装为 ToastRecord ,并添加到 mToastQueue 队列中。一直到 从队列中取出进行显示的时候就会调用 TN 的 show 方法,并发送一个延迟的消息。
@GuardedBy("mToastQueue")
private void scheduleTimeoutLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
mHandler.sendMessageDelayed(m, delay);
}
- SHORT_DELAY = 2 s
- LONG_DELAY = 3.5 s
接着先看 TN 的 show 方法
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
...
public void handleShow(IBinder windowToken) {
...
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
...
mWM.addView(mView, mParams);
...
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
依次从三个方法看下来就可以知道 TN 的 show 最后会调用 WindowManager 的 addView 方法,这里可以看到 为了调用 handleShow 方法这里使用了 Handler 。这是因为 NMS 调用 TN 的方法是一种跨跨进程的方式,show 方法就运行在 Binder 线程池中,因此需要使用 Handler 回到 Toast 的线程去调用 addView 方法。
现在回到 NMS 中,之前发送了个延迟的消息,然后过了相应的时间后就会对这个消息进行处理,也就是将 Toast 窗口删除。
@Override
public void handleMessage(Message msg)
{
switch (msg.what)
{
case MESSAGE_TIMEOUT:
handleTimeout((ToastRecord)msg.obj); //跳到下面
...
}
}
private void handleTimeout(ToastRecord record)
{
if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) {
cancelToastLocked(index);//跳到下面
}
}
}
@GuardedBy("mToastQueue")
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
record.callback.hide(); // 执行 hide
} catch (RemoteException e) {
...
}
ToastRecord lastToast = mToastQueue.remove(index);//移除队列
mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);//删除对应 窗口
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) {
...
showNextToastLocked(); // 如果队列中还有就继续执行
}
}
在延迟的消息到达后就会执行 hide 方法,这个过程和添加实际上是相似的,这里就不再说明。接着将 Toast 从队列中删除,并进行窗口的删除,接着判断队列中是否还有 Toast ,如果就继续执行。具体的过程可看下图:
Toast show.png最后再补充一点,前面提到过 为了使 TN 的 show 方法切换到 对应的线程而使用了 Hanler ,而其中 Handler 的 Looper 是这样的
if (looper == null) {
// Use Looper.myLooper() if looper is not specified.
looper = Looper.myLooper();
if (looper == null) {
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}