posts - 21,comments - 0,views - 3879

View绘制流程

一、View的绘制时机

1、知识储备

  • Window:每个Activity都会创建一个Window用于承载View视图的显示,Window是一个抽象类,存在一个唯一实现类PhoneWindow。

  • PhoneWindow:该类继承于Window类,是Window类的具体实现,我们可以通过该类去绘制窗口。并且,该类内部包含了一个 DecorView 对象,该 DectorView 对象是所有应用窗口的根 View。

  • DecorView:最顶层的View,是一个FrameLayout子类,最终会被加载到Window当中,它内部只有一个垂直方向的LinearLayout分为两部分:一个是TitleBar(ActionBar 的容器),另一个是ContentView(Activity对应的XML布局,通过setContentView设置到DecorView中)。

  • WindowManager : 是一个接口,里面常用的方法有:添加View,更新View和删除View。主要是用来管理 Window 的。WindowManager 具体的实现类是WindowManagerImpl。最终,WindowManagerImpl 会将业务交给 WindowManagerGlobal 来处理。

  • WindowManagerService (WMS) : 负责管理各 app 窗口的创建,更新,删除, 显示顺序。运行在 system_server 进程。

  • ViewRootImpl :拥有 DecorView 的实例,通过该实例来控制 DecorView ,最终通过执行ViewRootImpl的performTraversals()开启整个View树的绘制。ViewRootImpl 的一个内部类 W,实现了 IWindow 接口,IWindow 接口是供 WMS 使用的,WSM 通过调用 IWindow 一些方法,通过 Binder 通信的方式,最后执行到了 W 中对应的方法中。同样的,ViewRootImpl 通过 IWindowSession 来调用 WMS 的 Session 一些方法。Session 类继承自 IWindowSession.Stub,每一个应用进程都有一个唯一的 Session 对象与 WMS 通信。

2、Activity、Window、DecorView之间关系

首先来看一下Activity中setContentView源码,从代码可以看出, Activity的 setContentView实质是将 View传递到 Window的 setContentView()方法中, Window的 setContenView会在内部调用 installDecor()方法创建 DecorView。installDecor()方法通过 generateDecor()new一个 DecorView,然后调用 generateLayout()获取 DecorView中 content,最终通过 inflate将 Activity视图添加到 DecorView中的 content中,但此时 DecorView还未被添加到 Window中。添加操作需要借助 ViewRootImpl。ViewRootImpl的作用是用来衔接 WindowManager和 DecorView,在 Activity被创建后会通过 WindowManager将 DecorView添加到 PhoneWindow中并且创建 ViewRootImpl实例,随后将 DecorView与 ViewRootImpl进行关联,最终通过执行 ViewRootImpl的 performTraversals()开启整个View树的绘制。

public void setContentView(@LayoutRes int layoutResID) {
    //将xml布局传递到Window当中
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

public void setContentView(int layoutResID) { 
    if (mContentParent == null) {
        //初始化DecorView以及其内部的content
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
    ...............
    } else {
        //将contentView加载到DecorVoew当中
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    ...............
}

private void installDecor() {
    ...............
    if (mDecor == null) {
        //实例化DecorView
        mDecor = generateDecor(-1);
        ...............
        }
    } else {
        mDecor.setWindow(this);
    }
   if (mContentParent == null) {
        //获取Content
        mContentParent = generateLayout(mDecor);
   }  
    ...............
}

protected DecorView generateDecor(int featureId) {
    ...............
    return new DecorView(context, featureId, this, getAttributes());
}

二、绘制流程

View的绘制是从 ViewRootImpl的 performTraversals()方法开始,从最顶层的 View(ViewGroup)开始逐层对每个 View进行绘制操作

private void performTraversals() {
 ...............
//measur过程
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
 ...............
//layout过程
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
 ...............
//draw过程
performDraw();}
  • measure:为测量宽高过程,如果是ViewGroup还要在onMeasure中对所有子View进行measure操作
  • layout:用于摆放View在ViewGroup中的位置,如果是ViewGroup要在onLayout方法中对所有子View进行layout操作
  • draw:往View上绘制图像

1、Measure

  • 从performMeasure()源码可以看出从mView(最顶层ViewGroup)开始进行测量操作,然后逐层遍历View并执行measure操作

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    	if (mView == null) {
    		return;
    	}
    	try {
    		mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    	} finally {
    		Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    	}
    }
    
  • Measure是 View绘制三个过程中的第一步,提到 Measure就不得不提 MeasureSpac它是一个32位 int类型数值,高两位 SpacMode代表测量模式,低30位 SpacSize代表测量尺寸,是View的内部类,内部也包含三种测量模式:

    • UNSPECIFIED :父布局不会对子View做任何限制,例如我们常用的 ScrollView就是这种测量模式
    • EXACTLY :精确数值,比如使用了 match_parent或者xxxdp,表示父布局已经决定了子 View的大小,通常在这种情况下 View的尺寸就是 SpacSize
    • AT_MOST :自适应,对应 wrap_content子View可以根据内容设置自己的大小,但前提是不能超出父 ViewGroup的宽高
    public class MeasureSpec {
        private static final int MODE_SHIFT = 30;  
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
        public static final int EXACTLY = 1 << MODE_SHIFT;  
        public static final int AT_MOST = 2 << MODE_SHIFT;  
    }
    
    • 在我们自定义View的过程中都会在onMeasure中进行宽高的测量,这个方法会从父布局中接收两个参数 widthMeasureSpac和 heightMeasureSpac,所以子布局的宽高大小需要受限于父布局
  • 在自定义View宽高测量的过程中,我们需要获取 MeasurSpac中的宽高和测量模式,自定义 ViewGroup也必须给子View传递 MeasurSpac,Android也给我们提供了计算 MeasurSpac 和通过 MeasurSpac 获取相应值的方式,都位于 MeasurSpac中,从 ViewGroup到 View对尺寸和模式进行了一次封装和拆解,其目的是为了减少对象的创建,避免造成不必要的内存浪费。

    public static class MeasureSpec {
    	public static int makeMeasureSpec( int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
    	}
    	public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
    	}
    	public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK)
    	}
    }
    
  • 前面也提到了,子View的宽高是要受限于父布局的,所以不能通过setWidth或者setHeight直接设置宽高的,另外 LayoutParams的作用不仅如此,比如一个View的父布局是RelativeLayout,可以通过设置RelativeLayout.LayoutParams的above,below等属性来调整在父布局中的位置。

    • 自定义View宽高测量演示:创建一个类继承View,重写其 onMeasure()方法。

      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      	//默认宽  
      	int defaultWidth = 0;    
      	//默认高
      	int defaultHeight = 0;    
      	setMeasuredDimension(
      		getDefaultSize(defaultWidth, widthMeasureSpec),  
              getDefaultSize(defaultHeight, heightMeasureSpec)
      	);
      }
      
    • 一般的自定义View中,如果对宽高没有特殊需求可直接通过 getDefaultSize()方法获取。从代码中可知,获取 mode和 size后会分别对三种测量模式进行判断, UNSPECIFIED使用默认尺寸,而 AT_MOST和 EXACTLY使用父布局给出的测量尺寸。尺寸计算完毕后通过 setMeasuredDimension(width,height)设置最终宽高。

      public static int getDefaultSize(int size, int measureSpec) {
          //默认尺寸
          int result = size;
          //获取测量模式
          int specMode = MeasureSpec.getMode(measureSpec);
          //获取尺寸
          int specSize = MeasureSpec.getSize(measureSpec);
          switch (specMode) {
             case MeasureSpec.UNSPECIFIED:
                 result = size;
                 break;
             case MeasureSpec.AT_MOST:
             case MeasureSpec.EXACTLY:
                 result = specSize;
                 break;
          }
          return result;
      }
      

2、Layout

  • 跟measure类似,performLayout同样是从 mView(最顶层ViewGroup)开始进行 layout操作,随后逐层遍历。 layout(l,t,r,b)四个参数分别对应 左上右下的位置,从而确定View在ViewGroup中的位置

    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        .........
        final View host = mView;
        if (host == null) {
            return;
        }
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
        .........}
    
  • layout()会将四个位置参数传递给 setOpticalFrame()或者 setFrame(),而 setOpticalFrame()内部会调用 setFrame(),所以最终通过 setFrame()确定 View在 ViewGroup中的位置。位置确定完毕会调用 onLayout(l,t,r,b)对子View进行摆放

    public void layout(int l, int t, int r, int b) {
        .......
        //通过setOpticalFrame()和setFrame()老确定四个点的位置
        boolean changed = isLayoutModeOptical(mParent) ? 
        setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        .......
        //调用onLayout(),ViewGroup须重写此方法
        onLayout(changed, l, t, r, b);
        .......}
    
  • View和 ViewGroup在执行完 setFrame()后都会调用 onLayout()方法,但上面也有提到该方法的作用是对子View进行位置摆放,所以单一View是不需要重写此方法。而 ViewGroup会根据自己的特性任意对子View进行摆放

3、Draw

  • 相比于measure和layout阶段,draw阶段中View和ViewGroup变得没那么紧密了,View的绘制过程中不需要考虑ViewGroup,而ViewGroup也只需触发子View的绘制方法即可。performDraw()执行后同样会从根布局开始逐层对每个View进行draw操作,在View中绘制操作时通过 draw()进行。draw()方法中主要包含四部分内容,其中我们开发者只需要关心onDraw(canvas)即可,即自身的内容绘制

    public void draw(Canvas canvas) {
         ........
        // 绘制背景
        drawBackground(canvas);
        // 绘制内容
        onDraw(canvas);
        // 绘制子View
        dispatchDraw(canvas);
        // 绘制装饰,如scrollBar
        onDrawForeground(canvas)
        ........}
    
  • 绘制内容

    • Canvas:画布,不管是文字,图形,图片都要通过画布绘制而成
    • Paint:画笔,可设置颜色,粗细,大小,阴影等等等等,一般配合画布使用
    • Path:路径,用于形成一些不规则图形。
    • Matrix:矩阵,可实现对画布的几何变换。

三、总结

View的绘制流程:绘制时机,宽高测量,位置摆放,图像绘制

posted on   幺幺零零  阅读(497)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示