Android进阶知识(二十一):Window的创建过程

  View是Android中的视图呈现方式,但是View不能单独存在,它必须依附于Window,因此有视图的地方就有Window。这一篇章笔者将分析Activity、Dialog以及Toast的Window的创建过程,加深对Window的理解。

一、Activity的Window创建过程

  Activity中的Window创建过程涉及到Activity的启动过程(后续介绍),Activity的启动过程很复杂,最终会由ActivityThread中的performLaunchActivity()来完成整个启动过程。该方法内部通过类加载器创建Activity的实例对象。

android 设置window宽高 android on windows_初始化


  各个方法的作用如下表所示。

方法

描述

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

android 设置window宽高 android on windows_初始化_02


  那么知道了Window的创建,Activity的视图是如何附属到Window上的呢?

  Activity的视图由setContentView方法提供,我们看看这个方法的源码。

public void setContentView(int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

  从中可以看到,Activity将具体实现交给了Window处理,而Window的具体实现是PhoneWindow,因此我们只需要看PhoneWindow的相关逻辑。PhoneWindow的setContentView方法大致遵循如下步骤。

  1. 如果没有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);
  1. 将View添加到DecorView的mContentParent中

  这个过程比较简单,在步骤1中已经创建并初始化了DecorView,因此只需要直接将Activity的视图添加到DecorView的mContentParent中即可:mLayoutInflater.inflate(layoutResID, mContentParent)。

  1. 回调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创建过程就分析完毕了。

android 设置window宽高 android on windows_布局文件_03

二、Dialog的Window创建过程

  Dialog的Window创建过程和Activity类似,主要为如下步骤。

  1. 创建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);
}
  1. 初始化DecorView并将Dialog的视图添加到DecorView中

  这个过程与Activity类似,都是通过Window去添加指定的布局文件。

public void setContentView(int layoutResID) {
    mWindow.setContentView(layoutResID);
}
  1. 将DecorView添加到Window中并显示

  在Dialog的show方法中,会通过WindowManager将DecorView添加到Window中。当Dialog被关闭时,它会通过WindowManager来移除DecorView。

mWindowManager.addView(mDecor, 1);
mShowing = true;

  这也是为什么创建Dialog后,与Dialog的交互需要在show方法调用之后才不会报错,原因在于只有show方法调用之后,DecorView才真正添加到Window中,才能与外界交互

android 设置window宽高 android on windows_初始化_04


  普通的Dialog有一个特殊之处,必须采用Activity的Context,如果采用Application的Context就会报错,原因在于Activity的Context中才有应用token。

  系统Window比较特殊的是其不需要token,所以解决上述问题的方法就是指定Dialog的Window为系统Window。

android 设置window宽高 android on windows_初始化_05

三、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的显示过程流程如下。

android 设置window宽高 android on windows_android 设置window宽高_06


  关于流程里面的几个点:

  1. 当NMS处理Toast显示或隐藏请求时,跨进程回到TN方法中

  由于TN运行在Binder线程池中,所以需要通过Handler将其切换到当前线程(即发送Toast亲求所在的线程)中

android 设置window宽高 android on windows_初始化_07

  1. enqueueToast将Toast请求封装为ToastRecord,添加到mToastQueue队列

  mToastQueue是一个ArrayList,对于非系统应用来说,mToastQueue最多同时存在50个ToastRecord,以防止DOS(Denial of Service)。

android 设置window宽高 android on windows_布局文件_08

  1. 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。

  1. TN的show和hide内部使用Handler切换线程

  由于这两个方法是被NMS以跨进程的方式调用,因此它们运行在Binder线程池中,为了把执行环境切换到Toast请求所在线程,在其内部使用了Handler

android 设置window宽高 android on windows_初始化_09

参考资料:《Android开发艺术探索》