Android 理解Window和WindowManager

概述

Window表示窗口的概念,他是一个抽象类,他的真正实现类是PhoneWindowWindowManager用来对Window进行管理,是外接访问Window的入口,Window操作的具体实现是在WindowManagerService中,WindowMagerWindowManagerService交互是IPC的过程

Android中所有的视图都是附加在Window上上呈现的,不管Activity,Dialog,Toast,他们的视图都是附加在Window上的,因此Window实际上是View的直接管理者

下面我们来详细的了解Window

Window和WindowMagaer

我们先来了解一下如何使用WindwoMagaer来添加一个Window

        Button button = new Button(this);
        button.setText("Window");
        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSPARENT);

        layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;

        layoutParams.flags= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;

        layoutParams.gravity= Gravity.LEFT|Gravity.TOP;
        layoutParams.x=100;
        layoutParams.y=300;

        WindowManager windowManager = getWindowManager();

        windowManager.addView(button,layoutParams);

这段代码可以添加一个Window,位置在(100,300)处,这里面有俩个参数比较重要分别是,typeflag,下面分别介绍一下这俩个参数

TYPE 窗口的属性

type参数表示Window的类型,Window有三种类型,分别是Application Window(应用窗口),Sub Window(子窗口)和System Window(系统窗口),每个大类型又包含多个小类型,他们都定义在WindowMager的静态内部类LayoutParams中,下面对这三种类型进行讲解

Application Window(应用窗口)

Activity就是典型的应用窗口,应用窗口包含的类型如下:

        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;

应用窗口就包括了以上几中类型,其中最上方是起始值,最下方是结束值,也就是说应用窗口的Type值的范围是1-99,这个数值的大小涉及窗口的层级

Sub Window(子窗口)

子窗口不能够独立存在,要依附在其他窗口上才行,PopupWindow就属于子窗口,子窗口的定义类型如下:

        //子窗口的初始值
        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;

可以看出子窗口的type值范围是1000-1999

System Window (系统窗口)

Toast,输入法窗口,系统音量条窗口,系统错误窗口,都属于系统窗口,系统窗口的类型定义如下:

        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;
        @Deprecated
        public static final int TYPE_PHONE              = FIRST_SYSTEM_WINDOW+2;
        @Deprecated
        public static final int TYPE_SYSTEM_ALERT       = FIRST_SYSTEM_WINDOW+3;
        public static final int TYPE_KEYGUARD           = FIRST_SYSTEM_WINDOW+4;
        @Deprecated
        public static final int TYPE_TOAST              = FIRST_SYSTEM_WINDOW+5;
        @Deprecated
        public static final int TYPE_SYSTEM_OVERLAY     = FIRST_SYSTEM_WINDOW+6;
        @Deprecated
        public static final int TYPE_PRIORITY_PHONE     = FIRST_SYSTEM_WINDOW+7;
        ...
        public static final int LAST_SYSTEM_WINDOW      = 2999;

系统窗口接近40个,这里只列出一小部分,系统窗口的Type值在2000-2999之间

窗口的显示次序

上面介绍的Type值越大,就意味着靠用户越近,很显然系统的窗口是最大的,他在应用窗口和子窗口的上方

FLAG 窗口的标志

Flag就是窗口的标志,用于控制Window的显示,同样被定义在WindowManager的内部类LayoutParams中,一共有20多个,这里列出一些常用的

type描述
FLAG_ALLOW_LOCK_WHILE_SCREEN_ON只要窗口可见,就允许在开启状态的屏幕上锁屏
FLAG_NOT_FOCUSABLE窗口不能获取输入焦点,设置该标志的同时,FLAG_NOT_TOUCH_MODAL也会被设置
FLAG_NOT_TOUCH_MODAL将该窗口区域外的触摸事件,传递给其他Window,而自己只会处理窗口区域内的触摸事件
FLAG_NOT_TOUCHABLE窗口不接受任何触摸事件
FLAG_KEEP_SCREEN_ON只要窗口可见,就一直保持屏幕长亮
FLAG_LAYOUT_NO_LIMITS允许窗口超出屏幕外
FLAG_FULLSCREEN隐藏所有的屏幕装饰窗口,比如游戏视频等全屏显示
FLAG_SHOW_WHEN_LOCKED窗口可以在锁屏窗口之上显示
FLAG_IGNORE_CHEEK_PRESSES当用户脸贴近屏幕时(比如打电话时),不会响应此事件
FLAG_TURN_SCREEN_ON窗口显示时将屏幕点亮

设置Window的Flag除了上方的方式外还可以采用下面的方式

        //第一种
        Window window = getWindow();
        window.addFlags();
        
        //第二种
        Window window = getWindow();
        window.setFlags();

软键盘模式

我们在写登陆界面的时候,默认弹出的软键盘窗口可能会覆盖输入框下面的按钮,为了让软键盘按照期望的方式显示,,WindowMagaer的静态内部类LayoutParams中定义了软键盘的相关模式,我们介绍一下常用的

SoftInputMode描述
SOFT_INPUT_STATE_UNSPECIFIED没有指定状态,系统会选择一个合适的状态或依赖于主题的设置
SOFT_INPUT_STATE_UNCHANGED不会改变软键盘的状态
SOFT_INPUT_STATE_HIDDEN当用户进入该窗口时,软键盘默认隐藏
SOFT_INPUT_STATE_ALWAYS_HIDDEN当窗口获取焦点时,软键盘总是隐藏
SOFT_INPUT_ADJUST_RESIZE当软键盘弹出时,窗口会调整大小
SOFT_INPUT_ADJUST_PAN当软键盘弹出时,窗口不需要调整大小,要确认输入焦点是否可见

软键盘模式可以在AndroidManifest中设置

  <activity android:name=".CameraActivity"
            android:launchMode="singleTask"
            android:windowSoftInputMode="adjustPan">

        </activity>

也可以代码设置

     getWindow().setSoftInputMode();

WindowManager

WindowMagaer所提供的功能很简单,只有常用的三个方法即,添加View,更新View,删除View,这个三个方法定义在ViewManager中,而WindowManager继承自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);
}

Window的内部机制

Window是一个抽象概念,每一个Window都对应一个View和一个ViewRootImplWindowView通过ViewRootImpl来建立联系,因此Window不是实际存在的,他是以View的形式存在的,在实际是一个中,不能直接访问Window,只有通过WindowManager才能访问

Window的添加过程

Window的添加是通过WindowManageraddView方法实现的,我们WindowManager##addView方法作为入口来分析,WindowMagager是一个接口,真正的实现在WindowManagerImpl

 @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

我们发现他其实把事情交给了WindowManagerGlobal

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        //注释1
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        if (display == null) {
            throw new IllegalArgumentException("display must not be null");
        }
        if (!(params instanceof WindowManager.LayoutParams)) {
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
        }
            ····
            //注释2
            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);
            //注释3
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

            // do this last because it fires off messages to start doing things
            try {
                //注释4
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }

我们首先了解几个重要的变量

    //储存所有Window对应的View
    private final ArrayList<View> mViews = new ArrayList<View>();
    //储存所有Window对应的ViewRoot
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
    //布局参数列表
    private final ArrayList<WindowManager.LayoutParams> mParams =
            new ArrayList<WindowManager.LayoutParams>();
  • 注释1处,它主要是检查参数是否合法
  • 注释2处,在此处创建了ViewRootImpl并赋值给root变量
  • 注释3处,将View,root和params添加到列表中
  • 注释4处,调用ViewRootImpl来更新界面并完成Window的添加过程

ViewRootImpl有很多的职责

  • View树的根,并管理View树
  • 触发View的测量,布局和绘制
  • 输入时间的中转站
  • 管理Surface
  • 负责与WMS通信

我们继续看一下ViewRootImplsetView方法

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
        ···
             // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                requestLayout();
        ···
           try {
                    mOrigWindowType = mWindowAttributes.type;
                    mAttachInfo.mRecomputeGlobalAttributes = true;
                    collectViewAttributes();
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
                } catch (RemoteException e) {
    
        }
        }

这个方法首先会调用requestLayout方法来完成一部刷新请求

  public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

scheduleTraversals实际是View绘制的入口

然后调用mWindowSession.addToDisplay方法,mWindowSession是一个IWindowSession类型的,是一个Binder对象,用于进程间通信,也就是说addToDisplay方法其实是运行在WMS所在的进程system_server进程

   @Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
            Rect outOutsets, InputChannel outInputChannel) {

        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                outContentInsets, outStableInsets, outOutsets, outInputChannel);
    }

addToDisplay方法内部调用了WMSaddWindow方法并将自身也就是Session传入了进去,每个应用程序都会有一个SessionWMS会用ArrayList来保存起来,这样所有的工作都交给了WMS来做

WMS会为这个添加的窗口分配Surface,并确定窗口的显示次序,负责显示界面的是画布Surface,而不是窗口本身,WMS会把Surface交给SurfaceFlinger处理,SurfaceFlinger会把这些Surface混合并绘制到屏幕上

Window的更新过程

Window的更新过程和添加过程是类似的,需要调用WindowManagerupdateViewLayout方法,然后会继续进入WindowManagerGlobalupdateViewLayout方法,我们直接从这个方法进行分析

   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
        view.setLayoutParams(wparams);

        synchronized (mLock) {
            //注释2
            int index = findViewLocked(view, true);
            //注释3
            ViewRootImpl root = mRoots.get(index);
            //注释4
            mParams.remove(index);
            //注释5
            mParams.add(index, wparams);
            //注释6
            root.setLayoutParams(wparams, false);
        }
    }
  • 注释1,将更新的参数设置到View中
  • 注释2,得到要更新的窗口在View列表中的索引
  • 注释3,根据索引获取窗口的ViewRoot
  • 注释4 5,用于更新布局参数列表
  • 注释6,调用ViewRootsetLayoutParams方法,将更新的参数设置到ViewRootImpl中,setLayoutParams方法最终会调用ViewRootImplscheduleTraversals方法,我们看下这个方法

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //注释1
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

我们看下注释1,mChoreographer翻译为编舞者,用于接受系统的VSync信号,在下一个帧渲染时控制一些操作,mChoreographerpostCallback方法用于添加回调,这个添加的回调,将在下一帧渲染时执行,这个添加的回调指的是TraversalRunnable类型的mTraversalRunnable,如下:

  final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

这个方法内部调用了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,这个方法中更新了Window的视图,并且完成了View的绘制流程,measure,layout,draw,这样就完成了View的更新

Activity的Window的创建过程

这个需要了解App的启动过程,这个我就不再重复说了,不了解的可以看我之前的文章Android App启动过程,他最后会调用performLaunchActivity方法来完成整个启动过程,这个方法内部会通过类加载器创建Activity的实例对象,并调用了attach方法,为其关联运行中所依赖的一系列变量

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ...
    Activity activity = null;
    try {
        java.lang.ClassLoader cl = appContext.getClassLoader();
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
        ...
    } catch (Exception e) {
        ...
    }

    try {
        // 返回之前创建过的 application 对象
        Application app = r.packageInfo.makeApplication(false, mInstrumentation);
        ...
        if (activity != null) {
            ...
            // attach 到 window 上
            activity.attach(appContext, this, getInstrumentation(), r.token,
                    r.ident, app, r.intent, r.activityInfo, title, r.parent,
                    r.embeddedID, r.lastNonConfigurationInstances, config,
                    r.referrer, r.voiceInteractor, window, r.configCallback);
            ...
            if (r.isPersistable()) {
                mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
            } else {
                mInstrumentation.callActivityOnCreate(activity, r.state);
            }
            ...
        }
    } catch (Exception e) {
        ...
    }
    return activity;
}
  final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
            mWindow.setSoftInputMode(info.softInputMode);
        }
        if (info.uiOptions != 0) {
            mWindow.setUiOptions(info.uiOptions);
        }
        mUiThread = Thread.currentThread();

       ...

        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;

        mWindow.setColorMode(info.colorMode);

        setAutofillCompatibilityEnabled(application.isAutofillCompatibilityEnabled());
        enableAutofillCompatibilityIfNeeded();
    }

这个方法,会创建Activity所属的Window对象并为其设置回调接口,到这里Window已经创建完成了,下面我们分析一下Activity的视图是怎么依附到Window上的,由于Activity的视图是从setContentView方法提供,我们从setContentView方法开始分析

getWindow().setContentView(layoutResID);
initWindowDecorActionBar();

我们点进去发现他其实调用了PhoneWindow的setContentView方法,我们看下这个方法

  public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

这个方法主要做了以下几件事

  • 如果DecorView不存在则创建DecorView,DecorViewActivity中的顶级View,一般来说他包括标题栏内容栏,这个会随着主题的改变而改变,反正内容栏一定存在,并且他有固定的idandroid.R.id.content, 创建DecorViewinstallDecor方法完成,内部会通过generateDecor方法创建,这个时候DecorView还是一个空白的Framlayout
   private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            //创建DecroView
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            //向DecorView添加内容
            mContentParent = generateLayout(mDecor);
        }
        ...
    }
   protected DecorView generateDecor(int featureId) {
        ...
        return new DecorView(context, featureId, this, getAttributes());
    }
  • 初始化DecorView的结构,通过generateLayout方法加载具体的布局文件到DecorView中,并为内容栏变量赋值
 ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
  • 然后将setContentViewView添加到内容栏中, mLayoutInflater.inflate(layoutResID, mContentParent);这时Activity的布局文件就已经添加到了DecorView的内容栏中
  • 最后回调onContentChanged方法,通知Activity视图已经改变

通过上方的步骤,现在DecorView已经创建并初始化完成,Activity的布局也添加到DecorView的内容栏中,但是这个时候DecorView还没有被WindowManager添加到Window

ActivityThreadhandleResumeActivity会调用ActivityonResume方法,并且会调用ViewManageraddView方法把DecorView添加到Window

    @Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {

    
        //调用Activity的onResume方法
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
        ...
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            //获取WindowMagaer
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
         
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    //把DecorView添加到Window
                    wm.addView(decor, l);
                } 
                ...
            
    }

到这里Activity的Window创建过程分析完毕

参考:《Android开发艺术探索》《Android进阶解密》

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值