WindowManager源码分析-从悬浮窗说起

在android开发中,有时候我们需要一个悬浮在所有activity之上的悬浮窗口来展示某些重要信息,例如360安全卫士的加速球就是这样一个悬浮窗口。那么悬浮窗口是怎么实现的呢?这里就轮到我们今天的主角WindowManager出场了。

悬浮窗口实例

我们使用一个例子来说明如何创建一个悬浮窗,该悬浮窗可以在屏幕上自由拖动,并且点击中心区域后悬浮窗自动消失。想亲自尝试的同学,将这些代码复制到自己的工程中,启动该service就可以了。

我们在一个Service中使用了WindowManager创建了ui,没有使用我们平时常用的Activity,Fragment等。在该服务中我们演示了WindowManager的3个最重要的方法,addView,removeView和updateViewLayout,分别用来添加窗口,移除窗口,更新窗口布局。

    public class FloatService extends Service{
       //悬浮窗口布局
       private View vMain;
       //点击停止按钮
       private View btnStop;
       //我们的主角,WindowManager
       private WindowManager wm;

       @Nullable
       @Override
       public IBinder onBind(Intent intent) {
           return null;
       }

       @Override
       public void onCreate() {
           //1,获取WindowManager服务
           wm = (WindowManager) getSystemService(WINDOW_SERVICE);
           //创建悬浮窗口的布局
           createView();
           super.onCreate();
       }

       @Override
       public int onStartCommand(Intent intent, int flags, int startId) {
           //显示悬浮窗口
           showFloatWindow();
           return super.onStartCommand(intent, flags, startId);
       }

       private void showFloatWindow(){
           if(vMain.getParent()==null) {
                //创建布局参数对象,宽高的布局参数均为wrap_content
                WindowManager.LayoutParams param = new WindowManager.LayoutParams(
                       WindowManager.LayoutParams.WRAP_CONTENT
                       ,WindowManager.LayoutParams.WRAP_CONTENT);
               //设置窗口的类型,这里设置为了Toast类型,该类型的好处是不用额外的权限声明,坏处是无法获取焦点
               //所以有edittext这样的控件,是不适合使用这种类型的
               //个人建议不需要获取焦点的悬浮窗都使用Toast类型。
               //还可以设置TYPE_SYSTEM_ERROR类型,该类型的好处是显示优先级非常高,可以显示在锁屏等界面的上面
               //缺点是需要申请SYSTEM_ALERT_WINDOW权限。该权限在android6.0之后需要动态在代码中申请,比较麻烦
               //还有TPYE_SYSTEM_ALERT,TYPE_PHONE等类型,和TYPE_SYSTEM_ERROR一样需要SYSTEM_ALERT_WINDOW权限,
               //不过显示优先级没有那么高,显示在状态栏和锁屏界面之下。
               param.type = WindowManager.LayoutParams.TYPE_TOAST;
               //设置对齐方式,这里我们设置在中间,相当于以屏幕中心为原点
               param.gravity = Gravity.CENTER;
               param.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE ;
               2,addView方法,将窗口添加到WindowManager中
               wm.addView(vMain, param);
           }
       }

       private void createView(){
           //加载悬浮窗的布局
           vMain = LayoutInflater.from(this).inflate(R.layout.view_float,null,false);
           //为停止按钮设置点击监听器
           btnStop = vMain.findViewById(R.id.btnStop);
           btnStop.setOnClickListener(new View.OnClickListener() {
               @Override
               public void onClick(View view) {
                   //3,removeView方法,将窗口从WM中移除
                   if(vMain!=null && vMain.getParent()!=null) {
                       wm.removeView(vMain);
                   }
               }
           });
           //为主布局添加触摸监听器
           vMain.setOnTouchListener(new View.OnTouchListener() {
               @Override
               public boolean onTouch(View view, MotionEvent motionEvent) {
                   switch(motionEvent.getAction()){
                       case MotionEvent.ACTION_MOVE:
                           //获取窗口的布局
                           WindowManager.LayoutParams params = (WindowManager.LayoutParams) vMain.getLayoutParams();
                           //将x,y坐标设置为当前触摸点坐标
                           params.x = (int) motionEvent.getRawX()-vMain.getWidth()/2;
                           params.y = (int) motionEvent.getRawY()-vMain.getHeight()/2;
                           //设置对齐方式为左上
                           params.gravity = Gravity.LEFT | Gravity.TOP;
                           //4,updateViewLayout方法更新布局
                           wm.updateViewLayout(vMain,params);

                   }
                   return true;
               }
           });
       }
    }

布局文件如下,为一个FrameLayout加上TextView,Layout区域可以拖动,TextView区域点击悬浮窗消失。

"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="32dp"
    android:background="#FF4081"
    >

    "@+id/btnStop"
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:text= "点我消失"
        android:background="#ffffff"
        />

context的getSystemService方法

从上面的示例我们可以看到,要使用WindowManager,我们首先要使用Context获取它的服务,那我们就来分析一下。

Context.getSystemService(WINDOW_SERVICE);

这个语句获取到的究竟是什么,查看代码,我们发现Context的getSystemService方法是一个抽象方法。

public abstract Object getSystemService(@ServiceName @NonNull String name);

那么我们只有去Context类的实际实现类ContextImpl中查看究竟了。

contextImpl的getSystemService方法

@Override
public Object getSystemService(String name) {
    return SystemServiceRegistry.getSystemService(this, name);
}

可以看到该类调用SystemServiceRegistry类的getSystemService方法来获取结果。

SystemServiceRegistry类的getSystemService方法

该方法从全局的HashMap结构SYSTEMSERVICERETCHERS中取出一个ServiceFetcher对象,然后调用ServiceFetcher类的getService方法获取实际的服务。

//静态全局HashMap,存储了所有的ServiceFetcher对象。
private static final HashMap> SYSTEM_SERVICE_FETCHERS =
        new HashMap>();

public static Object getSystemService(ContextImpl ctx, String name) {
    //获取对应名称的ServiceFetcher对象
    ServiceFetcher fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
    //调用该对象的getService方法来实际获取服务
    return fetcher != null ? fetcher.getService(ctx) : null;
}

ServiceFetcher内部静态接口

之前我们看到服务实际通过ServiceFetcher类来获取,那么它是何方圣神呢?可以看到,原来它仅仅是一个接口。

static abstract interface ServiceFetcher {
        T getService(ContextImpl ctx);
    }

SystemServiceRegistry类的registerService方法

从ServiceFetcher接口的定义中,我们看不出来getSystemService方法究竟是怎么工作的,那我们换个思路,还记得我们的getSystemService方法中使用到的静态全局变量SYSTEMSERVICEFETCHERS么,我们从这个变量中根据名称取出了ServiceFetcher接口,那么是谁将ServiceFetcher接口放到它里面的呢?我们搜索SYSTEMSERVICEFETCHERS变量,发现了registerService方法将名称和ServiceFetcher接口一一对应了起来。

//建立实际服务的class对象和服务名称的对应关系
private static final HashMap, String> SYSTEM_SERVICE_NAMES =
        new HashMap, String>();
//建立名称和类的class与真正服务的对应关系。
private static  void registerService(String serviceName, Class serviceClass,
        ServiceFetcher serviceFetcher) {
    SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
    SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
}

SystemServiceRegistry类的静态初始化代码块

我们来查找是谁调用registerService方法注册了服务的信息,在静态初始化代码块中,我们找到了注册WINDOW_SERVICE的语句,其他常用服务,如ActivityManager,LayoutInflater等等,都是在这里静态注册的。

static{
    registerService(Context.WINDOW_SERVICE, WindowManager.class,
            new CachedServiceFetcher() {
        @Override
        public WindowManager createService(ContextImpl ctx) {
            return new WindowManagerImpl(ctx);
        }});
}

从这段代码中,我们可以看到,我们实际获得的所谓WINDOW_SERVICE服务,原来是一个WindowManagerImpl类的对象,不过那个CachedServiceFetcher类是什么鬼,我们来看看。

CachedServiceFetcher内部类

我们可以看到,该类的主要逻辑就是为系统service的实际对象提供一个缓存机制,由于service实际对象的创建比较消耗系统资源,而且它们在一个进程中也只需要存在一个,所以我们使用CachedServiceFetcher类来达到缓存系统服务,节约资源的目的。

//这个静态变量保存了缓存的服务的数量
private static int sServiceCacheSize;

static abstract class CachedServiceFetcher implements ServiceFetcher {
    //该局部变量保存的是当前ServiceFetcher产生的服务在缓存列表中的位置
    private final int mCacheIndex;

    public CachedServiceFetcher() {
        //在构造方法中,我们将要缓存的服务数量加1,
        //并记录下我们在缓存中的位置
        mCacheIndex = sServiceCacheSize++;
    }

    @Override
    @SuppressWarnings("unchecked")
    public final T getService(ContextImpl ctx) {
        //从ContextImpl类中获取我们的真实服务缓存
        //该缓存就是一个size为sServiceCacheSize的静态objcet数组
        final Object[] cache = ctx.mServiceCache;
        synchronized (cache) {
            //根据index获取服务
            Object service = cache[mCacheIndex];
            //如果该服务为空,我们创建该服务,并将它加入index位置,也就是将服务缓存下来
            if (service == null) {
                service = createService(ctx);
                cache[mCacheIndex] = service;
            }
            return (T)service;
        }
    }
    //抽象方法,创建服务
    public abstract T createService(ContextImpl ctx);
}

WindowManagerImpl类

从上面的分析中,我们知道,我们最终获得的是一个WindowManagerImpl类的对象,那么我们来看看该对象的构成。

public final class WindowManagerImpl implements WindowManager {
    //使用一个进程唯一的WindowManagerGlobal单例来代理执行请求
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    //保存context对象
    private final Context mContext;
    //保存父Window
    private final Window mParentWindow;

    //我们使用getSystemService方法获取的服务,最终调用的就是这个构造方法
    public WindowManagerImpl(Context context) {
        this(context, null);
    }

    private WindowManagerImpl(Context context, Window parentWindow) {
        mContext = context;
        mParentWindow = parentWindow;
    }


    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        //addView方法,没有实际内容,委托WindowManagerGlobal来实际执行操作。
        //其他removeView,updateViewLayout方法也同样如此。
        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);
    }

}

可以看到该类继承自WindowManager接口,并实现了该接口的所有方法,不过该类自身没有任何功能性代码,而是将所有的功能都委托给进程唯一的WindowManagerGlobal类进行处理。

WindowManager接口

WindowManager接口继承自ViewManager接口,ViewManager接口中定义了我们之前说的addView,removeView,updateViewLayout三个重要方法。ViewManager还有一个实现者那就是ViewGrorp,由此我们猜测,android的设计者觉得ViewGroup和WindowManager还是比较相似的,都具备添加View,移除View和修改view布局的功能。只不过一个是操作父ViewGroup,一个是操作窗口管理者。

public interface ViewManager{
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

我们在看WindowManager接口,该接口除了新增几个方法外,最重要的功能是定义了一个LayoutParams的内部类,该类继承自ViewGroup.LayoutParms,用来表示Window的布局参数。

下面我们重点说说type参数,该参数决定了窗口的类型。在android系统中,一共有3种类型的窗口。

  • 应用程序窗口,我们平时使用的Activity就是该窗口类型,type的取值范围是0-99。
  • 子窗口,显示时需要依附应用程序窗口,例如AlertDialog,popWindow都是这种类型。它的取值范围是1000-1999,它们必须依赖主窗口而存在,可以显示在应用程序窗口前面或后面。
  • 系统窗口,代表为系统状态栏,导航条,输入法,Toast,以及系统对话框等,type的取值范围是2000-2999,显示在应用程序窗口之上。

在系统的窗口管理服务WindowManagerService中,对于每个窗口都有一个层级,我们把它叫做Z或者layer,窗口的这个值越大,表示它越显示在屏幕的前面。也就是说Z值大的窗口会遮挡住Z值在后面的窗口。一般来说 系统窗口Z值>应用窗口的Z值。而子窗口的Z值可以大于或者小于它依附的应用程序窗口。因为Toast是一个系统窗口,所以我们使用Toast时,它会显示在我们自己的Activity之上。

我们在来看看flags参数,该参数决定了窗口的一些重要的行为,通过它我们可以控制窗口全屏显示,显示在锁屏界面上,保存屏幕开启,解锁屏幕等效果。下面的代码中,我们注释了一些常用的效果,还有其他更多效果没有列出,当我们想要控制窗口的行为时,可以到该类中查看,有许多有用的行为。这些flag之间可以通过 | 让几个效果共同起作用。

最后还有一个softInputMode标志,用来控制该窗口中系统软键盘的行为,具体含义可以直接看代码中的注释,我们一般混合使用STATE和ADJUST,来同时指定软键盘的弹出行为和窗口对软键盘弹出的调整。

public interface WindowManager extends ViewManager {

    public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
        //窗口的类型
        public int type;
        //下面是应用程序窗口,范围是1-99
        //第一个应用程序窗口
        public static final int FIRST_APPLICATION_WINDOW = 1;
        //应用程序基础窗口
        public static final int TYPE_BASE_APPLICATION   = 1;
        //应用程序窗口,我们自己的App的Activity的窗口类型通常都是这个
        public static final int TYPE_APPLICATION        = 2;
        //启动窗口,在一个应用程序的窗口真正启动之前,通常会显示这个窗口
        public static final int TYPE_APPLICATION_STARTING = 3;
        //最后一个应用程序窗口
        public static final int LAST_APPLICATION_WINDOW = 99;

        //下面是子窗口,范围是1000-1999,例如各类app的dialog等
        //第一个子窗口
        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 LAST_SUB_WINDOW = 1999;

        //下面是系统类型,范围是2000-2999
        //第一个系统窗口
        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;
        //系统来电页面窗口,需要显示在所有APP窗口之上
        public static final int TYPE_PHONE              = FIRST_SYSTEM_WINDOW+2;
        //系统警告对话框窗口,类似低电量关机等
        public static final int TYPE_SYSTEM_ALERT       = FIRST_SYSTEM_WINDOW+3;
        //系统锁屏窗口
        public static final int TYPE_KEYGUARD           = FIRST_SYSTEM_WINDOW+4;
        //系统Toast窗口,Toast的窗口类型就是它
        public static final int TYPE_TOAST              = FIRST_SYSTEM_WINDOW+5;
        //还是来电窗口,不过该类型可以显示在锁屏之上
        public static final int TYPE_PRIORITY_PHONE     = FIRST_SYSTEM_WINDOW+7;
        //系统提示框窗口,例如低电量关机等
        public static final int TYPE_SYSTEM_DIALOG      = FIRST_SYSTEM_WINDOW+8;
        //系统错误窗口,优先级非常高
        public static final int TYPE_SYSTEM_ERROR       = FIRST_SYSTEM_WINDOW+10;
        //系统软键盘输入法窗口
        public static final int TYPE_INPUT_METHOD       = FIRST_SYSTEM_WINDOW+11;
        //系统壁纸窗口
        public static final int TYPE_WALLPAPER          = FIRST_SYSTEM_WINDOW+13;
        //系统导航条窗口
        public static final int TYPE_NAVIGATION_BAR = FIRST_SYSTEM_WINDOW+19;
        //最后一个系统窗口编号
        public static final int LAST_SYSTEM_WINDOW      = 2999;


        //窗口标志
        public int flags;
        //所有在这个窗口之后的窗口都会变暗
        public static final int FLAG_DIM_BEHIND        = 0x00000002;
        //这个窗口没有焦点
        public static final int FLAG_NOT_FOCUSABLE      = 0x00000008;
        //这个窗口不能触摸
        public static final int FLAG_NOT_TOUCHABLE      = 0x00000010;
        //这个窗口将保持亮屏
        public static final int FLAG_KEEP_SCREEN_ON     = 0x00000080;
        //这个窗口将全屏显示
        public static final int FLAG_FULLSCREEN      = 0x00000400;
        //该窗口可以显示在锁屏界面上
        public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000;
        //使用壁纸作为当前窗口的背景
        public static final int FLAG_SHOW_WALLPAPER = 0x00100000;
        //该窗口显示时,将会点亮屏幕
        public static final int FLAG_TURN_SCREEN_ON = 0x00200000;
        //该窗口显示时将会自动解锁屏幕,如果有锁屏密码,则跳到密码解锁界面
        public static final int FLAG_DISMISS_KEYGUARD = 0x00400000;

        //系统输入法模式
        public int softInputMode;

        /*
        下面这些标志决定了软键盘是否弹出
        */
        //软键盘的状态并没有指定,系统将选择一个合适的状态或依赖于主题的设置
        public static final int SOFT_INPUT_STATE_UNSPECIFIED = 0;
        //这个窗口出现时,软键盘将一直保持在上一个窗口状态,无论是隐藏还是显示
        public static final int SOFT_INPUT_STATE_UNCHANGED = 1;
        //这个窗口出现时,软键盘将被隐藏(和下面的标志的区别是,如果该窗口有焦点,那么软键盘还是会弹出)
        public static final int SOFT_INPUT_STATE_HIDDEN = 2;
        //即使该窗口有焦点,软键盘也会隐藏  
        public static final int SOFT_INPUT_STATE_ALWAYS_HIDDEN = 3;
        //这个窗口出现时,软键盘将会弹出
        public static final int SOFT_INPUT_STATE_VISIBLE = 4;
        //这个窗口收到焦点时,软键盘总是弹出
        public static final int SOFT_INPUT_STATE_ALWAYS_VISIBLE = 5;

        /*
        下面这些标志决定了弹出的软键盘要怎么显示
        */
        //默认设置,软键盘由系统自行决定是隐藏还是显示
        public static final int SOFT_INPUT_ADJUST_UNSPECIFIED = 0x00;
        //该窗口总是调整自身的大小以便留出软键盘的空间
        public static final int SOFT_INPUT_ADJUST_RESIZE = 0x10;
        //该窗口的内容将自动移动以便当前焦点不被软键盘覆盖
        public static final int SOFT_INPUT_ADJUST_PAN = 0x20;
        //该窗口不对软键盘的弹出做任何调整
        public static final int SOFT_INPUT_ADJUST_NOTHING = 0x30;

    }
}

WindowManagerGlobal类

前面我们分析过了WindowManagerImpl类,该类内部使用了WindowManagerGlobal类作为它的方法的具体实现者,我们来看看WindowMangerGlobal类的getInstance方法。该方法是一个单例模式,也就是说一个进程中只存在一个WindowManagerGlobal对象。

//WindowManagerService(WMS)的binder代理对象,其服务端就是WMS。
 private static IWindowManager sWindowManagerService;
 //IWindowSession的binder代理对象,其服务端就是Session。
 private static IWindowSession sWindowSession;
 //静态单例对象
 private static WindowManagerGlobal sDefaultWindowManager;
 //典型的单例模式,由于可能在多线程中使用,需要同步
 public static WindowManagerGlobal getInstance() {
    synchronized (WindowManagerGlobal.class) {
        if (sDefaultWindowManager == null) {
            sDefaultWindowManager = new WindowManagerGlobal();
        }
        return sDefaultWindowManager;
    }
}

在看看WindowManagerGlobal类的私有构造方法,为什么构造方法要是私有的呢,当然是为了防止外界绕过getInstance方法,直接使用构造方法创建该类的实例了,这也是单例模式的一部分啦。该类的构造方法为空。

private WindowManagerGlobal() {
}

上面我们说过了android中窗口的概念,现在我们来说说android系统中的一个核心服务,WindowManagerService(WMS),它是系统中所有窗口的管理者,之前我们说过窗口层级的概念,WMS的作用就是就管理窗口层级,计算窗口大小并布局,管理和分配窗口的Surface。我们通过WindowManager添加,删除或者修改窗口布局的操作最终都将调用到WMS中的相关方法,才算完成了窗口相关的操作。IWindowManager就是WMS在客户端的binder代理对象。

那么IWindowSeesion是什么呢?我们固然可以直接获取WMS的binder客户端IWindowManager来使用WMS的服务。但是由于WMS是一个重要的系统服务,每一个窗口都需要和它打交道,android可能认为它的负担太重了,于是就使用了IWondowSession binder客户端来和一个匿名binder服务端Session进行通信,一般来说我们为每一个客户端进程创建了一个Seesion服务端进行通信。该服务端Session再将信息转给WMS。关于匿名binder,实名binder以及binder通信的信息,不熟悉的同学请查阅binder的相关介绍。

WindowManagerGlobal类的addView方法

在WindowManager类中,使用了View,LayoutParams,和ViewRootImpl三个全局的列表来存储该进程中所有添加到WindowManager中管理的View的信息,该类的执行过程如下。

  • 如果要添加的窗口还有父窗口,那么对它的布局参数进行调整,主要的目的是使用父窗口的Token作为子窗口布局参数的Token。
  • 查找要添加的View在mViews全局列表中的位置,如果该View已经存在,且没有在mDyingViews之中,则抛出异常。这里的实际含义是我们不能使用addView重复添加一个View到WindowManager中。
  • 创建ViewRootImpl对象,并将View,布局参数和ViewRootImpl加入全局列表中存储。
  • 调用ViewRootImpl类的setView方法继续进行下面的处理。

这里我们大概说一下Token的概念,我们使用WindowManager添加,删除窗口的操作时,实际上通过WindowManagerGlobal中转到ViewRootImpl,最后会调用WindowManagerService(WMS)中的相关方法,来完成对窗口的添加和删除等操作。

这里的Token对于WMS来说相当于一个令牌,WMS需要使用它来区分各个窗口的信息。我们的应用程序窗口,例如Activity,AlertDialog等窗口要添加到WMS上之前,都需要先添加Token信息到WMS中,这部分的工作是由framework层自动完成的,以后我们分析WMS的时候再说。

//这个变量作为一个同步锁来使用
private final Object mLock = new Object();
//这个列表保存了本进程中所有添加到WindowManager管理的根View
private final ArrayList mViews = new ArrayList();
//这个列表保存了所有添加到WindowManager管理的ViewRootImpl类,该类非常重要,我们之后分析。
private final ArrayList mRoots = new ArrayList();
//这个列表保存了所有添加到WM管理的布局参数。这3个列表使用下标来保证View,ViewRootImpl和LayoutParams之间的一一对应关系。
private final ArrayList mParams =
        new ArrayList();
//这个列表表示我们正在移除,但是还没有移除完成的view(即调用了removeView方法移除的View)
private final ArraySet mDyingViews = new ArraySet();


public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    //这里省略一些对View,display等参数的判空语句
    //另外特别注意ViewGroup.LayoutParams参数,这里为了接口的统一,使用的是ViewGroup的param类型。
    //但是实际上在这里判断如果传入的参数不是WindowManager.LayoutParams或者其子类型,会直接抛出异常。

    //获取布局参数
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    //1,如果我们要添加的窗口还有父窗口,那么对布局参数进行调整。主要目的是将父窗口的Id(IWindow)作为布局参数的Token
    if (parentWindow != null) {
        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    } 

    //ViewRootImpl类型的变量,很重要,我们之后分析。
    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {
        //这里省略一段对系统属性变化进行监听的代码。

        //2,查找该view在全局列表mViews中的位置
        int index = findViewLocked(view, false);
        //如果该view已经保存在列表中了
        if (index >= 0) {
            //如果我们已经使用过removeView方法移除过该view,该view会保存到mDyingViews全局列表中
            //我们调用ViewRootImpl的doDie方法,让它立即死亡。
            if (mDyingViews.contains(view)) {
                        mRoots.get(index).doDie();
            }
            //否则抛出异常,因为我们不能将一个同样的View多次添加到WindowManger中 
            else {
                throw new IllegalStateException("View " + view
                        + " has already been added to the window manager.");
            }
            // The previous removeView() had not completed executing. Now it has.
        }

        //如果该窗口是一个子窗口,则我们遍历ViewRootImpl查找该子窗口的父View。
        if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
            final int count = mViews.size();
            for (int i = 0; i < count; i++) {
                //因为子窗口必须使用父窗口的IWindow对象(Id)作为Token,所以我们查找token就能确定父窗口
                if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                    panelParentView = mViews.get(i);
                }
            }
        }
        //创建ViewRootImpl对象
        root = new ViewRootImpl(view.getContext(), display);
        //为根View设置布局参数
        view.setLayoutParams(wparams);
        //分别将View,ViewRootImpl和LayoutParam添加到对应的列表中。
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
    }

    try {
        //3,调用ViewRootImpl类的setView方法继续下面的操作
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        //添加失败,则我们将找出该View所在的位置,并清理资源
        synchronized (mLock) {
            final int index = findViewLocked(view, false);
            if (index >= 0) {
                removeViewLocked(index, true);
            }
        }
        throw e;
    }
}

Window类的adjustLayoutParamsForSubWindow方法

这个方法主要就是对我们之前说的token值进行调整,具体来说,将子窗口的布局参数中的token变量设置为父窗口的token,这样在WMS中,就可以凭借这个token找到该子窗口的父窗口了。

void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
    //省略部分无关代码

    //如果布局参数是子窗口类型
    if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
            wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
        //如果布局参数的token为空(默认的情况下,子窗口不会设置布局参数的token,也就是这里为空)
        if (wp.token == null) {
            //获取父窗口的DecorView(DecorView是Activity中的根布局View,它继承自frameLayout,我们以后讨论)
            View decor = peekDecorView();
            //使用父窗口的Token作为我们布局参数中的token,这样我们就可以通过这个token值找到父窗口了。
            //如果不是子窗口的情况下,布局参数中的token值将会为空。
            if (decor != null) {
                wp.token = decor.getWindowToken();
            }
        }

    } 
}

WindowManagerGlobal类的findViewLocked方法

该方法的实现很简单,就是从mViews列表中找到当前View的位置。

private int findViewLocked(View view, boolean required) {
    //从全局列表中查找当前View的位置
    final int index = mViews.indexOf(view);
    //如果没有找到当前View且需要抛出异常,那么我们就抛出异常。
    if (required && index < 0) {
        throw new IllegalArgumentException("View=" + view + " not attached to window manager");
    }
    return index;
}

WindowManagerGlobal类的updateViewLayout方法

该方法的实现比较简单,就是更新之前我们说的3个全局列表的信息,然后调用ViewRootImpl类的setLayoutParams来进行之后的工作。

public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
    //一些判断参数的语句,参数为空,就抛出异常。
    //设置布局参数
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
    view.setLayoutParams(wparams);

    synchronized (mLock) {
        //查找View的位置,如果找不到View那么就会抛出异常。也就是说必须先addView之后,才能updateViewLayout。
        int index = findViewLocked(view, true);
        //获取对应的ViewRootImpl对象
        ViewRootImpl root = mRoots.get(index);
        //更新mParams列表的布局参数。
        mParams.remove(index);
        mParams.add(index, wparams);
        //调用ViewRootImpl类的setLayoutParams方法进行下一步操作。
        root.setLayoutParams(wparams, false);
    }
}

removeView方法我们就不分析了,大致思路都差不多,也是先对3个全局的列表进行操作,然后调用ViewRootImpl类的die方法完成之后的操作。

这里我们总结一下ViewManager,WindowManager,WindowManagerImpl,WindowManagerGlobal这几个类之间的关系,它们之间的关系如下图所示。注意实际干活的WindowManagerGlobal类并没有继承这一系列的接口,个人猜测主要还是最初始的接口ViewManager需要保持和ViewGroup兼容的原因。
WindowManager源码分析-从悬浮窗说起_第1张图片

ViewRootImpl类

ViewRootImp类实现了ViewParent接口,该接口定义了很多我们熟悉的方法,例如requestLayout,createContextMenu等。ViewRootImpl和ViewGroup都实现了该接口。

ViewRootImpl作为整个控件树的根部,是控件树正常运转的动力所在,我们熟悉的控件树的measure,layout,draw过程最终都是由ViewRootImpl类的performTraversals方法所触发。注意该类虽然是控件树的根部,但是它自身并不是View类的子类,而仅仅是将控件树真正的根部View保存在自身的变量mView中。

另一方面,它是WindowManagerGlobal工作的实际实现者,因此它还要负责与WMS通信以调整窗口的大小和位置,以及对来自WMS的事件(如窗口大小改变)做出处理。

关于该类的详细信息,我们以后介绍,这里就仅仅简单的看一下setView方法是怎样最终和WMS产生关联的。

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            //保存控件树的根到mView中
            mView = view;
            //省略大段代码。
            //请求进行布局,该方法最后会调用到performTraversals方法
            //并最终完成measure,layout,draw这个过程
            requestLayout();

            try {
                //与WindowSession通信,最终将窗口添加到WMS中
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                        getHostVisibility(), mDisplay.getDisplayId(),
                        mAttachInfo.mContentInsets, mInputChannel);
            } catch (RemoteException e) {

            } 
         } 
    }
}

我们重点看一下mWindowSession这个变量,它是在ViewRootImpl类的构造方法中初始化的,它的类型就是我们之前介绍过的IWindowSession。

//IWindowSession类型,通过它与服务端的Session通信,并最终传递到WMS中。
final IWindowSession mWindowSession;

 public ViewRootImpl(Context context, Display display) {
    mContext = context;
    //从WindowManagerGlobal中获取IWindowSession实例,该实例是进程唯一的。
    mWindowSession = WindowManagerGlobal.getWindowSession();
}

WindowManagerGlobal的getWindowSession方法

可以看到,我们先获取WMS服务的binder代理对象,然后通过该对象跨进程调用WMS的openSession方法,让WMS给我们返回一个Session的binder代理对象。以后本进程就通过这个sWindowSession的binder代理对象和Session通信,Session在将信息转给WMS。

public static IWindowSession getWindowSession() {
    synchronized (WindowManagerGlobal.class) {
        //单例模式,这个静态变量我们之前介绍过了
        if (sWindowSession == null) {
            try {
                InputMethodManager imm = InputMethodManager.getInstance();
                //获取WMS的binder代理对象
                IWindowManager windowManager = getWindowManagerService();
                //调用IWindowManager的openSession方法,让WMS返回一个Session的
                //binder代理对象,以后我们的进程就和这个Session的代理对象进行通信
                sWindowSession = windowManager.openSession(
                        new IWindowSessionCallback.Stub() {
                            @Override
                            public void onAnimatorScaleChanged(float scale) {
                                ValueAnimator.setDurationScale(scale);
                            }
                        },
                        imm.getClient(), imm.getInputContext());
                ValueAnimator.setDurationScale(windowManager.getCurrentAnimatorScale());
            } catch (RemoteException e) {
            }
        }
        return sWindowSession;
    }
}

我们在看看getWindowManagerService方法,该方法同样是一个单例模式,通过ServiceManager取出系统中名称为window的服务,该服务就是WMS,然后将它转换为客户端的接口。可以看到WMS是从ServiceManager中通过名称获取的,所以我们叫它实名binder服务,而上面的Session就是匿名binder服务了。

public static IWindowManager getWindowManagerService() {
    synchronized (WindowManagerGlobal.class) {
        if (sWindowManagerService == null) {
            sWindowManagerService = IWindowManager.Stub.asInterface(
                    ServiceManager.getService("window"));
        }
        return sWindowManagerService;
    }
}

Session类的addToDisplay方法

可以看到,Session类的addToDiaplay就是调用了WMS的addWindow方法将窗口添加到了WMS中,这也是窗口能够显示出来的关键一步。到此,我们的分析就结束了,关于WMS和ViewRootImpl的更多细节,我们以后分析。

final class Session extends IWindowSession.Stub
        implements IBinder.DeathRecipient {
    //Session类中保存的WMS的实例
    final WindowManagerService mService;

    @Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outContentInsets,
            InputChannel outInputChannel) {
        //可以看到,该方法就是调用WMS的addWindow方法,最终将窗口添加到WMS中
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                outContentInsets, outInputChannel);
    }
}

总结

  • 在android framework中,所有显示在屏幕上的界面都是窗口,例如activity,状态栏,软键盘等。窗口分为系统窗口,应用窗口,子窗口三种。窗口的层级(Z)越高,越显示在上面。
  • WMS是对窗口进行管理的系统服务,由于所有的窗口管理都需要它来进行,android怕和它通信的进程太多,忙不过来。所以让所有的app都和Session通信,再由Session将信息转给WMS。
  • WindowManager并不是WMS或者Session的binder客户端,考虑到WMS是很重要的系统服务,android并不打算将里面的大多数方法开放给客户端使用。因此就有了我们的WindowManager,WindowManagerImpl以及WindowManagerGlob,它们仅仅开放了WMS很小一部分的方法。
  • ViewRootImpl类是android的View系统的核心,也是app和WMS交互的中转站。

下面这张图说明了WindowManager,WindowManagerImpl,WindowManagerGlobal,ViewRootImpl和WMS,Session之间的关系。
WindowManager源码分析-从悬浮窗说起_第2张图片

下面这张图是WindowManager的addView和removeView方法的流程,我们分析过的方法基本都包含在这个时序图中,大家也可以根据这个图分析。
WindowManager源码分析-从悬浮窗说起_第3张图片

你可能感兴趣的:(android源码分析)