Android面试view

Android - View 绘制流程

2022-01-23  本文已影响0人  Whyn
View 绘制流程

简介

我们知道,在 Android 中,View 绘制主要包含 3 大流程:

Android 中,主要有两种视图:ViewViewGroup,其中:

虽然ViewGroup继承于View,但是在 View 绘制三大流程中,某些流程需要区分ViewViewGroup,它们之间的操作并不完全相同,比如:

measure 流程

对 View 进行测量,主要包含两个步骤:

  1. 求取 View 的测量规格MeasureSpec
  2. 依据上一步求得的MeasureSpec,对 View 进行测量,求取得到 View 的最终测量宽/高。

MeasureSpec

对于第一个步骤,即求取 View 的MeasureSpec,首先我们来看下MeasureSpec的源码定义:

// frameworks/base/core/java/android/view/View.java
public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    /**
     * Measure specification mode: The parent has not imposed any constraint
     * on the child. It can be whatever size it wants.
     */
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    /**
     * Measure specification mode: The parent has determined an exact size
     * for the child. The child is going to be given those bounds regardless
     * of how big it wants to be.
     */
    public static final int EXACTLY     = 1 << MODE_SHIFT;

    /**
     * Measure specification mode: The child can be as large as it wants up
     * to the specified size.
     */
    public static final int AT_MOST     = 2 << MODE_SHIFT;

    // 生成测量规格
    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) {
        return (measureSpec & MODE_MASK);
    }

    // 获取测量大小
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
    ...
}

MeasureSpecView的一个公有静态内部类,它是一个 32 位的int值,高 2 位表示 SpecMode(测量模式),低 30 位表示 SpecSize(测量尺寸/测量大小)。
MeasureSpec将两个数据打包到一个int值上,可以减少对象内存分配,并且其提供了相应的工具方法可以很方便地让我们从一个int值中抽取出 View 的 SpecMode 和 SpecSize。

一个MeasureSpec表达的是:该 View 在该种测量模式(SpecMode)下对应的测量尺寸(SpecSize)。其中,SpecMode 有三种类型:

LayoutParams

对 View 进行测量,最关键的一步就是计算得到 View 的MeasureSpec,子View 在创建时,可以指定不同的LayoutParams(布局参数),LayoutParams的源码主要内容如下所示:

// frameworks/base/core/java/android/view/ViewGroup.java
public static class LayoutParams {
    ...
    /**
     * Special value for the height or width requested by a View.
     * MATCH_PARENT means that the view wants to be as big as its parent,
     * minus the parent's padding, if any. Introduced in API Level 8.
     */
    public static final int MATCH_PARENT = -1;

    /**
     * Special value for the height or width requested by a View.
     * WRAP_CONTENT means that the view wants to be just large enough to fit
     * its own internal content, taking its own padding into account.
     */
    public static final int WRAP_CONTENT = -2;

    /**
     * Information about how wide the view wants to be. Can be one of the
     * constants FILL_PARENT (replaced by MATCH_PARENT
     * in API Level 8) or WRAP_CONTENT, or an exact size.
     */
    public int width;

    /**
     * Information about how tall the view wants to be. Can be one of the
     * constants FILL_PARENT (replaced by MATCH_PARENT
     * in API Level 8) or WRAP_CONTENT, or an exact size.
     */
    public int height;
    ...
}

其中:

LayoutParams会受到父容器的MeasureSpec的影响,测量过程会依据两者之间的相互约束最终生成子View 的MeasureSpec,完成 View 的测量规格。

简而言之,View 的MeasureSpec受自身的LayoutParams和父容器的MeasureSpec共同决定(DecorViewMeasureSpec是由自身的LayoutParams和屏幕尺寸共同决定,参考后文)。也因此,如果要求取子View 的MeasureSpec,那么首先就需要知道父容器的MeasureSpec,层层逆推而上,即最终就是需要知道顶层View(即DecorView)的MeasureSpec,这样才能一层层传递下来,这整个过程需要结合Activity的启动过程进行分析。

Activity 视图基本结构

我们知道,在 Android 中,Activity是作为视图组件存在,主要就是在手机上显示视图界面,可以供用户操作,Activity就是 Andorid 中与用户直接交互最多的系统组件。

Activity的基本视图层次结构如下所示:

Android - Acvitity 视图架构

Activity中,实际承载视图的组件是Window(更具体来说为PhoneWindow),顶层View 是DecorView,它是一个FrameLayoutDecorView内部是一个LinearLayout,该LinearLayout由两部分组成(不同 Android 版本或主题稍有差异):TitleViewContentView,其中,TitleView就是标题栏,也就是我们常说的TitleBarActionBarContentView就是内容栏,它也是一个FrameLayout,主要用于承载我们的自定义根布局,即当我们调用setContentView(...)时,其实就是把我们自定义的布局设置到该ContentView中。

Activity启动完成后,最终就会渲染出上述层次结构的视图。

DecorView 测量规格

因此,如果我们要求取得到子View 的MeasureSpec,那么第一步就是求取得到顶层View(即DecorView)的MeasureSpec。大致过程如下所示:

  1. Activity启动过程中,会调用到ActivityThread.handleResumeActivity(...),该方法就是 View 视图绘制的起始之处:

    // frameworks/base/core/java/android/app/ActivityThread.java
    final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {
        ...
        ActivityClientRecord r = performResumeActivity(token, clearHide);
        ...
        // 此处的 window 为与 Activity 绑定的 PhoneWindow,即 Activity.mWindow
        r.window = r.activity.getWindow();
        // PhoneWindow 绑定的顶层视图:DecorView
        View decor = r.window.getDecorView();
        decor.setVisibility(View.INVISIBLE);
        // 获取与 Activity 绑定的 WindowManager,实际上是 PhoneWindow 的 WindowManager
        ViewManager wm = a.getWindowManager();
        WindowManager.LayoutParams l = r.window.getAttributes();
        ...
        // 添加 DecorView 到 PhoneWindow 上(相当于设置 Activity 根视图)
        wm.addView(decor, l);
        ...
    }
    

    其中,r.window.getDecorView()实际调用的是PhoneWindow.getDecorView(),其会返回顶层DecorView(不存在时会自动实例化):

    // frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
    public class PhoneWindow extends Window implements MenuBuilder.Callback {
        // This is the top-level view of the window, containing the window decor.
        private DecorView mDecor;
        ...
    
        @Override
        public final View getDecorView() {
            if (mDecor == null) {
                installDecor();
            }
            return mDecor;
        }
    
        private void installDecor() {
            if (mDecor == null) {
                mDecor = generateDecor();
                ...
            }
            ...
        }
    
        protected DecorView generateDecor() {
            // 实例化 DecorView
            return new DecorView(getContext(), -1);
        }
        ...
    }
    

    然后,r.window.getAttributes()实际调用的是Window.getAttributes()

    // frameworks/base/core/java/android/view/Window.java
    public abstract class Window {
        private final WindowManager.LayoutParams mWindowAttributes =
            new WindowManager.LayoutParams();
        ...
    
        public final WindowManager.LayoutParams getAttributes() {
            return mWindowAttributes;
        }
    }
    // frameworks/base/core/java/android/view/WindowManager.java
    public interface WindowManager extends ViewManager {
        ...
        public static class LayoutParams extends ViewGroup.LayoutParams
                implements Parcelable {
            public LayoutParams() {
                // DecorView 的布局参数为 MATCH_PARENT
                super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
                ...
            }
        }
    }
    

    这里可以看到,此处r.window.getAttributes()返回的是一个WindowManager.LayoutParams实例,对应的最终宽/高布局参数为LayoutParams.MATCH_PARENT,最后通过wm.addView(decor,l)DecorView添加到WindowManager上(最终其实是设置到ViewRootImpl上),所以DecorView的布局参数为MATCH_PARENT

  2. View 的绘制流程真正开始的地方为ViewRootImpl.performTraversals(),在其中,有如下代码片段:

    // frameworks/base/core/java/android/view/ViewRootImpl.java
    private void performTraversals() {
        ...
        int desiredWindowWidth;
        int desiredWindowHeight;
        ...
        // Ask host how big it wants to be
        windowSizeMayChange |= measureHierarchy(host, lp, res,
                desiredWindowWidth, desiredWindowHeight);
        ...
    }
    
    private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
        final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        ...
        childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...
    }
    

    此处的desiredWindowWidthdesiredWindowHeight是屏幕的尺寸,内部最终会调用到ViewRootImpl.getRootMeasureSpec(...),其源码如下所示:

    // frameworks/base/core/java/android/view/ViewRootImpl.java
    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
            case ViewGroup.LayoutParams.MATCH_PARENT:
                // Window can't resize. Force root view to be windowSize.
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
                break;
            case ViewGroup.LayoutParams.WRAP_CONTENT:
                // Window can resize. Set max size for root view.
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
                break;
            default:
                // Window wants to be an exact size. Force root view to be that size.
                measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
                break;
        }
        return measureSpec;
    }
    

    ViewRootImpl.getRootMeasureSpec(...)见名知意,其实就是用来获取顶层View(即DecorView)的MeasureSpec,其逻辑如下:

    1. DecorViewLayoutParamsMATCH_PARENT时,说明DecorView的大小与屏幕一样大,而又由于屏幕大小是确定的,因此,其 SpecMode 为EXACTLY,SpecSize 为windowSize,;
    2. DecorViewLayoutParamsWRAP_CONTENT时,说明DecorView自适应内容大小,因此它的大小不确定,但是最大不能超过屏幕大小,故其 SpecMode 为AT_MOST,SpecSize 为windowSize
    3. 其余情况为DecorView设置了具体数值大小或UNSPECIFIED,故以DecorView为主,其 SpecMode 为EXACTLY,SpecSize 就是自己设置的值,即rootDimension

    结合我们上面的分析,由于DecorViewLayoutParamsMATCH_PARENT,因此,DecorViewMeasureSpec最终为:MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY),即DecorView的 SpecMode 为EXACTLY,SpecSize 为屏幕大小。

默认测量(measure)

经过上述步骤求取得到 View 的MeasureSpec后,接下来就可以真正对 View 进行测量,求取 View 的最终测量宽/高:

Android 内部对视图进行测量的过程是由View#measure(int, int)方法负责的,但是对于ViewViewGroup,其具体测量过程有所差异。

因此,对于测量过程,我们分别对ViewViewGroup进行分析:

综上,无论是对View的测量还是ViewGroup的测量,都是由View#measure(int widthMeasureSpec, int heightMeasureSpec)方法负责,然后真正执行 View 测量的是 View 的onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法。

具体来说,View直接在onMeasure(...)中测量并设置自己的最终测量宽/高。在默认测量情况下,View的测量宽/高由其父容器的MeasureSpec和自身的LayoutParams共同决定,当View自身的测量模式为LayoutParams.UNSPECIFIED时,其测量宽/高为android:minWidth/android:minHeight和其背景宽/高之间的较大值,其余情况皆为自身MeasureSpec指定的测量尺寸。

而对于ViewGroup来说,由于布局特性的丰富性,只能自己手动覆写onMeasure(...)方法,实现自定义测量过程,但是总的思想都是先测量 子View 大小,最终才能确定自己的测量大小。

layout 流程

当确定了 View 的测量大小后,接下来就可以来确定 View 的布局位置了,也即将 View 放置到屏幕具体哪个位置。

View layout

View 的布局过程由View#layout(...)负责,其源码如下:

// android/view/View.java
/**
 * @param l Left position, relative to parent
 * @param t Top position, relative to parent
 * @param r Right position, relative to parent
 * @param b Bottom position, relative to parent
 */
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
    ...
    setFrame(l, t, r, b);
    ...
    onLayout(changed, l, t, r, b);
    ...
}

View#layout(...)主要就做了两件事:

  1. setFrame(...):首先通过View#setFrame(...)来确定自己的布局位置,其源码如下:

    // android/view/View.java
    protected boolean setFrame(int left, int top, int right, int bottom) {
        ...
        // Invalidate our old position
        invalidate(sizeChanged);
    
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
    }
    

    setFrame(...)其实就是更新记录 View 的四个顶点位置,这样 View 在父容器中的坐标位置就确定了。

  2. onLayout(...)setFrame(...)是用于确定 View 自身的布局位置,而onLayout(...)主要用于确定 子View 的布局位置:

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }
    

    由于 View 不包含子组件,因此其onLayout是一个空实现。

ViewGroup layout

ViewGroup 的布局流程由ViewGroup#layout(...)负责,其源码如下:

// android/view/ViewGroup.java
@UiThread
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    ...
    @Override
    public final void layout(int l, int t, int r, int b) {
        ...
        super.layout(l, t, r, b);
        ...
    }

可以看到,ViewGroup#layout(...)最终也是通过View#layout(...)完成自身的布局过程,一个注意的点是,ViewGroup#layout(...)是一个final方法,因此子类无法覆写该方法,主要是ViewGroup#layout(...)方法内部对子视图动画效果进行了相关设置。

由于ViewGroup#layout(...)内部最终调用的还是View#layout(...),因此,ViewGroup#onLayout(...)就会得到回调,用于处理 子View 的布局放置,其源码如下:

// android/view/ViewGroup.java
@Override
protected abstract void onLayout(boolean changed,
        int l, int t, int r, int b);

由于不同的ViewGroup,其布局特性不同,因此ViewGroup#onLayout(...)是一个抽象方法,交由ViewGroup子类依据自己的布局特性,摆放其 子View 的位置。

draw 流程

当 View 的测量大小,布局位置都确定后,就可以最终将该 View 绘制到屏幕上了。

View 的绘制过程由View#draw(...)方法负责,其源码如下:

// android/view/View.java
public void draw(Canvas canvas) {
    ...
    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background
     *      2. If necessary, save the canvas' layers to prepare for fading
     *      3. Draw view's content
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */

    // Step 1, draw the background, if needed
    drawBackground(canvas);

    // skip step 2 & 5 if possible (common case)
    ...
    // Step 2, save the canvas' layers
    if (drawTop) {
        canvas.saveLayer(left, top, right, top + length, null, flags);
    }

    if (drawBottom) {
        canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
    }

    if (drawLeft) {
        canvas.saveLayer(left, top, left + length, bottom, null, flags);
    }

    if (drawRight) {
        canvas.saveLayer(right - length, top, right, bottom, null, flags);
    }
    ...
    // Step 3, draw the content
    if (!dirtyOpaque) onDraw(canvas);

    // Step 4, draw the children
    dispatchDraw(canvas);

    // Step 5, draw the fade effect and restore layers
    ...
    if (drawTop) {
        ...
        canvas.drawRect(left, top, right, top + length, p);
    }

    if (drawBottom) {
        ...
        canvas.drawRect(left, bottom - length, right, bottom, p);
    }

    if (drawLeft) {
        ...
        canvas.drawRect(left, top, left + length, bottom, p);
    }

    if (drawRight) {
        ...
        canvas.drawRect(right - length, top, right, bottom, p);
    }
    ...
    // Step 6, draw decorations (foreground, scrollbars)
    onDrawForeground(canvas);
}

其实注释已经写的很清楚了,View#draw(...)主要做了以下 6 件事:

  1. 绘制背景:drawBackground(...)

  2. 如果有必要的话,保存画布图层:Canvas.saveLayer(...)

  3. 绘制自己onDraw(...),其源码如下:

    // android/view/View.java
    protected void onDraw(Canvas canvas) {
    }
    

    View#onDraw(...)是一个空方法,因为每个 View 的绘制都是不同的,自定义 View 时,通常会覆写该方法,手动绘制该 View 内容。

  4. 绘制子ViewdispatchDraw(...),其源码如下:

    // android/view/View.java
    protected void dispatchDraw(Canvas canvas) {
    }
    

    由于 View 没有子元素,因此其dispatchDraw是一个空实现。

    查看下ViewGroup#dispatchDraw(...),其源码如下:

    // android/view/ViewGroup.java
    @Override
    protected void dispatchDraw(Canvas canvas) {
        ...
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        ...
        for (int i = 0; i < childrenCount; i++) {
            ...
            more |= drawChild(canvas, child, drawingTime);
            ...
        }
        ...
    }
    
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }
    

    可以看到,其内部主要就是遍历子View,最后通过child.draw(...)让子View自己进行绘制。

  5. 如果有必要的话,绘制淡化效果并恢复图层:Canvas.drawRect(...)

  6. 绘制装饰:onDrawForeground(...),其源码如下:

    // android/view/View.java
    public void onDrawForeground(Canvas canvas) {
        onDrawScrollIndicators(canvas);
        onDrawScrollBars(canvas);
    
        final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
        ...
        foreground.draw(canvas);
        }
    }
    

    其实主要就是绘制滚动条,前景图片等视图相关的装饰。

绘制起始流程

我们知道,在Activity启动过程中,会调用到ActivityThread.handleResumeActivity(...),该方法就是 View 视图绘制的起始之处:

// frameworks/base/core/java/android/app/ActivityThread.java
final void handleResumeActivity(IBinder token,
        boolean clearHide, boolean isForward, boolean reallyResume) {
    ...
    // 回调 Activity.onResume() 方法
    ActivityClientRecord r = performResumeActivity(token, clearHide);
    ...
    // 获取当前 Activity 实例
    final Activity a = r.activity;
    ...
    // 此处的 window 为与 Activity 绑定的 PhoneWindow,即 Activity.mWindow
    r.window = r.activity.getWindow();
    // PhoneWindow 绑定的顶层视图:DecorView
    View decor = r.window.getDecorView();
    decor.setVisibility(View.INVISIBLE);
    // 获取与 Activity 绑定的 WindowManager,实际上是 PhoneWindow 的 WindowManager
    ViewManager wm = a.getWindowManager();
    WindowManager.LayoutParams l = r.window.getAttributes();
    ...
    // 添加 DecorView 到 PhoneWindow 上(相当于设置 Activity 根视图)
    wm.addView(decor, l);
    ...
}

可以看到,ActivityThread.handleResumeActivity(...)主要就是获取到当前Activity绑定的ViewManager,最后调用ViewManager.addView(...)方法将DecorView设置到PhoneWindow上,也即设置到当前Activity上。ViewManager是一个接口,WindowManager继承ViewManager,而WindowManagerImpl实现了接口WindowManager,此处的ViewManager.addView(...)实际上调用的是WindowManagerImpl.addView(...),源码如下所示:

// frameworks/base/core/java/android/view/WindowManagerImpl.java
public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    ...
    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
    ...
}

WindowManagerImpl.addView(...)内部转发到WindowManagerGlobal.addView(...)

// frameworks/base/core/java/android/view/WindowManagerGlobal.java
public final class WindowManagerGlobal {
    ...
    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...
        ViewRootImpl root;
        ...
        // 实例化一个 ViewRootImpl
        root = new ViewRootImpl(view.getContext(), display);
        ...
        // 将 ViewRootImpl 与 DecorView 关联到一起
        root.setView(view, wparams, panelParentView);
        ...
    }
    ...
}

WindowManagerGlobal.addView(...)内部,会创建一个ViewRootImpl实例,然后调用ViewRootImpl.setView(...)ViewRootImplDecorView关联到一起:

// frameworks/base/core/java/android/view/ViewRootImpl.java
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
    ...
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        ...
        // 将 DecorView 绑定到 ViewRootImpl.mView 属性上
        mView = view;
        ...
        mWindowAttributes.copyFrom(attrs);
        ...
        // Schedule the first layout -before- adding to the window
        // manager, to make sure we do the relayout before receiving
        // any other events from the system.
        requestLayout();
        ...
    }
    ...
    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            // 检查是否处于主线程
            checkThread();
            ...
            scheduleTraversals();
        }
    }
    ...
}

ViewRootImpl.setView(...)内部首先关联了传递过来的DecorView(通过属性mView指向DecorView即可建立关联),然后最终调用requestLayout(),而requestLayout()内部又会调用方法scheduleTraversals()

// frameworks/base/core/java/android/view/ViewRootImpl.java
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
    ...
    Choreographer mChoreographer;
    ...
    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            // 开始执行绘制
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
    ...
    void scheduleTraversals() {
        if (!mTraversalScheduled) { // 同一帧内不会多次调用遍历
            mTraversalScheduled = true;
            // 发送一个同步屏障
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 将 UI 绘制任务发送到 Choreographer,回调触发 mTraversalRunnable,执行绘制操作
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            ...
        }
    }
    ...
    void doTraversal() {
        ...
        performTraversals();
        ...
    }
    ...
}

ViewRootImpl.scheduleTraversals()内部主要做了两件事:

  1. 调用MessageQueue.postSyncBarrier()方法发送一个同步屏障,同步屏障可以拦截Looper对同步消息的获取与分发,即加入同步屏障后,此时Looper只会获取和处理异步消息,如果没有异步消息,则进入阻塞状态。
  2. 通过Choreographer.postCallback(...)发送一个Choreographer.CALLBACK_TRAVERSAL的异步视图渲染消息。因为前面已经发送了一个同步屏障,因此此处的视图绘制渲染消息会优先被处理。

Choreographer.postCallback(...)会申请一次 VSYNC 中断信号,当 VSYNC 信号到达时,便会回调Choreographer.doFrame(...)方法,内部会触发已经添加的回调任务,Choreographer的回调任务有以下四种类型:

// 回调 INPUT 任务
doCallbacks(Choreographer.CALLBACK_INPUT, mframeTimeNanos);
// 回调 ANIMATION
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
// 回调 View 绘制任务 TRAVERSAL
doCallbacks(Choreographer,CALLBACK_TRAVERSAL, frameTimeNanos);
// API Level 23 新增,COMMIT 
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);

因此,ViewRootImpl.scheduleTraversals(...)内部通过mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)发送的异步视图渲染消息就会得到回调,即回调mTraversalRunnable.run()方法,最终会执行doTraversal()方法,而doTraversal()内部又会调用performTraversals()方法,该方法才是真正开始执行 View 绘制流程的地方,其源码如下所示:

// frameworks/base/core/java/android/view/ViewRootImpl.java
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
    ...
    private void performTraversals() {
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        ...
        // Ask host how big it wants to be
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        ...
        performDraw();
        ...
    }
    ...
}

综上,performTraversals()会依次调用performMeasure(...)performLayout(...)performDraw()三个方法,这三个方法会依次完成顶层View(即DecorView)的测量(measure)、布局(layout)和绘制(draw)流程,具体详情请参考后文。

到此,我们才真正进入 View 绘制流程,总结一下上述流程,如下图所示:

View 绘制起始流程

performMeasure

书接前文,我们知道,真正开始 View 绘制流程是ViewRootImpl.performTraversals(),该方法内部首先进行的是performMeasure(...)流程:

// frameworks/base/core/java/android/view/ViewRootImpl.java
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        // 调用 DecorView.measure(...) 
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

此处的mView其实就是DecorView,其赋值指向在ViewRootImpl.setView(...)中进行,可以看到,performMeasure(...)实际调用的是DecorView.measure(...),所以最终会回调DecorView#onMeasure(...)方法,其源码如下:

// frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
    ...
    private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
        ...
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            ...
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            ...
        }
    ...
}

可以看到,DecorView#onMeasure(...)内部将测量过程交由其父类,即FrameLayout进行处理,那我们看下FrameLayout#onMeasure(...)源码:

// frameworks/base/core/java/android/widget/FrameLayout.java
public class FrameLayout extends ViewGroup {
    ...
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 获取 子View 数量 
        int count = getChildCount();
        ...
        // 最大高度
        int maxHeight = 0;
        // 最大宽度
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) {
            // 获取 子View
            final View child = getChildAt(i);
            // 只对可见的 子View 进行测量
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                // 测量子View
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                // 获取 子View 的布局参数
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                // 获取当前子View的宽度,包含其外边距,记录子View的最大宽度
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                // 记录子View的最大高度
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                ...
            }
        }

        // Account for padding too
        // 最大宽度包含前景偏移量:padding
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        // 最大高度包含前景偏移量:padding
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // Check against our minimum height and width
        // 比较子View 和 系统建议的 子View 最小高度,获取两者中的较大值
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        // 比较子View 和 系统建议的 子View 最小宽度,获取两者中的较大值
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width
        final Drawable drawable = getForeground();
        if (drawable != null) {
            // 子View 高度和 前景图片高度比较,记录其中较大值
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            // 子View 高度和 前景图片宽度比较,记录其中较大值
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }

        // 记录测量结果
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

        ...
    }
    ...
}

FrameLayout的布局特性为:所有 子View 层叠在一起,所以FrameLayout的测量宽/高就是其所有 子View 中最大的宽和高,因此FrameLayout#onMeasure(...)的核心逻辑就是遍历其所有子View,然后通过measureChildWithMargins(...)(该方法前面内容已详细介绍)测量子View,然后就可以获取 子View 的宽/高,记录其中最大的宽/高值,作为自己的测量宽/高。

经过以上步骤,DecorView的测量就已经完成了。

综上,ViewRootImpl#performMeasure(...)其实就是对DecorView的测量过程(DecorView#measure(...)),DecorView是一个FrameLayout,其测量过程主要由FrameLayout#onMeasure(...)负责,内部主要测量逻辑是先遍历所有子View,让 子View 先自己进行测量(child.measure(...)),然后就可以获取 子View 的测量大小,记录所有 子View 中占比最大的测量宽/高,作为自己的最终测量大小。

performLayout

ViewRootImpl#performMeasure(...)完成对DecorView的测量后,接下来执行的是ViewRootImpl#performLayout(...),其源码如下:

// frameworks/base/core/java/android/view/ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {
    ...
    // cache mView since it is used so much below...
    final View host = mView;
    ...
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ...
}

其中,参数lpwidthheight均为MATCH_PARENTdesiredWindowWidthdesiredWindowHeight为屏幕宽/高,mViewDecorView

所以,performLayout(...)内部其实就是调用DecorView#layout(...),前面 layout 流程中介绍过,ViewGroup#layout(...)内部最终会通过View#layout(...)进行布局,而View#layout(...)内部最终通过View#setFrame(...)方法记录四个顶点位置,这样DecorView自己的布局位置就已确定了,即host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight())

确定了DecorView自身的布局位置后,接下来就是要布局其 子View 了,因此,这里最终回调的是DecorView#onLayout(...)方法,其源码如下所示:

// frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
    ...
    private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
        ...
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            super.onLayout(changed, left, top, right, bottom);
            ...
        }
    ...
}

DecorView#onLayout(...)内部转交给FrameLayout#onLayout(...)进行 子View 布局操作,其源码如下:

// frameworks/base/core/java/android/widget/FrameLayout.java
public class FrameLayout extends ViewGroup {
    ...
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        // 布局子View
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

    void layoutChildren(int left, int top, int right, int bottom,
                                  boolean forceLeftGravity) {
        // 获取 子View 数量
        final int count = getChildCount();

        // 左边可放置起始点坐标
        final int parentLeft = getPaddingLeftWithForeground();
        // 右边可放置终点坐标
        final int parentRight = right - left - getPaddingRightWithForeground();

        // 顶部可放置起始点坐标
        final int parentTop = getPaddingTopWithForeground();
        // 底部可放置终点坐标
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        // 遍历 子View
        for (int i = 0; i < count; i++) {
            // 获取 子View
            final View child = getChildAt(i);
            // 不放置状态为 GONE 的子View
            if (child.getVisibility() != GONE) {
                // 获取 子View 布局参数
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                // 获取 子View 测量宽/高
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                // 当前 子View 的布局左边界
                int childLeft;
                // 当前 子View 的布局右边界
                int childTop;
                ...
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }
    ...
}

FrameLayout#onLayout(...)内部是通过FrameLayout#layoutChildren(...)进行 子View 的布局操作,其主要逻辑就是遍历所有 子View,计算得到 子View 的四个顶点位置坐标,最后将结果传递给child.layout(...),让 子View 记录自己在父容器中的布局位置,完成 子View 的布局过程。

综上,ViewRootImpl#performLayout(...)就是对DecorView的布局过程,此过程会递归计算各个 子View 的布局位置,调用 子View 的布局方法,完成各个 子View 的布局。

performDraw

完成了performMeasure(...)performLayout(...)后,最后一步就是performDraw(...)过程,其源码如下:

// frameworks/base/core/java/android/view/ViewRootImpl.java
private void performDraw() {
    ...
    draw(fullRedrawNeeded);
    ...
}

private void draw(boolean fullRedrawNeeded) {
    ...
    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
        return;
    }
    ...
}

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
        boolean scalingRequired, Rect dirty) {
    ...
    mView.draw(canvas);
    ...
}

可以看到,ViewRootImpl#performDraw()内部会经由ViewRootImpl#draw(...)ViewRootImpl#drawSoftware(...),最终执行的还是DecorView#draw(...)过程,其源码如下:

// frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
    ...
    private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
        @Override
        public void draw(Canvas canvas) {
            super.draw(canvas);

            if (mMenuBackground != null) {
                mMenuBackground.draw(canvas);
            }
        }
    ...
}

由于FrameLayout没有覆写draw(...)方法,因此,super.draw(...)最终调用的是View#draw(...)方法,所以DecorView默认采用的就是 View 的绘制方法,具体绘制详情上文已介绍过了,主要就是对DecorView的背景、内容、子View、滚动条等装饰视图进行绘制。

至此,View 绘制的整个流程已基本介绍完毕。

总结

View 的绘制主要有以下一些核心内容:

  1. 三大流程:View 绘制主要包含如下三大流程:

    • measure:测量流程,主要负责对 View 进行测量,其核心逻辑位于View#measure(...),真正的测量处理由View#onMeasure(...)负责。默认的测量规则为:如果 View 的布局参数为LayoutParams.WRAP_CONTENTLayoutParams.MATCH_PARENT,那么其测量大小为 SpecSize;如果其布局参数为LayoutParams.UNSPECIFIED,那么其测量大小为android:minWidth/android:minHeight和其背景之间的较大值。

    自定义View 通常覆写onMeasure(...)方法,在其内一般会对WRAP_CONTENT预设一个默认值,区分WARP_CONTENTMATCH_PARENT效果,最终完成自己的测量宽/高。而ViewGrouponMeasure(...)方法中,通常都是先测量子View,收集到相应数据后,才能最终测量自己。

    • layout:布局流程,主要完成对 View 的位置放置,其核心逻辑位于View#layout(...),该方法内部主要通过View#setFrame(...)记录自己的四个顶点坐标(记录与对应成员变量中即可),完成自己的位置放置,最后会回调View#onLayout(...)方法,在其内完成对 子View 的布局放置。

      :不同于 measure 流程首先对 子View 进行测量,最后才测量自己,layout 流程首先是先定位自己的布局位置,然后才处理放置 子View 的布局位置。

    • draw:绘制流程,就是将 View 绘制到屏幕上,其核心逻辑位于View#draw(...),主要就是对 背景自身内容(onDraw(...)子View(dispatchDraw(...)装饰(滚动条、前景等) 进行绘制。

      :通常自定义View 覆写onDraw(...)方法,完成自己的绘制即可,ViewGroup 一般充当容器使用,因此通常无需覆写onDraw(...)

  2. Activity 的根视图(即DecorView)最终是绑定到ViewRootImpl,具体是由ViewRootImpl#setView(...)进行绑定关联的,后续 View 绘制的三大流程都是均有ViewRootImpl负责执行的。

  3. 对 View 的测量流程中,最关键的一步是求取 View 的MeasureSpec,View 的MeasureSpec是在其父容器MeasureSpec的约束下,结合自己的LayoutParams共同测量得到的,具体的测量逻辑由ViewGroup#getChildMeasureSpec(...)负责。
    DecorViewMeasureSpec取决于自己的LayoutParams和屏幕尺寸,具体的测量逻辑位于ViewRootImpl#getRootMeasureSpec(...)

最后,稍微总结一下 View 绘制的整个流程:

  1. 首先,当 Activity 启动时,会触发调用到ActivityThread#handleResumeActivity(..),其内部会经历一系列过程,生成DecorViewViewRootImpl等实例,最后通过ViewRootImpl#setView(decor,MATCH_PARENT)设置 Activity 根View。

    ViewRootImpl#setView(...)内容通过将其成员属性ViewRootImpl#mView指向DecorView,完成两者之间的关联。

  2. ViewRootImpl成功关联DecorView后,其内部会设置同步屏障并发送一个CALLBACK_TRAVERSAL异步渲染消息,在下一次 VSYNC 信号到来时,CALLBACK_TRAVERSAL就会得到响应,从而最终触发执行ViewRootImpl#performTraversals(...),真正开始执行 View 绘制流程。

  3. ViewRootImpl#performTraversals(...)内部会依次调用ViewRootImpl#performMeasure(...)ViewRootImpl#performLayout(...)ViewRootImpl#performDraw(...)三大绘制流程,其中:

    • performMeasure(..):内部主要就是对DecorView执行测量流程:DecorView#measure(...)DecorView是一个FrameLayout,其布局特性是层叠布局,所占的空间就是其 子View 占比最大的宽/高,因此其测量逻辑(onMeasure(...))是先对所有 子View 进行测量,具体是通过ViewGroup#measureChildWithMargins(...)方法对 子View 进行测量,子View 测量完成后,记录最大的宽/高,设置为自己的测量大小(通过View#setMeasuredDimension(...)),如此便完成了DecorView的测量流程。

    • performLayout(...):内部其实就是调用DecorView#layout(...),如此便完成了DecorView的布局位置,最后会回调DecorView#onLayout(...),负责 子View 的布局放置,核心逻辑就是计算出各个 子View 的坐标位置,最后通过child.layout(...)完成 子View 布局。

    • performDraw():内部最终调用到的是DecorView#draw(...),该方法内部并未对绘制流程做任何修改,因此最终执行的是View#draw(...),所以主要就是依次完成对DecorView背景子View(dispatchDraw(...)视图装饰(滚动条、前景等) 的绘制。

参考

上一篇下一篇

猜你喜欢

热点阅读