Android 理解Window和WindowManager

概述

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

Android中全部的视图都是附加在Window上上呈现的,无论Activity,Dialog,Toast,他们的视图都是附加在Window上的,所以Window其实是View的直接管理者android

下面咱们来详细的了解Windowapp

Window和WindowMagaer

咱们先来了解一下如何使用WindwoMagaer来添加一个Windowide

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,下面分别介绍一下这俩个参数oop

TYPE 窗口的属性

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

Application Window(应用窗口)post

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

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,这个数值的大小涉及窗口的层级this

Sub Window(子窗口)spa

子窗口不可以独立存在,要依附在其余窗口上才行,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进阶解密》

相关文章
相关标签/搜索