直面底层:Android 中的Window

window机制的关键类

PhoneWindow是窗口类,继承自抽象类Window,也是唯一子类。WindowManager是Window管理接口,继承自ViewManager,他的唯一实现类是WindowManagerImpl。WindowManagerImpl并没有真正实现windowManager接口逻辑,而是把逻辑转给了WindowManagerGlobal,WindowManagerGlobal是全局单例。Window和View的联系通过ViewRootImpl桥梁,同时ViewRootImpl还负责管理view树、绘制view树、和WMS通信。WMS即WindowManagerService,是Window的真正管理类。

Window的添加过程

window的添加过程,指的是我们通过WindowManagerImpl的addView方法来添加window的过程。window的存在形式是view也可以从这个方法名字看出来,添加window即为添加view。

想要添加一个window,我们知道首先得有view和WindowManager.LayoutParams对象,才能去创建一个window,这是我们常见的代码:

Button button = new Button(this);
WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams();
// 这里对windowParam进行初始化
windowParam.addFlags...
// 获得应用PhoneWindow的WindowManager对象进行添加window
getWindowManager.addView(button,windowParams);
然后接下来我们进入addView方法中看看。我们知道这个windowManager的实现类是WindowManagerImpl,上面讲过,进入他的addView方法看一看:
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

可以发现他把逻辑直接交给mGlobal去处理了。这个mGlobal是啥?

有印象的读者就会知道他是WindowManagerGlobal,是一个全局单例,所以这里可以看到WindowManagerGlobal确实是WindowManager接口的具体逻辑实现,这里运用的是桥接模式。那我们进WindowManagerGlobal的方法看一下:

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) {
        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    } else {
        final Context context = view.getContext();
        if (context != null
                && (context.getApplicationInfo().flags
                        & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
            wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
        }
    }

    synchronized (mLock) {
        ...
        // 这里新建了一个viewRootImpl,并设置参数
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);

        // 添加到windowManagerGlobal的三个重要list中,后面会讲到
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        // 最后通过viewRootImpl来添加window
        try {
            root.setView(view, wparams, panelParentView);
        }
        ...
    }
}

代码有点长,一步步看不复杂的。

  • 首先对参数的合法性进行检查

  • 然后判断该窗口是不是子窗口,如果是的话需要对窗口进行调整,这个好理解,子窗口要跟随父窗口的特性。

  • 接着新建viewRootImpl对象,并把view、viewRootImpl、params三个对象添加到三个list中进行保存

  • 最后通过viewRootImpl来进行添加

补充一点关于WindowManagerGlobal中的三个list,他们分别是:

private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams =
        new ArrayList<WindowManager.LayoutParams>();

每一个window所对应的这三个对象都会保存在这里,之后对window的一些操作就可以直接来这里取对象了。当window被删除的时候,这些对象也会被从list中移除。

可以看到添加的window的逻辑就交给ViewRootImpl了。viewRootImpl是window和view之间的桥梁,viewRootImpl可以处理两边的对象,然后联结起来。下面看一下viewRootImpl怎么处理:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        ...
        try {
            mOrigWindowType = mWindowAttributes.type;
            mAttachInfo.mRecomputeGlobalAttributes = true;
            collectViewAttributes();
            // 这里调用了windowSession的方法,调用wms的方法,把添加window的逻辑交给wms
            res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                    getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
                    mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                    mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
                    mTempInsets);
            setFrame(mTmpFrame);
        }
        ...
    }
}

viewRootImpl的逻辑很多,重要的就是调用了mWindowSession的方法调用了WMS的方法。这个mWindowSession很重要重点讲一下。

mWindowSession是一个IWindowSession对象,看到这个命名很快地可以像到这里用了AIDL跨进程通信。IWindowSession是一个IBinder接口,他的具体实现类在WindowManagerService,本地的mWindowSession只是一个Binder对象,通过这个mWindowSession就可以直接调用WMS的方法进行跨进程通信。

那这个mWindowSession是从哪里来的呢?我们到viewRootImpl的构造器方法中看一下:

public ViewRootImpl(Context context, Display display) {
      ...
      mWindowSession = WindowManagerGlobal.getWindowSession();
      ...
}
可以看到这个session对象是来自WindowManagerGlobal。再深入看一下:
public static IWindowSession getWindowSession() {
    synchronized (WindowManagerGlobal.class) {
        if (sWindowSession == null) {
            try {
                ...
                sWindowSession = windowManager.openSession(
                        new IWindowSessionCallback.Stub() {
                            ...
                        });
            }
            ...
        }
        return sWindowSession;
    }
   }
这熟悉的代码格式,可以看出来这个session是一个单例,也就是整个应用的所有viewRootImpl的windowSession都是同一个,也就是一个应用只有一个windowSession

对于wms而言,他是服务于多个应用的,如果说每个viewRootImpl整一个session,那他的任务就太重了。

WMS的对象单位是应用,他在内部给每个应用session分配了一些数据结构如list,用于保存每个应用的window以及对应的viewRootImpl。当需要操作view的时候,通过session直接找到viewRootImpl就可以操作了。

后面的逻辑就交给WMS去处理了,WMS就会创建window,然后结合参数计算window的高度等等,最后使用viewRootImpl进行绘制。这后面的代码逻辑就不讲了,这是深入到WMS的内容,再讲进去就太复杂了(笔者也还没读懂WMS)。读源码的目的是了解整个系统的本质与工作流程,对系统整体的感知,而不用太深入代码细节,Android系统那么多的代码,如果深入进去会出不来的,所以点到为止就好了。

我们知道windowManager接口是继承viewManager接口的,viewManager还有另外两个接口:removeView、updateView。这里就不讲了,有兴趣的读者可以自己去阅读源码。讲添加流程主要是为了理解window系统的运作,对内部的流程感知,以便于更好的理解window。

总结:window的添加过程是通过PhoneWindow对应的WindowManagerImpl来添加window,内部会调用WindowManagerGlobal来实现。WindowManagerGlobal会使用viewRootImpl来进行跨进程通信让WMS执行创建window的业务。

每个应用都有一个windowSession,用于负责和WMS的通信,如ApplicationThread与AMS的通信。

常见组件的window创建流程

上面讲的是通过windowManagerImpl创建window的过程,我们通过前面的讲解了解到,WindowManagerImpl是管理PhoneWindow的,他们是同时出现的。因而有两种创建window的方式:

  • 已经存在PhoneWindow,直接通过WindowManagerImpl创建window

  • PhoneWindow尚未存在,先创建PhoneWindow,再利用windowManagerImpl来创建window

当我们在Activity中使用getWindowManager方法获取到的就是应用的PhoneWindow对应的WindowManagerImpl。下面来讲一下不同的组件是如何创建window的。

Activity

如果有阅读过Activity的启动流程的读者,会知道Activity的启动最后来到了ActivityThread的handleLaunchActivity这个方法。

关于Activity的启动流程,我写过一篇文章,有兴趣的读者可以点击下方链接前往:

Activity启动流程详解(基于api28)https://blog.csdn.net/weixin_43766753/article/details/107746968

至于为什么是这个方法这里就不讲了,有兴趣的读者可以去看上面的文章。我们直接来看这个方法的代码:

public void handleLaunchActivity(IBinder token, boolean finalStateRequest, boolean isForward,
        String reason) {
    ...;
    // 这里对WindowManagerGlobal进行初始化
    WindowManagerGlobal.initialize();

    // 启动Activity并回调activity的onCreate方法
    final Activity a = performLaunchActivity(r, customIntent);
    ...
}


private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    try {
        // 这里创建Application
        Application app = r.packageInfo.makeApplication(false, mInstrumentation);
        ...
        if (activity != null) {
            ...
            Window window = null;
            if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
                window = r.mPendingRemoveWindow;
                r.mPendingRemoveWindow = null;
                r.mPendingRemoveWindowManager = null;
            }
            appContext.setOuterContext(activity);
            // 这里将window作为参数传到activity的attach方法中
            // 一般情况下这里window==null
            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,
                    r.assistToken);
            ...
            // 最后这里回调Activity的onCreate方法
            if (r.isPersistable()) {
                mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
            } else {
                mInstrumentation.callActivityOnCreate(activity, r.state);
            }
        }

    ...
}

handleLaunchActivity的代码中首先对WindowManagerGlobal进行初始化,然后调用了performLaunchActivity方法。代码很多,这里只截取了重要部分。

首先会创建Application对象,然后再调用Activity的attach方法,把window作为参数传进去,最后回调activity的onCreate方法。所以这里最有可能创建window的方法就是Activity的attach方法了。我们进去看一下:

final void attach(...,Context context,Window window, ...) {
    ...;
    // 这里新建PhoneWindow对象,并对window进行初始化
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    // Activity实现window的callBack接口,把自己设置给window
    mWindow.setWindowControllerCallback(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);
    ...
    // 这里初始化window的WindowManager对象
    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
}

同样只截取了重要代码,attach方法参数非常多,我只留下了window相关的参数。在这方法里首先利用传进来的window创建了PhoneWindow。

Activity实现window的callBack接口,可以把自己设置给window当观察者。当window发生变化的时候可以通知activity。然后再创建WindowManager和PhoneWindow绑定在一起,这样我们就可以通过windowManager操作PhoneWindow了。(这里不是setWindowManager吗,windowManager是什么时候创建的?)我们进去setWindowManager方法看一下:

public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
        boolean hardwareAccelerated) {
    mAppToken = appToken;
    mAppName = appName;
    mHardwareAccelerated = hardwareAccelerated;
    if (wm == null) {
        wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
    }
    // 这里创建了windowManager
    mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

这个方法里首先会获取到应用服务的WindowManager(实现类也是WindowManagerImpl),然后通过这个应用服务的WindowManager创建了新的windowManager。

从这里可以看到是利用系统服务的windowManager来创建新的windowManagerImpl,因而这个应用所有的WindowManagerImpl都是同个内核windowManager,而创建出来的仅仅是包了个壳。

这样PhoneWindow和WindowManagerImpl就绑定在一起了。Activity可以通过WindowManagerImpl来操作PhoneWindow。

到这里Activity的PhoneWindow和WindowManagerImpl对象就创建完成了,接下来是如何把Activity的布局文件设置给PhoneWindow。在上面我讲到调用Activity的attach方法之后,会回调Activity的onCreate方法,在onCreate方法我们会调用setContentView来设置布局,如下:

public void setContentView(View view, ViewGroup.LayoutParams params) {
    getWindow().setContentView(view, params);
    initWindowDecorActionBar();
}

这里的getWindow就是获取到我们上面创建的PhoneWindow对象。我们继续看下去:

// 注意他有多个重载的方法,要选择参数对应的方法
public void setContentView(int layoutResID) {
    // 创建DecorView
    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 {
        // 这里根据布局id加载布局
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        // 回调activity的方法
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

同样我们只看重点代码:

  • 首先看decorView创建了没有,没有的话创建DecorView

  • 把布局加载到DecorView中

  • 回调Activity的callBack方法

这里补充一下什么是DecorView。DecorView是在PhoneWindow中预设好的一个布局,这个布局长这样:

他是一个垂直排列的布局,上面是ActionBar,下面是ContentView,他是一个FrameLayout。我们的Activity布局就加载到ContentView里进行显示。所以Decorview是Activity布局最顶层的viewGroup。

然后我们看一下怎么初始化DercorView的:

private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        // 这里创建了DecorView
        mDecor = generateDecor(-1);
        ...
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        // 对DecorView进行初始化,得到ContentView
        mContentParent = generateLayout(mDecor);
        ...
    }
}

installDecor方法中主要是新建一个DecorView对象,然后加载预设好的布局对DecorView进行初始化,(预设好的布局就是上面讲述的布局)并获取到这个预设布局的ContentView。

好了然后我们再回到window的setContentView方法中,初始化了DecorView之后,把Activity布局加载到DecorView的ContentView中如下代码:

// 注意他有多个重载的方法,要选择参数对应的方法
public void setContentView(int layoutResID) {
    ...
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        // 这里根据布局id加载布局
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    ...
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        // 回调activity的方法
        cb.onContentChanged();
    }
}

所以可以看到Activitiy的布局确实是添加到DecorView的ContentView中,这也是为什么onCreate中使用的是setContentView而不是setView。最后会回调Activity的方法告诉Activity,DecorView已经创建并初始化完成了。

到这里DecorView创建完成了,但还缺少了最重要的一步:把DecorView作为window添加到屏幕上。从前面的介绍我们知道添加window需要用到WindowManagerImpl的addView方法。这一步是在ActivityThread的handleResumeActivity方法中被执行:

public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
        String reason) {
    // 调用Activity的onResume方法
    final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
    ...
    // 让decorView显示到屏幕上
    if (r.activity.mVisibleFromClient) {
        r.activity.makeVisible();
    }

这一步方法有两个重点:回调onResume方法,把decorView添加到屏幕上。我们看一下makeVisible方法做了什么:

void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}

是不是非常熟悉?直接调用WindowManagerImpl的addView方法来吧decorView添加到屏幕上,至此,我们的Activity界面就会显示在屏幕上了。

好了,这部分很长,最后来总结一下:

  • 从Activity的启动流程可以得到Activity创建Window的过程

  • 创建PhoneWindow -> 创建WindowManager -> 创建decorView -> 利用windowManager把DecorView显示到屏幕上

  • 回调onResume方法的时候,DecorView还没有被添加到屏幕,所以当onResume被回调,指的是屏幕即将到显示,而不是已经显示

从Android架构角度看Window

前面我们介绍过关于PhoneWindow和window之间的关系,了解到PhoneWindow其实不是Window,只是一个window容器。不知读者有没想过一个问题,为什么谷歌要建一个不是window但却名字是window的类?是故意要迷惑我们吗?要了解这个问题,我们先来回顾一下整个android的window机制结构。

首先从WindowManagerService开始,我们知道WMS是window的最终管理者,在WMS中为每一个应用持有一个session,关于session前面我们讲过,每个应用都是全局单例,负责和WMS通信的binder对象。WMS为每个window都建立了一个windowStatus对象,同一个应用的window使用同个session进行跨进程通信,结构大概如下:

而负责与WMS通信的,是viewRootImpl。前面我们讲过每个view树即为一个window,viewRootImpl负责和WMS进行通信,同时也负责view的绘制。如果把上面的图画仔细一点就是:

图中每一个windowStatus对应一个viewRootImpl,WMS通过viewRootImpl来控制view。这也就是window机制的管理结构。当我们需要添加window的时候,最终的逻辑实现是WindowManagerGlobal,他的内部使用自己的session创建一个viewRootImpl,然后向WMS申请添加window,结构图大概如下:

windowManagerGlobal使用自己的IWindowSession创建viewRootImpl,这个IWindowSession是全局单例。viewRootImpl和WMS申请创建window,然后WMS允许之后,再通知viewRootImpl绘制view,同时WMS通过windowStatus存储了viewRootImpl的相关信息,这样如果WMS需要修改view,直接通过viewRootImpl就可以修改view了。

从上面的描述中可以发现我全程没有提及到PhoneWindow和WindowManagerImpl。这是因为他们不属于window机制内的类,而是封装于window机制之上的框架。

假设如果没有PhoneWindow和WindowManager我们该如何添加一个window?

首先需要调用WindowGlobal获取session,再创建viewRootImpl,再访问wms,然后再利用viewRootImpl绘制view,是不是很复杂,而这仅仅只是整体的步骤。

而WindowManagerImpl正是这个功能。他内部拥有WindowManagerGlobal的单例,然后帮助我们完成了这一系列的步骤。同时,windowManagerImpl也是只有一个实例,其他的windowManagerImpl都是建立在windowManagerImpl单例上。这一点在前面有通过源码介绍到。

另外,上面我讲到PhoneWindow并不是window而是一个window容器,那为什么他不要命名为windowContainer呢?

PhoneWindow这个类是谷歌给window机制进行更上一层的封装。PhoneWindow内部拥有一个DecorView,我们的布局view都是添加到decorView中的,因为我们可以通过给decorView设置背景,宽高度,标题栏,按键反馈等等,来间接给我们的布局view设置。

当我们通过windowManagrImpl添加window的时候,都会把创建的window和PhoneWindow联系起来,让PhoneWindow可以统一处理通过windowManagerImpl添加的window,这点上面有讲到。这样一来,PhoneWindow的存在,向开发者屏蔽真正的window,暴露给开发者一个“存在的”window。

我们可以认为PhoneWindow就是一个window,window是view容器。当我们需要在屏幕上添加view的时候,只需要获得应用window对应的windowManagerImpl,然后直接调用addView方法添加view即可。

这里也可以解释为什么windowManager的接口方法是addView而不是addWindow,一个是window确实是以view的存在形式没错,二是为了向开发者屏蔽真正的window,让我们以为是在往window中添加view。画个图如下:

黄色部分输于谷歌提供给开发者的window框架,而绿色是真正的window机制结构。通过谷歌的框架我们可以很方便地管理屏幕上的view,而不须了解底层究竟是如何工作的。PhoneWindow的存在,更是让window的“可见性”得到了实现,让window变成了一个“view容器”。

好了最后来总结一下:

  • Android内部的window机制与谷歌暴露给我们的api是不一样的,谷歌封装的目的是为了让我们更好地使用window。

  • dialog、popupWindow等框架更是对具体场景进行更进一步的封装。

  • 我们在了解window机制的时候,需要跳过应用层,看到window的本质,才能更好地帮助我们理解window。

  • 在android的其他地方也是一样,利用封装向开发者屏蔽底层逻辑,让我们更好地运用。但如果我们需要了解他的机制的时候,就需要绕过这层封装,看到本质。

 

转载:https://mp.weixin.qq.com/s/Q9HeT39w0LXGoR_ifyd9bg

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值