理解Window和WindowManager

​ 现如今,很多视频App都支持悬浮式的视频播放器,可以在阅读其他内容的同时观看视频内容,还有很多音频App可以在屏界面展示歌词、操作按钮等内容,这些看上去酷酷的feature是怎么做到的呢?在Android开发中,大家都用到过Toast,Toast在应用运行过程中可以在系统全局展示一个消息提醒,这里面暗藏着Window的相关知识。

​ Window是一个抽象的概念,在Android中对应着一个抽象类,在Google注释中对它的解释是

"一个代表着顶级窗口的视觉和行为的抽象基类,这个类的实例可作为顶级View被添加到WindowManager中。它提供了标准的UI规范,例如背景、标题、默认的按键行为等。"

​ 总的来说,Window对应着应用的窗口,为应用提供了界面展示、交互行为等功能。看到这里可能会有疑惑,为应用提供界面展示、交互行为功能的不是Activity吗?这并不矛盾,在Activity中,负责这两个功能的恰恰是Activity所持有的Window。

如何使用Window

​ Window的管理都通过WindowManager完成。WindowManager是一个接口,继承自ViewManager,提供了addView、updateViewLayout、removeView等三个方法,分别对应Window的添加、更新、删除。WidowManager的实现类是WindowManagerImpl,在WindowMangerImpl中,这三个方法又调用到了WindowManagerGlobal的对应方法。

WindowManagerGlobal源码: android.googlesource.com/platform/fr…

添加Window

添加Window最终会调用到WindowManagerGlobal的addView方法,在WindowManagerGlobal中维护了三个List,分别表示当前添加的View,当前添加的View对应的ViewRootImpl,当前添加的View对应的LayoutParams

public void addView(View view , ViewGroup.LayoutParams params , Display display , Window parentWindow)
复制代码

首先介绍方法的几个入参:

  1. View:需要添加的View,每个Window都对应着一个View,这里传入的就是Window要展示的内容
  2. params:与ViewGroup中添加View相同,Window中的View也有自己的LayoutParams。这里的入参必须是WindowManager.LayoutParams,不然会报错。Window的LayoutParams除了可以设置宽高外,还可以设置类型、标题、token等属性
  3. display : 逻辑显示器的参数,可以通过WindowManager.getDefaultDisplay获取
  4. parentWindow:需要添加的Window依赖的父Window,根据要添加的Window的类型决定,可以为null

在这个方法中,首先会对view、display判空,会检查params的类型,输入参数合法才会执行接下来的操作。添加Window主要分为以下几个步骤:

adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp)

这一过程的代码也很简单

//WindowManagerGlobal.java  
public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {
        //参数校验
        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");
        }
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        if (parentWindow != null) {
            //根据父Window调整子window的LayoutParam
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        } else {
            //没有父Window的情况下根据Application的硬件加速属性设置子Window的LayoutParams的flag
            final Context context = view.getContext();
            if (context != null
                    && (context.getApplicationInfo().flags
                            & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
                wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
            }
        }
        //中间一段省略  
        ..........

        // 如果Window是一个子Window,则需要根据token找到这个window所属的父window
        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++) {
                if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                    panelParentView = mViews.get(i);
                }
            }
        }
        //创建ViewRootImpl,设置LayoutParams
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        mViews.add(view); //WindowManagerGlobal维护的三个队列
        mRoots.add(root);
        mParams.add(wparams);
        try {
            //通过ViewRootImpl添加View并更新,对于非子Window,这里的panelParentView为null
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            if (index >= 0) {
                removeViewLocked(index, true);
            }
            throw e;
       }
  }
复制代码

这部分代码很简短且逻辑清晰,但是看到这里仍然没有看到Window是怎么添加的。很显然,能包含这一个过程的就只有ViewRootImpl的setView方法了。

ViewRootImpl的setView方法很长,足足200多行,我们只关心和Window有关的内容。

  1. 首先,进入这个方法的时候会上锁,避免出现线程问题。毕竟目前没有看到添加Window的过程中有地方限制必须在UI线程执行,更何况还有Toast这种系统层面的Window。
  2. 所有过程只会在当前ViewRootImpl没有设置view的情况下执行,ViewRootImpl只能有一个对应的View
  3. 通过IWindowSession添加Window。IWindowSession是一个binder类,通过WindowManagerService中的openSession获得,这其实很好理解,如果应用能添加在其他应用中也可见的Window的话,那这个过程肯定是需要经过系统调度的。
//https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/view/ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if(mView == null){
                mView = view;
                //中间省略
                ......
                int index = findViewLocked(view, false);
                if (index >= 0) {
                    if (mDyingViews.contains(view)) {
                        //这个view上一次的移除过程还没完成,需要先移除
                        mRoots.get(index).doDie();
                    } else {
                        throw new IllegalStateException("View " + view
                                + " has already been added to the window manager.");
                    }
                }
                try {
                    mOrigWindowType = mWindowAttributes.type;
                    mAttachInfo.mRecomputeGlobalAttributes = true;
                    collectViewAttributes();
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
                            mTempInsets);
                    setFrame(mTmpFrame);
                } catch (RemoteException e) {
                    mAdded = false;
                    mView = null;
                    mAttachInfo.mRootView = null;
                    mInputChannel = null;
                    mFallbackEventHandler.setView(null);
                    unscheduleTraversals();
                    setAccessibilityFocus(null, null);
                    throw new RuntimeException("Adding window failed", e);
                } finally {
                    if (restore) {
                        attrs.restore();
                    }
                }
                ......//后面还有一部分代码是判断添加Window失败的异常类型的,有兴趣的可以看一看
            }
        }
 }
复制代码

看完上面这些过程,应该对Window的添加过程有了一定的了解,在添加Window的过程中,我们两次用到了LayoutParams中的type属性,这个int值代表着Window的类型,通过查看WindowManger.LayoutParams我们可以看到关于type的一些说明:Window有三种类型,应用Window、子Window、系统Window,展示的优先级依次递增,反映到type中就是优先级越高,type越大。应用Window的type在1-99之间,子Window的type在1000-1999之间,系统Window的type在2000-2999之间,并且Android为我们预定义了很多个Type,大多数情况下我们不需要自己编一个值。

删除Window和更新Window

与添加Window相同,删除和更新Window会经过WindowManagerGlobal,并且最终会通过ViewRootImpl完成。

WindowManagerGlobal中,删除View的代码很简短,immediate表示这个方法是同步执行的还是异步执行的,一般情况情况下不需要使用同步的方法,避免出现错误。

public void removeView(View view, boolean immediate) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        synchronized (mLock) {
            int index = findViewLocked(view, true);//找到需要删除的View的index
            View curView = mRoots.get(index).getView();//找到对应的view
            removeViewLocked(index, immediate);
            if (curView == view) {
                return;
            }
            throw new IllegalStateException("Calling with view " + view
                    + " but the ViewAncestor is attached to " + curView);
        }
    }


private void removeViewLocked(int index, boolean immediate) {
        ViewRootImpl root = mRoots.get(index);
        View view = root.getView();
        if (view != null) {
            InputMethodManager imm = view.getContext().getSystemService(InputMethodManager.class);
            if (imm != null) {
                imm.windowDismissed(mViews.get(index).getWindowToken());
            }
        }
        boolean deferred = root.die(immediate);
        if (view != null) {
            view.assignParent(null);
            if (deferred) {
                mDyingViews.add(view); //与添加View的过程类似,这里也维护了一个需要移除的View的集合
            }
        }
    }
复制代码

在代码中我们可以看到最终是调用到了ViewRootImpl的die方法中。如果是同步删除,即immediate = true的情况下,会立即执行ViewRootImpl的doDie方法,immediate = false时,则是通过handler发送一个消息,在handleMsg时调用doDie方法。

//https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/view/ViewRootImpl.java
 void doDie() {
        checkThread();
        if (LOCAL_LOGV) Log.v(mTag, "DIE in " + this + " of " + mSurface);
        synchronized (this) {
            if (mRemoved) {
                return;
            }
            mRemoved = true;
            if (mAdded) {
                dispatchDetachedFromWindow();//这个方法里会通过WindowSession移除window
            }
            if (mAdded && !mFirst) {
                destroyHardwareRenderer();
                if (mView != null) {
                    int viewVisibility = mView.getVisibility();
                    boolean viewVisibilityChanged = mViewVisibility != viewVisibility;
                    if (mWindowAttributesChanged || viewVisibilityChanged) {
                        // If layout params have been changed, first give them
                        // to the window manager to make sure it has the correct
                        // animation info.
                        try {
                            if ((relayoutWindow(mWindowAttributes, viewVisibility, false)
                                    & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0){
                                mWindowSession.finishDrawing(mWindow);
                            }
                        } catch (RemoteException e) {
                        }
                    }
                    destroySurface();
                }
            }
            mAdded = false;
        }
        //还记的WindowManagerGlobal维护的三个List吗以及mDyingViews吗?
        WindowManagerGlobal.getInstance().doRemoveView(this);
    }
复制代码

与删除过程类似,更新过程同样是WindowManagerGlobal找到View对应的ViewRootImpl,然后调用ViewRootImpl的setLayoutParams方法。ViewRootImpl最后会调用scheduleTraversals方法,后面就是View的measure、layout、draw了。

总结

  1. 每个Window都会对应一个ViewRootImpl,Window的添加、删除、更新都是通过WindowManagerGlobal对ViewRootImpl的调度完成的,添加和删除过程最后会调用IWindowSession接口中的方法,IWindowSession是一个Binder接口,由WindowManagerService创建
  2. WindowManagerGlobal中会维护三个List和一个Set,三个List分别对应已添加的View以及对应的LayoutParams、ViewRootImpl,待删除的View则由Set维护,在添加Window时,会判断Window对应的View是否存在未完成的移除过程,是的话立即移除。
  3. Window分为应用Window、子Window、系统Window三个类型,根据LayoutParams中的type值区分

常见的Window

在日常的开发中隐藏着Window的使用,只是不阅读源码的话很那认识到这些能力其实都是Window赋予的。比如Activity和Dialog。

像PopupWindow这种就不用说了,从名字就能看出来,顺便一提PopupWindow的type是TYPE_APPLICATION_PANEL,优先级最低的子Window

Activity中Window的创建过程

Activity的启动过程比较复杂,在这里我们只关心和Window有关的部分。

在经过一系列的调用后,startActivity方法最终会调用到ActivityThread中的performLaunchActivity中,在这个方法中会通过类加载器完成Activity的创建。

//https://android.googlesource.com/platform/frameworks/base/+/android-4.4_r1/core/java/android/app/ActivityThread.java 
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ......//这部分先忽略,其中包括了创建Activity的过程
        if (activity != null) {
            Context appContext = createBaseContextForActivity(r, activity);
            CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
            
            Configuration config = new Configuration(mCompatConfiguration);
            if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
                     + r.activityInfo.name + " with config " + config);
            //创建Window的过程就隐藏在Activity的attach之中
            activity.attach(appContext, this, getInstrumentation(), r.token,
                     r.ident, app, r.intent, r.activityInfo, title, r.parent,
                     r.embeddedID, r.lastNonConfigurationInstances, config);
        }
    ......//后续的代码中还会回调Activity的onCreate方法,这也是为什么我们在Activity的onCreate中可以使用setContentView
 }
复制代码

接下来我们转到Activity的attach方法。

//https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/app/Activity.java
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, IBinder assistToken) {
        attachBaseContext(context);
        //在这里创建了一个PhoneWindow
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
    
}
复制代码

很多介绍Activity的文章都有说过,Activity中有一个PhoneWindow,PhoneWindow中包含有一个DecorView,在Activity中调用setContentView方法实际上是设置DecorView的content,看到attach方法中Window的创建,就明白为什么说Activity中有一个PhoneWindow了。

在《Android开发艺术探索》书中有说道,Activity的Window是通过PolicyManager创建的,这个应该是以前的源码中的实现,我没有去具体查证,这并不影响我们理解Activity中和Window相关的事

到这里,我们看到了如何创建Activity的Window,但是这个Window并没有被添加。回顾一下Activity的生命周期,onCreate表示Activity被创建,onResume表示Activity回到前台可以被看见,很显然,Activity的onResume中会完成Window的添加。下面来看ActivityThread的handleResumeActivity方法。

// https://android.googlesource.com/platform/frameworks/base/+/android-4.4_r1/core/java/android/app/ActivityThread.java
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward,
            boolean reallyResume) {
    ......
    		if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (a.mVisibleFromClient) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);//在这里
                }
            } else if (!willBeVisible) {
                if (localLOGV) Slog.v(
                    TAG, "Launch " + r + " mStartedActivity set");
                r.hideForNow = true;
            }
    ......
}
复制代码

Dialog中Window的创建

Dialog中Window的创建就很简单了,直接看Dialog的构造方法

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
       .......
        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

        final Window w = new PhoneWindow(mContext);//同样是直接new一个PhoneWindow
        mWindow = w;
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        w.setOnWindowSwipeDismissedCallback(() -> {
            if (mCancelable) {
                cancel();
            }
        });
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);//设置弹窗居中

        mListenersHandler = new ListenersHandler(this);
    }
复制代码

添加Window的过程在dialog的show方法中

public void show() {
     ......
     mDecor = mWindow.getDecorView();
     ......
     WindowManager.LayoutParams l = mWindow.getAttributes();
     boolean restoreSoftInputMode = false;
     if ((l.softInputMode
                & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
         l.softInputMode |=
                    WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
         restoreSoftInputMode = true;
     }

     mWindowManager.addView(mDecor, l);//添加Window
     ......
 }
复制代码

原文 

https://juejin.im/post/5e8e6c4051882573c74d4fdc

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » 理解Window和WindowManager

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址