Android View和 Window 的关系

导语 本文主要介绍了Android中Window和View的关系,并在用户进程中说明了View的加载过程,最后再简单认识了一下WMS控制、显示Window的主要步骤。

1、架构

在Android中,我们知道Activity是由中心控制器ActivityManagerService来管理控制的。和这Activity类似,UI层的内容是由另一个控制器WindowManagerService(WMS)来管理的。其主要功能模块分层架构如下图:

UI显示的涉及很复杂,图中我们可以看到最底层是硬件相关的,往上是屏幕的任何显示的缓存区FrameBuffer。但是WMS不直接操作FrameBuffer,它是通过SurfaceFlinger和FrameBufferNativeWindow等库来间接进行的,这部分不是本文的内容,所以不细讲了。

再往上就是Framework和Application层,可以看到Android的任意View都通过Window间接的被WMS管理。

2、Window分类

根据权限的不同或者说z轴层级的高低来分,Window有3类,应用层Window、子Window、系统Window。应用层Window对应的比如说Activity,而子Window必须附着在父Window上,如Dialog、PopupWindow。系统Window有如Toast、System Alert等。其层级对应区间如下:

应用层Window: 1 - 99

子Window: 1000 - 1999

系统Window: 2000 - 2999

毫无疑问,层级越高的显示的越靠上。我们可以看一下WindowManager对层级的部分声明。

@WindowManager

        public static final int TYPE_BASE_APPLICATION   = 1;
        /**
         * Window type: a normal application window.  The {@link #token} must be
         * an Activity token identifying who the window belongs to.
         * In multiuser systems shows only on the owning user's window.
         */
        public static final int TYPE_APPLICATION        = 2;
        /**
         * End of types of application windows.
         */
        public static final int LAST_APPLICATION_WINDOW = 99;
        /**
         * Start of types of sub-windows.  The {@link #token} of these windows
         * must be set to the window they are attached to.  These types of
         * windows are kept next to their attached window in Z-order, and their
         * coordinate space is relative to their attached window.
         */
        public static final int FIRST_SUB_WINDOW = 1000;
        /**
         * Window type: a panel on top of an application window.  These windows
         * appear on top of their attached window.
         */
        public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;

以PopupWindow举例,可以发现它的默认type确实是SUB_WINDOW

private int mWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
  private WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
        。。。
        p.type = mWindowLayoutType;
        。。。
}

3、View在用户进程的加载

当我们想在桌面上增加一个按钮的时候,通常会这么做

Button button = new Button(this);
        button.setText("hello");
        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT
        );
        layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;
        windowManager.addView(button, layoutParams);

这样在桌面上,我们就能看到一个在顶层的常驻的button。

最开始说了,我们所以的View都是以Window的方式存在。这个的button对应的Window在哪儿呢?

@WindowManagerImpl

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

可以看到这个button是附着在mParentWindow的Window上,而这个mParentWindow怎么来的呢,它其实是当前Resume的Activity的Window,这块后面还会细讲。这就意味着,这个button并不是悬浮在屏幕顶层的,其实是从一个Activity上搬到另一个Activity的。

好了,有了View的展示确实是把Window当载体的这个认知的话,我们继续以Activity的展示做更详细的解释。

在Activity中,我们会在onCreate中调用setContentView来写入我们的layout。

Activity#setContentView

public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

这里其实是把layoutId交给Activity的Window处理,而这个Window是在Activity launch的时候初始化的。

final void attach(。。。) {
        mWindow = new PhoneWindow(this, window);
        。。。
}

可以看到Activity对应载体是PhoneWindow,我们看看PhoneWindow的setContentView方法

PhoneWindow#setContentView

@Override

    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            installDecor();
        } 
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        。。。
    }

这块做的操作是先载入DecorView,再把layout inflate到mContentParent中。这几者的关系我大家可以认识一下:

图中我们可以看到,每个Window必定有一个DecorView。而在DecorView中,每种Window的view可能会不一样。

在Activity中,UI的主要3步为:

1、DecorView会先设置System Layout的一些属性,比如加载预设的主题风格,就解析对应的系统的layout,然后设置是否包含title等等。

2、然后把系统layout中的android.R.id.content所对应的ViewGroup赋给mContentParent。

3、最后把开发者的layout Inflate到mContentParent,完成整个Activity的view的载入和初始化。

其他的Window就不介绍了,都是大同小异的。

以上步骤都是在onCreate中完成的,WMS还没有真正的把它显示出来,这一步是在onResume中完成的。

从之前的Activity启动文中 《Android 7.0中Launcher启动Activity过程》我们知道,Activity的resume的时候会跳到ActivityThread中的handleResumeActivity方法中,我们看看其中与Window相关的内容

ActivityThread#handleResumeActivity

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
            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) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);
                }
            }
    }

可以看到在resume的时候,先会获取到Activity的Window,然后再获取其DecorView,并把它的type设为TYPE_BASE_APPLICATION,也是文章上面说的最低级别的那一层。最后通过WindowManger把DecorView add上去。

但是还有一个问题没有得到解释,就是本节的最开始的例子,说的是System Alert是怎么跟随Activity的?

刚才提到对于系统Alert,WindowManager会把它add到mParentWindow,最开始我们提到这个mParentWindow是跟随Resume Activity的。我们简单说一下原理,

Activity在attach的时候会调这么个方法

mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
 public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        。。。
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }
 private WindowManagerImpl(Context context, Window parentWindow) {
        mContext = context;
        mParentWindow = parentWindow;
    }

    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow);
    }

从上面代码我们就可以明显的看清楚它的路径,每次在Activity attach的时候,都会改变WindowManager中的mParentWindow,这样就达到了系统Alert看上去是悬浮顶部不动的“幻象”。

3、WMS显示View过程简介

在前两个例子中,我们都是走到WindowManager.addView了,接下来看看WMS是怎么处理的。

WindowManagerGlobal#addView

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        。。。
        root.setView(view, wparams, panelParentView);
    }

里面核心的一段是调用ViewRoot的setView方法。这里简单的提一下ViewRoot,它的本质不是一个view,而是一个Handler,它是Application进程和WMS的桥梁。

ViewRootImpl#addView

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
          requestLayout();
           try {
               res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
           }
}

这个里面有两个主要的方法,requestLayout和addToDisplay。这两个方法主要做的事有:

requestLayout:通知Surface更新并绘制。

addToDisplay:准备并建立要更新的Window。

大家可能会很好奇,为什么绘制会先于准备呢?因为requestLayout中,setView是在主线程的一个消息中执行的,而requestLayout方法中,ViewRoot会发送一个绘制的message,但是这个message是和主线程共用一个MessageQueue的。因此虽然requestLayout其先调用,但是绘制消息还是需要等setView的消息执行完毕后才能进行,因此真的绘制过程还是后于addToDisplay。(这块也很合理的解释了,为什么子线程不能操作UI,操作了就真的混乱了)

因此我们先看看WMS的准备工作addToDisplay吧

mWindowSession是用户进程和WMS的一个会话层,是一个桥梁。

Session#addToDisplay

public int addToDisplay(。。。) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                outContentInsets, outStableInsets, outOutsets, outInputChannel);
    }

WMS#addWindow

public int addWindow(。。。) {
   addWindowToListInOrderLocked(win,true);
   。。。
   assignLayersLocked(displayContent.getWindowList());
}

这部分顾名思义,把Window按照一定的顺序进行排序(主序和子序等等),并把它加到list中。然后根据窗口的主序和列表的位置最终确定其显示的layer。这部分由于篇幅有限,就不深入了,后面有时间再开一文讲一讲Window的排序、层级规则,还是比较有趣的。

Window都已经准备好了后,可以绘制了,我们回过头看看requestLayout。

ViewRootImpl#scheduleTraversal

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        }
}

可以看到,这里post了一个Runnable,而这个Runnable最终会跑到这

ViewRootImpl#performTraversal

private void performTraversals(){
     。。。
     relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
}
 private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
            boolean insetsPending) throws RemoteException {
        int relayoutResult = mWindowSession.relayout(
                mWindow, mSeq, params,
                (int) (mView.getMeasuredWidth() * appScale + 0.5f),
                (int) (mView.getMeasuredHeight() * appScale + 0.5f),
                viewVisibility, insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0,
                mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
                mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingConfiguration,
                mSurface);
    }

可以看得,最终的绘制调用了Session的relayout。而在WMS中,Session会调用relayoutWindow

public int relayoutWindow(Session session, IWindow client, int seq,
            WindowManager.LayoutParams attrs, int requestedWidth,
            int requestedHeight, int viewVisibility, int flags,
            Rect outFrame, Rect outOverscanInsets, Rect outContentInsets,
            Rect outVisibleInsets, Rect outStableInsets, Rect outOutsets, Rect outBackdropFrame,
            Configuration outConfig, Surface outSurface){
        mWindowPlacerLocked.performSurfacePlacement();
}

后续的步骤主要是计算Window大小,并把之交给Surface处理了,这部分本文就不做深入了。

4、总结

本文主要介绍了Android中Window和View的关系,并在用户进程中说明了View的加载过程,最后再简单认识了一下WMS控制、显示Window的主要步骤。

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏曾大稳的博客

ffmpeg添加水印和滤镜效果

更多的特效使用: http://www.ffmpeg.org/ffmpeg-filters.html

923
来自专栏C++

FFmpeg4.0笔记:file2rtmp

975
来自专栏一个会写诗的程序员的博客

LoggerFactory is not a Logback LoggerContext but Logback is on the classpath. Either remove Logba...

LoggerFactory is not a Logback LoggerContext but Logback is on the classpath. Ei...

895
来自专栏Python

利用ForgeryPy生成虚拟数据

 在程序研发过程中,我们往往需要大量的虚拟实验数据。Python中有多个包可以用于生成虚拟数据,其中功能较为完善的是ForgeryPy。

590
来自专栏转载gongluck的CSDN博客

利用FFmpeg对火眼一体摄像机的回调数据进行处理:YUV转H264,H264封装flv,所有输入都是在内存中。

整个工程代码下载地址 http://download.csdn.net/download/gongluck93/10175326 Code //#define ...

4866
来自专栏10km的专栏

cuda8+cuDNN Faster R-CNN安装塈运行demo

安装cuda cuda8安装参见网上教程 安装cuDNN py-faster-rcnn/caffe-fast-rcnn目前不支持cuDNN5。 如果使用cu...

2616
来自专栏曾大稳的博客

结合ffmpeg用SDL播放YUV实现简易播放器

通过解码之后得到的yuv视频数据我们直接可以进行播放,本篇使用SDL来实现视频播放。

451
来自专栏hightopo

原 基于HTML5的WebGL设计汉诺塔3

1297
来自专栏hightopo

HT for Web 3D游戏设计设计--汉诺塔(Towers of Hanoi)

882
来自专栏技术博文

php QR Code二维码生成类

<?php /* * PHP QR Code encoder * * This file contains MERGED version of PHP ...

3445

扫码关注云+社区