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 条评论
登录 后参与评论

相关文章

来自专栏流媒体

Activity启动流程源码分析

launcher就是android桌面应用程序。也是操作系统启动有第一个app。同时作为其他app的入口。我们找到其源码 android-6.0.0_r1\p...

951
来自专栏跟着阿笨一起玩NET

DBHelper数据库操作类(一)

可以参考的:http://www.oschina.net/code/snippet_4946_748

391
来自专栏何俊林

FFmpeg总结(十一)用ffmpeg进行转格式,Android下播放网络音频流

图:杭州西湖 思路: 1、mp3转成pcm(音频数据),ffmpeg做的事 2、OpenSL ES引擎创建AudioPlayer,实际调用了AudioTra...

4075
来自专栏全栈之路

android 实现本地定时推送(兼容)

首先写几点感悟: - 做兼容真的很累很费劲~ - android 8.0 广播部分不再支持动态注册,所以应该用service来实现定时推送功能 - 无论是闹钟还...

1632
来自专栏Android开发经验

常用的代码片段,不断更新

1232
来自专栏项勇

[Android笔记7]之通过DatePickerDialog,TimePickerDialog调用系统时间设置

2513
来自专栏码匠的流水账

spring 5 webclient使用指南

之前写了一篇restTemplate使用实例,由于spring 5全面引入reactive,同时也有了restTemplate的reactive版webclie...

1352
来自专栏后端之路

android 4.0 启动 Launcher 分析(1)

1.配置文件 (1)packege 属性可指定生成的gen源文件夹的包名,同时也表示程序运行时的进程名称 original-package 表示源码中真实的源...

1869
来自专栏游戏杂谈

cocos2d-x 2.x版本接入bugly的总结

最开始项目使用的是自己DIY的很简陋的上报系统,后来改成google breakpad来上报,发现其实都做的不太理想,游戏引擎因为版本历史问题存在一些崩溃问题。...

420
来自专栏向治洪

仿微信聊天表情发送

如题,这是公司项目的一个功能模块,先上个效果图: ? 其次大致说说原理: 1,首先判断输入的字符,是否包含表情的文字,比如   ?  这个表情对应的文件名为 e...

3517

扫码关注云+社区