Android窗口框架(View\Window\WindowManager)

View

1. 生命周期

在xml中声明自定义View
在这里插入图片描述

2. View的测量流程

View的定义及MeasureSpec

View是一个矩形区域,有自己的位置、大小和边距
View的大小最终由子View的LayoutParams和父View的测量要求公共决定。
MeasureSpec就是源码中用于保存测量要求的32位int值(2位测量模型、30位测量size)

顶级View(DecorView)的MeasureSpec由窗口大小和自身的LayoutParams确定,普通View的MeasureSpec由父容器的Measure和自身的LayoutParams决定

MeasureSpec的两种组成:

  • 2位 测量模式,分为3种:UNSPECIFIED、EXACTLY、AT_MOST
public static class MeasureSpec {
    
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    
    //父View不对子View做任何限制,需要多大给多大,这种情况一般用于系统内部,表示一种测量的状态
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    
    //父View已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值,它对应LayoutParams中的match_parent和具体数值这两种模式
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    
    //父View给子VIew提供一个最大可用的大小,子View去自适应这个大小。
    public static final int AT_MOST     = 2 << MODE_SHIFT;  
}
  • 30位 特定测量模式下的大小
    测量规则:
  • 当View采用固定宽高,不管父容器的MeasureSpec是什么,resultSize都是指定的宽高,resultMode都是MeasureSpec.EXACTLY。
  • 当View的宽高是match_parent,当父容器是MeasureSpec.EXACTLY,则View也是MeasureSpec.EXACTLY,并且其大小就是父容器的剩余空间。当父容器是MeasureSpec.AT_MOST 则View也是MeasureSpec.AT_MOST,并且大小不会超过父容器的剩余空间。
  • 当View的宽高是wrap_content,View的模式总是MeasureSpec.AT_MOST,并且大小都不会超过父类的剩余空间。

3. View的onMeasure函数

通过setMeasureDimension设置View宽高的测量值,其中利用getDefaultSize()来获取测量的大小

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
           setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                   getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
       }
//设置View宽高的测量值
       protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
       
       //measureSpec指的是View测量后的大小
       public static int getDefaultSize(int size, int measureSpec) {
           int result = size;
           int specMode = MeasureSpec.getMode(measureSpec);
           int specSize =  MeasureSpec.getSize(measureSpec);
   
           switch (specMode) {
           //MeasureSpec.UNSPECIFIED一般用来系统的内部测量流程
           case MeasureSpec.UNSPECIFIED:
               result = size;
               break;
           //我们主要关注着两种情况,它们返回的是View测量后的大小
           case MeasureSpec.AT_MOST:
           case MeasureSpec.EXACTLY:
               result = specSize;
               break;
           }
           return result;
       }

Measure的整个流程完成以后,可以通过getMeasuredWidth()与getMeasuredHeight()来获得View的宽高。但是在某些情况下,系统需要经过多次Measure才能确定最终的宽高,因此在onMeasure()方法中拿到的宽高很可能是不正确的,比较好的做法是在onLayout()方法中获取View的宽高。

4. View的布局流程

layout()方法用来确定View本身的位置,onLayout()方法用来确定子元素的位置。
onLayout的实现依赖于具体的布局,所以View/ViewGroup并没有实现这个方法,下面是FrameLayout中onLayout的实现。
其中

  • (left, top,right, bottom)描述的是当前视图的坐标。
  • (mPaddingLeft,mPaddingRight,mPaddingTop,mPaddingBottom
    )描述当前视图内容区域padding
  • (mForegroundPaddingLeft,mForegroundPaddingRight,mForegroundPaddingTop,mForegroundPaddingBottom)表示当前视图的各个子视图所围成的区域当前视图内容区域的padding。
    因此该方法的onLayout最后得到的位置信息是对比父View的相对坐标
public class FrameLayout extends ViewGroup {
    /**
	int left, int top, int right, int bottom: 描述的是当前视图的坐标。
	(mPaddingLeft,mPaddingRight,mPaddingTop,mPaddingBottom )描述当前视图内容区域padding   	(mForegroundPaddingLeft,mForegroundPaddingRight,mForegroundPaddingTop,mForegroundPaddingBottom)表示当前视图的各个子视图所围成的区域当前视图内容区域的padding。
    **/
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    		//子View个数
            final int count = getChildCount();
    		//本View(Parent)的子View其内容区域的padding
            final int parentLeft = mPaddingLeft + mForegroundPaddingLeft;
            final int parentRight = right - left - mPaddingRight - mForegroundPaddingRight;
    
            final int parentTop = mPaddingTop + mForegroundPaddingTop;
            final int parentBottom = bottom - top - mPaddingBottom - mForegroundPaddingBottom;
    
            mForegroundBoundsChanged = true;
            
            for (int i = 0; i < count; i++) {
                final View child = getChildAt(i);
                if (child.getVisibility() != GONE) {
                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    
                    final int width = child.getMeasuredWidth();
                    final int height = child.getMeasuredHeight();
    
                    int childLeft = parentLeft;
                    int childTop = parentTop;
    
                    final int gravity = lp.gravity;
    
                    if (gravity != -1) {
                        final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
                        final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
    
                        switch (horizontalGravity) {
                            case Gravity.LEFT:
                                childLeft = parentLeft + lp.leftMargin;
                                break;
                            case Gravity.CENTER_HORIZONTAL:
                                childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                                        lp.leftMargin - lp.rightMargin;
                                break;
                            case Gravity.RIGHT:
                                childLeft = parentRight - width - lp.rightMargin;
                                break;
                            default:
                                childLeft = parentLeft + lp.leftMargin;
                        }
    
                        switch (verticalGravity) {
                            case Gravity.TOP:
                                childTop = parentTop + lp.topMargin;
                                break;
                            case Gravity.CENTER_VERTICAL:
                                childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                                        lp.topMargin - lp.bottomMargin;
                                break;
                            case Gravity.BOTTOM:
                                childTop = parentBottom - height - lp.bottomMargin;
                                break;
                            default:
                                childTop = parentTop + lp.topMargin;
                        }
                    }
    
                    child.layout(childLeft, childTop, childLeft + width, childTop + height);
                }
            }
        }
}

5. View绘制流程

绘制过程从ViewRoot.draw开始
这个函数主要做了以下事情:

  • 调用Scroller.computeScrollOffset()方法计算应用是否处于滑动状态,并获得应用窗口在Y轴上的即时滑动位置yoff。
  • 根据AttachInfo里描述的数据,判断窗口是否需要缩放。 根据成员变量React mDirty的描述来判断窗口脏区域的大小,脏区域指的是需要全部重绘的窗口区域。 (有dirty才是需要重绘制的地方)
  • 根据成员变量boolean mUserGL判断是否需要用OpenGL接口来绘制UI
  • 如果不是用OpenGL来绘制,则用Surface来绘制,先调用Surface.lockCanvas()来创建画布,UI绘制完成后,再调用urface.unlockCanvasAndPost(canvas)S来请求SurfaceFlinger进行UI的渲染
if (!dirty.isEmpty() || mIsAnimating) {
                Canvas canvas;
                try {
                    int left = dirty.left;
                    int top = dirty.top;
                    int right = dirty.right;
                    int bottom = dirty.bottom;
                    //调用Surface.lockCanvas()来创建画布
                    canvas = surface.lockCanvas(dirty);
    
                    if (left != dirty.left || top != dirty.top || right != dirty.right ||
                            bottom != dirty.bottom) {
                        mAttachInfo.mIgnoreDirtyState = true;
                    }
    
                    // TODO: Do this in native
                    canvas.setDensity(mDensity);
                } catch (Surface.OutOfResourcesException e) {
                    Log.e(TAG, "OutOfResourcesException locking surface", e);
                    // TODO: we should ask the window manager to do something!
                    // for now we just do nothing
                    return;
                } catch (IllegalArgumentException e) {
                    Log.e(TAG, "IllegalArgumentException locking surface", e);
                    // TODO: we should ask the window manager to do something!
                    // for now we just do nothing
                    return;
                }
    
                try {
                    if (!dirty.isEmpty() || mIsAnimating) {
                        long startTime = 0L;
    
                        if (DEBUG_ORIENTATION || DEBUG_DRAW) {
                            Log.v(TAG, "Surface " + surface + " drawing to bitmap w="
                                    + canvas.getWidth() + ", h=" + canvas.getHeight());
                            //canvas.drawARGB(255, 255, 0, 0);
                        }
    
                        if (Config.DEBUG && ViewDebug.profileDrawing) {
                            startTime = SystemClock.elapsedRealtime();
                        }
    
                        // If this bitmap's format includes an alpha channel, we
                        // need to clear it before drawing so that the child will
                        // properly re-composite its drawing on a transparent
                        // background. This automatically respects the clip/dirty region
                        // or
                        // If we are applying an offset, we need to clear the area
                        // where the offset doesn't appear to avoid having garbage
                        // left in the blank areas.
                        if (!canvas.isOpaque() || yoff != 0) {
                            canvas.drawColor(0, PorterDuff.Mode.CLEAR);
                        }
    
                        dirty.setEmpty();
                        mIsAnimating = false;
                        mAttachInfo.mDrawingTime = SystemClock.uptimeMillis();
                        mView.mPrivateFlags |= View.DRAWN;
    
                        if (DEBUG_DRAW) {
                            Context cxt = mView.getContext();
                            Log.i(TAG, "Drawing: package:" + cxt.getPackageName() +
                                    ", metrics=" + cxt.getResources().getDisplayMetrics() +
                                    ", compatibilityInfo=" + cxt.getResources().getCompatibilityInfo());
                        }
                        int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
                        try {
                        //因为有滚动,画布朝着y方向偏移
                            canvas.translate(0, -yoff);
                            if (mTranslator != null) {
                                mTranslator.translateCanvas(canvas);
                            }
                            canvas.setScreenDensity(scalingRequired
                                    ? DisplayMetrics.DENSITY_DEVICE : 0);
                            mView.draw(canvas);
                        } finally {
                            mAttachInfo.mIgnoreDirtyState = false;
                            canvas.restoreToCount(saveCount);
                        }
    
                        if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) {
                            mView.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_DRAWING);
                        }
    
                        if (SHOW_FPS || Config.DEBUG && ViewDebug.showFps) {
                            int now = (int)SystemClock.elapsedRealtime();
                            if (sDrawTime != 0) {
                                nativeShowFPS(canvas, now - sDrawTime);
                            }
                            sDrawTime = now;
                        }
    
                        if (Config.DEBUG && ViewDebug.profileDrawing) {
                            EventLog.writeEvent(60000, SystemClock.elapsedRealtime() - startTime);
                        }
                    }
    
                } finally {
                    //UI绘制完成后,调用urface.unlockCanvasAndPost(canvas)S来请求SurfaceFlinger进行UI的渲染
                    surface.unlockCanvasAndPost(canvas);
                }
            }
    
            if (LOCAL_LOGV) {
                Log.v(TAG, "Surface " + surface + " unlockCanvasAndPost");
            }
    
            if (scrolling) {
                mFullRedrawNeeded = true;
                scheduleTraversals();
            }
        }

draw函数过程

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *           - final Drawable background = mBGDrawable;
         *           - background.draw(canvas);
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content  
         *           - if (!dirtyOpaque) onDraw(canvas); 内容不透明开始绘制
         *      4. Draw children      
         * 			 - dispatchDraw(canvas); 
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)  
         * 			- onDrawScrollBars(canvas);
         */

6.渲染流程总结

至此,Android应用程序窗口的渲染流程就分析完了,我们再来总结一下。

渲染Android应用视图的渲染流程,测量流程用来确定视图的大小、布局流程用来确定视图的位置、绘制流程最终将视图绘制在应用窗口上。
Android应用程序窗口UI首先是使用Skia图形库API来绘制在一块画布上,实际地是绘制在这块画布里面的一个图形缓冲区中,这个图形缓冲区最终会被交给SurfaceFlinger服 务,而SurfaceFlinger服务再使用OpenGL图形库API来将这个图形缓冲区渲染到硬件帧缓冲区中。

7.View的事件分发

View的事件分发是面试常考的知识点,当发生点击事件,最先传给Activity,然后Activity会交给Window处理调用superDispatchTouchEvent()方法,然后传到ViewGroup中。事件分发机制可以用下面代码表示:

public boolean dispatchTouchEvent(MotionEvent event){
    boolean consume = false;
    //父View决定是否拦截事件
    if(onInterceptTouchEvent(event)){
        //父View调用onTouchEvent(event)消费事件
        consume = onTouchEvent(event);
    }else{
        //调用子View的dispatchTouchEvent(event)方法继续分发事件
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
}

onInterceptTouchEvent(event)=true则调用onTouchEvent(event)消费事件。false则调用child.dispatchTouchEvent(event)继续分发

  • 事件的传递是按照Activity -> Window -> ViewGroup ->View的顺序进行的
  • 一般情况下,一个事件序列只能由一个View拦截并消耗,一旦一个View拦截了该事件,则该事件序列的后续事件都会交由该View来处理。
  • ViewGroup默认不拦截任何事件
  • View没有onInterceptTouchEvent()方法,一但有点击事件传递给它,它的ouTouchEvent()方法就会被调用。

Window

1. 窗口类型

Window管理View,WindowManager管理Window

  • 应用Window:z-index在1~99之间,对应一个Activity
  • 子Window:z-index在1000~1999,需要依附父Window,如Dialog
  • 系统Window:z-index在2000~2999,如Toast、状态栏、系统音量和错误提示框等

窗口参数

在WindowManager里定义了一个LayoutParams内部类,它描述了窗口的参数信息

  • public int x:窗口x轴坐标
  • public int y:窗口y轴坐标
  • public int type:窗口类型
  • public int flags:窗口属性
  • public int softInputMode:输入法键盘模式

关于窗口属性,它控制着窗口的行为,举几个常见的:

  • FLAG_ALLOW_LOCK_WHILE_SCREEN_ON 只要窗口可见,就允许在开启状态的屏幕上锁屏
  • FLAG_NOT_FOCUSABLE 窗口不能获得输入焦点,设置该标志的同时FLAG_NOT_TOUCH_MODAL也会被设置
  • FLAG_NOT_TOUCHABLE **窗口不接收任何触摸事件 **
  • FLAG_NOT_TOUCH_MODAL 在该窗口区域外的触摸事件传递给其他的Window,而自己只会处理窗口区域内的触摸事件
  • FLAG_KEEP_SCREEN_ON 只要窗口可见,屏幕就会一直亮着
  • FLAG_LAYOUT_NO_LIMITS 允许窗口超过屏幕之外
  • FLAG_FULLSCREEN 隐藏所有的屏幕装饰窗口,比如在游戏、播放器中的全屏显示
  • FLAG_SHOW_WHEN_LOCKED 窗口可以在锁屏的窗口之上显示
  • FLAG_IGNORE_CHEEK_PRESSES 当用户的脸贴近屏幕时(比如打电话),不会去响应此事件
  • FLAG_TURN_SCREEN_ON 窗口显示时将屏幕点亮

2. 窗口实现

Window是一个抽象类,它的唯一实现类是PhoneWindow,PhoneWindow里包含了以下内容:

  • private DecorView mDecor:DecorView是Activity中的顶级View,它本质上是一个FrameLayout,一般说来它内部包含标题栏和内容栏(com.android.internal.R.id.content)。
  • ViewGroup mContentParent:窗口内容视图,它是mDecor本身或者是它的子View。
  • 图标
    private ImageView mLeftIconView:左上角图标
    private ImageView mRightIconView:右上角图标
    private ProgressBar mCircularProgressBar:圆形loading条
    private ProgressBar mHorizontalProgressBar:水平loading条
    其他的一些和转场动画相关的Transition与listener

setContentView()就是在PhoneWindow中实现的

WindowUtils工具类

WindowManager

对Window进行添加、删除和更新,申请分配Surface的过程,由WindowServiceManager管理,有序地将Window显示在屏幕上。窗口的操作被定义WindowManager中,WindowManager是一个接口,继承于ViewManager,实现类是WindowManagerImpl,实际上我们常用的功能,也是定义在ViewManager里的。

public interface ViewManager{
    //添加View
    public void addView(View view, ViewGroup.LayoutParams params);
    //更新View
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    //删除View
    public void removeView(View view);
}

WindowManager可以通过Context来获取,WindowManager也会和其他服务一样在开机时注册到ContextImpl里的map容器里,然后通过他们的key来获取。

windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);

在Android开发中,Window是所有视图的载体,如Activity,Dialog和Toast的视图,我们想要对Window进行添加和删除就要通过WindowManager来操作,而WindowManager就是通过Binder与WindowManagerService进行跨进程通信,把具体的实现工作交给WindowManagerService。
WindowManager的实现类是WindowManagerImpl,在WindowManagerImpl内部实际的功能是有WindowManagerGlobal来完成的。Android的各种服务都是基于C/S结构来设计的,系统层提供服务,应用层使用服务。
WindowManager也是一样,它与WindowManagerService的通信是通过WindowSession来完成的。
在这里插入图片描述

1. 基础概念

SurfaceFlinger (Flinger有护圈的意思)是一个独立的Service, 它接收所有Window的Surface作为输入,根据层级(Z-Order)、透明度,大小,位置等参数,计算出每个Surface在最终合成图像中的位置,然后交由生成最终的显示Buffer, 然后显示到特定的显示设备上。

Surface—在Android中,Window与Surface一一对应,如果说Window关心的是层次和布局,是从设计者角度定义的类,Surface则从实现角度出发,是工程师关心和考虑的类。Window的内容是变化的,Surface需要有空间来记录每个时刻Window的内容。在Android的SurfaceFlinger实现里,通常一个Surface有两块 Buffer, 一块用于绘画,一块用于显示,两个Buffer按照固定的频率进行交换,从而实现Window的动态刷新。

Layer——Layer是SurfaceFlinger 进行合成的基本操作单元。Layer在应用请求创建Surface的时候在SurfaceFlinger内部创建,因此一个Surface对应一个 Layer, 但注意,Surface不一定对应于Window,Android中有些Surface并不跟某个Window相关,而是有程序直接创建,比如说 SurfaceView, 用于显示有硬件输出的视频内容等。
当多个Layer进行合成的时候,并不是整个Layer的空间都会被完全显示,根据这个Layer最终的显示效果,一个Layer可以被划分成很多的Region, Android SurfaceFlinger 定义了以下一些Region类型:
TransparantRegion: 完全透明的区域,在它之下的区域将被显示出来。
OpaqueRegion: 完全不透明的区域,是否显示取决于它上面是否有遮挡或是否透明。
VisibleRegion: 可见区域,包括完全不透明无遮挡区域或半透明区域
visibleRegion = Region - above OpaqueRegion.
CoveredRegion: 被遮挡区域,在它之上,有不透明或半透明区域。
DirtyRegion: 脏区域,可见部分改变区域,包括新的被遮挡区域,和新的露出区域。

2. 相关类

窗口的创建到UI的绘制主要涉及

  • WindowManager:应用与窗口管理服务WindowManagerService交互的接口
  • WindowManagerService:窗口管理服务,继承于IWindowManager.Stub,是Binder的服务端,该服务运行在一个单独的进程中,因此WindowManager与WindowManagerService的交互也是一个IPC的过程。
  • SurfaceFlinger:SurfaceFlinger服务运行在Android系统的System进程中,它负责管理Android系统的帧缓冲区(Frame Buffer),Android设备的显示屏被抽象为一个帧缓冲区,而Android系统中的SurfaceFlinger服务就是通过向这个帧缓冲区写入内容来绘制应用程序的用户界面的。
  • Surface:每个显示界面的窗口都是一个Surface。
  • PhoneWindowManager:实现了窗口的各种策略。
  • Choreographer:用于控制窗口动画、屏幕旋转等操作。 DisplayContent:用于描述多屏输出相关信息。
  • WindowState:描述窗口的状态信息以及和WindowManagerService进行通信,一般一个窗口对应一个WindowState。
  • WindowToken:窗口Token,用来做Binder通信。
  • Session:通信对象,App进程通过建立Session代理对象和Session对象通信,进而和WindowManagerService建立连接。

每个Activity对应一个Window,每个Window对应一个ViewRootImpl,ViewRootImpl负责管理DecorView与WindowManagerService的交互,每次调用WindowManager.addView()添加窗口时,都会创建一个ViewRootImpl对象,它内部也保存了一些重要信息

参考:
博客1View

博客2Window

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值