Android进阶知识(二十一):Window的创建过程
View是Android中的视图呈现方式,但是View不能单独存在,它必须依附于Window,因此有视图的地方就有Window。这一篇章笔者将分析Activity、Dialog以及Toast的Window的创建过程,加深对Window的理解。
一、Activity的Window创建过程
Activity中的Window创建过程涉及到Activity的启动过程(后续介绍),Activity的启动过程很复杂,最终会由ActivityThread中的performLaunchActivity()来完成整个启动过程。该方法内部通过类加载器创建Activity的实例对象。
各个方法的作用如下表所示。
方法 | 描述 |
performLaunchActivity() | 完成Activity启动过程,通过类加载器创建Activity实例对象,调用attach方法 |
attach() | Activity的方法,关联运行过程中所依赖的一系列上下文环境变量,内部调用makNewWindow()和setCallback() |
makeNewWindow() | PolicyManager的方法,创建Window对象(PhoneWindow实例对象) |
setCallback() | 设置Window对象的回调接口(此处Activity实现了该接口),当Window接收到外界的状态改变时就会回调Acitivity的方法,常见的接口有onAttachedToWindow、onDetachedFromWindow、dispatchTouchEvent等等。 |
在Activity的attach方法中,系统会创建Activity所属的Window对象并设置其回调接口。Window对象的创建是通过PolicyManager的makeNewWindow()实现的,而PolicyManager是一个策略类,真正实现是Policy类,从中可以发现Window的具体实现的确是PhoneWindow。
那么知道了Window的创建,Activity的视图是如何附属到Window上的呢?
Activity的视图由setContentView方法提供,我们看看这个方法的源码。
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
从中可以看到,Activity将具体实现交给了Window处理,而Window的具体实现是PhoneWindow,因此我们只需要看PhoneWindow的相关逻辑。PhoneWindow的setContentView方法大致遵循如下步骤。
- 如果没有DecorView,那么创建它
DecorView的创建过程由installDecor方法完成,其内部通过generateDecor方法直接创建DecorView。
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
为了初始化DecorView的结构,PhoneWindow还需要通过generateLayout方法加载具体的布局文件到DecorView中,具体布局文件与系统版本以及主题有关。
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
- 将View添加到DecorView的mContentParent中
这个过程比较简单,在步骤1中已经创建并初始化了DecorView,因此只需要直接将Activity的视图添加到DecorView的mContentParent中即可:mLayoutInflater.inflate(layoutResID, mContentParent)。
- 回调Activity的onContentChanged方法通知Activity视图已经发生改变
由于Activity实现了Window的Callback接口,此时Activity的布局文件已经被添加到DecorView的mContentParent中,需要通知Activity,使其可以做相应处理。具体实现需要在子Activity中实现。
至此,DecorView创建并初始化了,Activity的布局文件也添加到DecorView中了,但是DecorView尚未被WindowManager正式添加到Window中。
由于DecorView没有被WindowManager识别,所以这个时候的Window无法提供具体功能,无法接收外界的输入信息。在ActivityThread的handleResumeActivity方法中,首先调用Activity的onResum方法,接着调用Activity的makeVisible()完成DecorView的添加和显示。
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
从上面的描述也可以明白,Android基础知识(五):Activity的生命周期说的只有在onResume方法回调之后,Activity才是前台可见、才可与用户交互的原因。
至此,Activity的Window创建过程就分析完毕了。
二、Dialog的Window创建过程
Dialog的Window创建过程和Activity类似,主要为如下步骤。
- 创建Window
Dialog中Window的创建同样是通过PolicyManager的makeNewWindow方法来完成。
Dialog(Context context, int theme, boolean createContextThemeWrapper) {
...
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Window w = PolicyManager.makeNewWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setWindowManager(mWindowManager, null, null);
mListenersHandler = new ListenersHandler(this);
}
- 初始化DecorView并将Dialog的视图添加到DecorView中
这个过程与Activity类似,都是通过Window去添加指定的布局文件。
public void setContentView(int layoutResID) {
mWindow.setContentView(layoutResID);
}
- 将DecorView添加到Window中并显示
在Dialog的show方法中,会通过WindowManager将DecorView添加到Window中。当Dialog被关闭时,它会通过WindowManager来移除DecorView。
mWindowManager.addView(mDecor, 1);
mShowing = true;
这也是为什么创建Dialog后,与Dialog的交互需要在show方法调用之后才不会报错,原因在于只有show方法调用之后,DecorView才真正添加到Window中,才能与外界交互。
普通的Dialog有一个特殊之处,必须采用Activity的Context,如果采用Application的Context就会报错,原因在于Activity的Context中才有应用token。
系统Window比较特殊的是其不需要token,所以解决上述问题的方法就是指定Dialog的Window为系统Window。
三、Toast的Window创建过程
Toast和Dialog不同,其工作过程稍微复杂。首先Toast也是基于Window来实现的,但是由于Toast具有定时取消的功能,所以系统采用了Handler。在Toast内部有两类IPC过程,第一类是Toast访问NotificationManagerService,第二类是NotificationManagerService回调Toast里的TN接口。
Toast属于系统Window,其内部的视图由两种方式指定,一种是系统默认样式,另一种通过setView指定自定义View。Toast提供了show和cancel分别用于显示和隐藏Toast,其内部是一个IPC过程(通过NMS实现)。
Toast的显示过程流程如下。
关于流程里面的几个点:
- 当NMS处理Toast显示或隐藏请求时,跨进程回到TN方法中
由于TN运行在Binder线程池中,所以需要通过Handler将其切换到当前线程(即发送Toast亲求所在的线程)中。
- enqueueToast将Toast请求封装为ToastRecord,添加到mToastQueue队列
mToastQueue是一个ArrayList,对于非系统应用来说,mToastQueue最多同时存在50个ToastRecord,以防止DOS(Denial of Service)。
- Toast显示后,NMS通过scheduleTimeoutLocked发送延时消息控制Toast时长
scheduleTimeoutLocked方法代码如下所示。
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);
}
从上面的代码可以看到,Toast的延迟只有LENGTH_DELAY(3.5s)以及SHORT_DELAY(2s)两个,因此自定义Toast显示时间只能通过自定义计时器实现。
在延迟时间之后,NMS通过cancelToastLocked方法隐藏Toast并从mToastQueue中移除,NMS继续显示mToastQueue中其他的Toast。
- TN的show和hide内部使用Handler切换线程
由于这两个方法是被NMS以跨进程的方式调用,因此它们运行在Binder线程池中,为了把执行环境切换到Toast请求所在线程,在其内部使用了Handler。
参考资料:《Android开发艺术探索》