Android View工作流程

本文来自网络,也可能略有改动,如有任何不妥可以联系删除,原文地址 https://www.jianshu.com/p/af266ff378c6

本文的目的有两个:

  1. 给对自定义 View 感兴趣的人一些入门的指引
  2. 给正在使用自定义 View 的人一些更深入的解析
    自定义 View 一直都被认为是 Android 开发高手的必备技能,而稳中带皮的学习 View 的基础体系,这是自定义 View 的必经之路,如果自定义 View 如果设计的不好或者不考虑性能的话会造成很大的问题。所以我们进入 View 工作流程的分析。

一、Android 的 UI 层级绘制体系

Android 中的 Activity 是作为应用程序的载体存在的,它代表一个完整的用户界面并提供了窗口进行视图绘制。

  • 在这里,我们这里所说的视图绘制,实质上就是在对 View 及其子类进行操作。而 View 作为视图控件的顶层父类,在本文中会对其进行详细分析。我们以 Android 的 UI 层级绘制体系为切入点对 View 进行探究。

    图 1 View 的层级结构

Android 的 UI 层级绘制体系如图 1 所示。

绘制体系中做了这些事情
①当调用 Activity 的 setContentView 方法后会调用 PhoneWindow 类的 setContentView 方法(PhoneWindow 是抽象类 Windiw 的实现类,Window 用来描述 Activity 视图最顶端的窗口的显示内容和行为动作)。
②PhoneWindow 类的 setContentView 方法中最终会生成一个 DecorView 对象(DectorView 是是 PhoneWindow 的内部类,继承自 FrameLayout)。
③DecorView 容器中包含根布局,根布局中包含一个 id 为 content 的 FrameLayout 布局,Activity 加载布局的 xml 最后通过 LayoutInflater 将 xml 文件中的内容解析成 View 层级体系,最后填加到 id 为 content 的 FrameLayout 布局中。

至此,View 最终就会显示到手机屏幕上。

二、View 的视图绘制流程剖析

1、DecorView 被加载到 Window 中

DecorView 被加载到 Window 的过程中,WindowManager 起到了关键性的作用,最后交给 ViewRootImpl 做详细处理,通过如下的局部 ActivityThread 的源码分析这一点可以得到印证 (在这里我只展示核心源码,详细源码可以在代码中查看)。

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        ActivityClientRecord r = mActivities.get(token);
       ...
       
        r = performResumeActivity(token, clearHide, 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();
          ...
          if (r.mPreserveWindow) {
          ...
          
          ViewRootImpl impl = decor.getViewRootImpl();
           ...
          }
          if (a.mVisibleFromClient) {
              if (!a.mWindowAdded) {
                   a.mWindowAdded = true;
                   
                   wm.addView(decor, l);
                   } 
                   ...
          }
          ...
    }


WindowManager 将 DecorView 添加到 PhoneWindow 中,即 addView() 方法执行时将视图添加的动作交给了 ViewRoot,ViewRoot 作为接口,其实现类 ViewRootImpl 具体实现了 addView() 方法,最后,视图的具体绘制在 performTraversals() 中展开,如下图 2.1 所示:

图 2.1 View 绘制的代码层级分析

2、ViewRootImpl 的 performTraversals() 方法完成具体的视图绘制流程

在源码中 ViewRootImpl 中视图具体绘制的流程如下:

private void performTraversals() {
        
        
        final View host = mView;
        
        if (host == null || !mAdded)
            return;
        
        mIsInTraversal = true;
        
        mWillDrawSoon = true;
         ...
        
        int desiredWindowWidth;
        int desiredWindowHeight;

         ...
        
        if (mFirst) {
            mFullRedrawNeeded = true;
            mLayoutRequested = true;
            
            if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
                    || lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
                
                Point size = new Point();
                mDisplay.getRealSize(size);
                desiredWindowWidth = size.x;
                desiredWindowHeight = size.y;
            } else {
                DisplayMetrics packageMetrics =
                    mView.getContext().getResources().getDisplayMetrics();
                desiredWindowWidth = packageMetrics.widthPixels;
                desiredWindowHeight = packageMetrics.heightPixels;
            }
    }
  ...

 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
 int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

  
  
 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
  ...
  
 performLayout(lp, desiredWindowWidth, desiredWindowHeight);
  ...
  
 performDraw();
}


该方法主要流程就体现了 View 绘制渲染的三个主要步骤,分别是测量,摆放,绘制三个阶段。流程图如下图 2.2 所示:

图 2.2 View 的绘制流程

接下来,我们对于 performMeasure()、performLayout()、 performDraw() 完成具体拆解分析。实质上最后就需要定位到 View 的 onMeasure()、onLayout()、onDraw() 方法中。

三、MeasureSpec 在 View 体系中的作用

1、MeasureSpec 的作用

首先我们从 performMeasure() 入手分析,在上面的内容中,我们通过源码可以看到 performMeasure() 方法中传入了 childWidthMeasureSpec、childHeightMeasureSpec 两个 int 类型的值,performMeasure 方法的源码如下所示:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
}


这两个值又传递到 mView.measure(childWidthMeasureSpec, childHeightMeasureSpec) 方法中,其中 measure 方法的核心源码如下:

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }
        ...
        if (forceLayout || needsLayout) {
            
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                
                
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                ...
            } 
         ...
    }


到这里我们应该明确,childWidthMeasureSpec, childHeightMeasureSpec 是 MeasureSpec 根据原有宽高计算获取不同模式下的具体宽高值。

2、MeasureSpec 剖析

MeasureSpec 是 View 的内部类,内部封装了 View 的规格尺寸,以及 View 的宽高信息。在 Measure 的流程中,系统会将 View 的 LayoutParams 根据父容器是施加的规则转换为 MeasureSpec,然后在 onMeasure() 方法中具体确定控件的宽高信息。源码及分析如下所示:

public static class MeasureSpec {
        
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}
        

        
        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;

        
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        
        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

        
        
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            
            return (measureSpec & MODE_MASK);
        }

        
        
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

        
        static int adjust(int measureSpec, int delta) {
            final int mode = getMode(measureSpec);
            int size = getSize(measureSpec);
            if (mode == UNSPECIFIED) {
                
                return makeMeasureSpec(size, UNSPECIFIED);
            }
            size += delta;
            if (size < 0) {
                Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                        ") spec: " + toString(measureSpec) + " delta: " + delta);
                size = 0;
            }
            return makeMeasureSpec(size, mode);
        }

        
        public static String toString(int measureSpec) {
            int mode = getMode(measureSpec);
            int size = getSize(measureSpec);

            StringBuilder sb = new StringBuilder("MeasureSpec: ");

            if (mode == UNSPECIFIED)
                sb.append("UNSPECIFIED ");
            else if (mode == EXACTLY)
                sb.append("EXACTLY ");
            else if (mode == AT_MOST)
                sb.append("AT_MOST ");
            else
                sb.append(mode).append(" ");

            sb.append(size);
            return sb.toString();
        }
    }



MeasureSpec 的常量中指定了两种内容,一种为尺寸模式,一种为具体的宽高信息。其中高 2 位表示尺寸测量模式,低 30 位表示具体的宽高信息。
尺寸测量模式有如下三种:

尺寸测量模式的 3 种类型
①UNSPECIFIED:未指定模式,父容器不限制 View 的大小,一般用于系统内部的测量
②AT_MOST:最大模式,对应于在 xml 文件中指定控件大小为 wrap_content 属性,子 View 的最终大小是父 View 指定的大小值,并且子 View 的大小不能大于这个值
③EXACTLY :精确模式,对应于在 xml 文件中指定控件为 match_parent 属性或者是具体的数值,父容器测量出 View 所需的具体大小

我帮你总结一下

对于每一个 View,都持有一个 MeasureSpec,MeasureSpec 保存了该 View 的尺寸测量模式以及具体的宽高信息,MeasureSpec 受自身的 LayoutParams 和父容器的 MeasureSpec 共同影响。

四、View 的 Measure 流程分析

1、View 树的 Measure 测量流程逻辑图

2、View 的 Measure 流程分析

那么在上文 3.1 的分析中,我们能够明确在 measure 方法中最后调用 onMeasure() 方法完成子 View 的具体测量,onMeasure() 方法的源码如下所示:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }


setMeasuredDimension() 方法在 onMeasure() 中被调用,被用于存储测绘的宽度、高度,而不这样做的话会触发测绘时的异常。

    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);
    }


在 setMeasuredDimension() 方法中传入的是 getDefaultSize(),接着分析 getDefaultSize() 中做了哪些操作:

    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;
    }


通过上文对 MeasureSpec 的分析,在这里我们就能明确,getDefaultSize 实质上就是根据测绘模式确定子 View 的具体大小,而对于自定义 View 而言,子 View 的宽高信息不仅由自身决定,如果它被包裹在 ViewGroup 中就需要具体测量得到其精确值。

3、View 的 Measure 过程中遇到的问题以及解决方案

View 的 measure 过程和 Activity 的生命周期方法不是同步执行的,因此无法保证 Activity 执行了 onCreate、onStart、onResume 时某个 View 已经测量完毕了。如果 View 还没有测量完毕,那么获得的宽和高都是 0。下面是 3 种解决该问题的方法:

①Activity/View 的 onWindowsChanged() 方法

onWindowFocusChanged() 方法表示 View 已经初始化完毕了,宽高已经准备好了,这个时候去获取是没问题的。这个方法会被调用多次,当 Activity 继续执行或者暂停执行的时候,这个方法都会被调用,代码如下:

  public void onWindowFocusChanged(boolean hasWindowFocus) {
         super.onWindowFocusChanged(hasWindowFocus);
       if(hasWindowFocus){
       int width=view.getMeasuredWidth();
       int height=view.getMeasuredHeight();
      }      
  }


②View.post(runnable) 方法

通过 post 将一个 Runnable 投递到消息队列的尾部,然后等待 Looper 调用此 runnable 的时候 View 也已经初始化好了

@Override  
protected void onStart() {  
    super.onStart();  
    view.post(new Runnable() {  
        @Override  
        public void run() {  
            int width=view.getMeasuredWidth();  
            int height=view.getMeasuredHeight();  
        }  
    });  
}  


③ViewTreeObsever

使用 ViewTreeObserver 的众多回调方法可以完成这个功能,比如使用 onGlobalLayoutListener 接口,当 View 树的状态发生改变或者 View 树内部的 View 的可见性发生改变时,onGlobalLayout 方法将被回调。伴随着 View 树的变化,这个方法也会被多次调用。

  @Override  
  protected void onStart() {  
    super.onStart();  
    ViewTreeObserver viewTreeObserver=view.getViewTreeObserver();  
    viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {  
        @Override  
        public void onGlobalLayout() {  
            view.getViewTreeObserver().removeOnGlobalLayoutListener(this);  
            int width=view.getMeasuredWidth();  
            int height=view.getMeasuredHeight();  
        }  
    });  
} 


当然,在这里你可以通过 setMeasuredDimension() 方法对子 View 的具体宽高以及测量模式进行指定。

五、View 的 layout 流程分析

1、View 树的 layout 摆放流程逻辑图

2、View 的 layout 流程分析

layout 的作用是 ViewGroup 来确定子元素的位置,当 ViewGroup 的位置被确定后,在 layout 中会调用 onLayout ,在 onLayout 中会遍历所有的子元素并调用子元素的 layout 方法。
在代码中设置 View 的成员变量 mLeft,mTop,mRight,mBottom 的值,这几个值是在屏幕上构成矩形区域的四个坐标点,就是该 View 显示的位置,不过这里的具体位置都是相对与父视图的位置而言,而 onLayout 方法则会确定所有子元素位置,ViewGroup 在 onLayout 函数中通过调用其 children 的 layout 函数来设置子视图相对与父视图中的位置,具体位置由函数 layout 的参数决定。下面我们先看 View 的 layout 方法 (只展示关键性代码) 如下:

  
    public void layout(int l, int t, int r, int b) {
        ...
        
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        
        
        
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        
            onLayout(changed, l, t, r, b);
            ...
        }
        ...
    }


六、View 的 draw 流程分析

1、View 树的 draw 绘制流程逻辑图

2、View 的 draw 流程分析

在 View 的 draw() 方法的注释中,说明了绘制流程中具体每一步的作用,源码中对于 draw() 方法的注释如下,我们在这里重点分析注释中除第 2、第 5 步外的其他步骤。

①View 中的 drawBackground() 绘制背景

核心源码如下:

  private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        ...
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }


如果背景有偏移,实质上对画布首先做偏移处理,然后在其上进行绘制。

②View 内容的绘制

View 内容的绘制源码如下所示:

    protected void onDraw(Canvas canvas) {
    }


该方法是空实现,就根据不同的内容进行不同的设置,自定义 View 中就需要重写该方法加入我们自己的业务逻辑。

③子 View 的绘制

子 View 的绘制源码如下所示:

    protected void dispatchDraw(Canvas canvas) {

    }


该方法同样为空实现,而对于 ViewGroup 而言对子 View 进行遍历,并最终调用子 View 的 onDraw 方法进行绘制。

④装饰绘制

装饰绘制的源码如下所示 (只展示核心源码):

    public void onDrawForeground(Canvas canvas) {
        
        onDrawScrollIndicators(canvas);
        onDrawScrollBars(canvas);
       ...
            foreground.draw(canvas);
    }


很明显,在这里 onDrawForeground() 方法用于绘制例如 ScrollBar 等其他装饰,并将它们显示在视图的最上层。

七、视图重绘

1、requestLayout 重新绘制视图

子 View 调用 requestLayout 方法,会标记当前 View 及父容器,同时逐层向上提交,直到 ViewRootImpl 处理该事件,ViewRootImpl 会调用三大流程,从 measure 开始,对于每一个含有标记位的 view 及其子 View 都会进行测量、布局、绘制。

2、invalidate 在 UI 线程中重新绘制视图

当子 View 调用了 invalidate 方法后,会为该 View 添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递到 ViewRootImpl 中,最终触发 performTraversals 方法,进行开始 View 树重绘流程 (只绘制需要重绘的视图)。

3、postInvalidate 在非 UI 线程中重新绘制视图

这个方法与 invalidate 方法的作用是一样的,都是使 View 树重绘,但两者的使用条件不同,postInvalidate 是在非 UI 线程中调用,invalidate 则是在 UI 线程中调用。

我要总结了

  • 总结一下
    一般来说,如果 View 确定自身不再适合当前区域,比如说它的 LayoutParams 发生了改变,需要父布局对其进行重新测量、摆放、绘制这三个流程,往往使用 requestLayout。而 invalidate 则是刷新当前 View,使当前 View 进行重绘,不会进行测量、布局流程,因此如果 View 只需要重绘而不需要测量,布局的时候,使用 invalidate 方法往往比 requestLayout 方法更高效。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容