View绘制流程--Based on kitkat

从Acitivty的启动开始,我们就看到setContentView(见从setContentView()谈起)是如何创建和初始化的,但不清楚视图View如何添加到窗口以及绘制到窗口的,那下面我们就一起来看一下视图View是如何绘制的。

1. View绘制的触发

可能很多人看过一些文章或者书籍,大概都知道ViewRootImpl.performTraversals()是绘制的开始关键方法调用,可这个方法是怎么调用到的呢,他的流程是怎么样的呢?接下来我们就一起看下View绘制是如何触发的。

startActivity

首先我们从Activity的启动中就可以看到调用ActivityThread.handleLaunchActivity(ActivityClientRecord r, Intent customIntent),在这个函数里面有两个关键的调用:
  1. 调用performLaunchActivity来进行Activity的创建和View的初始化(setContentView相关)
  2. 调用handleResumeActivity将视图View添加到窗口并绘制
    public final class ActivityThread {
        private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
            ……
    
            1. 调用performLaunchActivity来进行Activity的创建和View的初始化(setContentView相关)
            if (localLOGV) Slog.v(
                TAG, "Handling launch of " + r);
            Activity a = performLaunchActivity(r, customIntent);
    
            if (a != null) {
                r.createdConfig = new Configuration(mConfiguration);
                Bundle oldState = r.state;
                2. 调用handleResumeActivity将视图View添加到窗口并绘制
                handleResumeActivity(r.token, false, r.isForward,
                        !r.activity.mFinished && !r.startsNotResumed);
    
                ……
            } else {
                ……
            }
        }
    }
这两个调用之后我们可以看到了界面了,而界面是怎么绘制的,我们来通过另外一张顺序图看下绘制是如何触发的。
    public final class ActivityThread {
        final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward,
                    boolean reallyResume) {
                // If we are getting ready to gc after going to the background, well
                // we are back active so skip it.
                unscheduleGcIdler();
                
                1. 调用performResumeActivity执行resume相关操作,最终会调用到activity的onResume
                ActivityClientRecord r = performResumeActivity(token, clearHide);
        
                if (r != null) {
                    final Activity a = r.activity;
        
                    if (localLOGV) Slog.v(
                        TAG, "Resume " + r + " started activity: " +
                        a.mStartedActivity + ", hideForNow: " + r.hideForNow
                        + ", finished: " + a.mFinished);
        
                    final int forwardBit = isForward ?
                            WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
        
                    // If the window hasn't yet been added to the window manager,
                    // and this guy didn't finish itself or start another activity,
                    // then go ahead and add the window.
                    boolean willBeVisible = !a.mStartedActivity;
                    if (!willBeVisible) {
                        try {
                            willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible(
                                    a.getActivityToken());
                        } catch (RemoteException e) {
                        }
                    }
                    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 = true;
                            2. 调用WindowManagerImpl的addView方法将performLaunchActivity的过程创建的DecorView对象decor与WMS关联起来
                            wm.addView(decor, l);
                        }
        
                    ……
                    }
        
                    ……
        
                    // Tell the activity manager we have resumed.
                    if (reallyResume) {
                        try {
                            ActivityManagerNative.getDefault().activityResumed(token);
                        } catch (RemoteException ex) {
                        }
                    }
        
                } else {
                    ……
                }
            }
    }

怎么触发绘制

从上图里面我们看到handleResumeActicvity通过调用WindowManagerImpl的addView方法将performLaunchActivity的过程创建的DecorView对象decor与Window关联起来。addView通过一系列的调用,最终调用ViewRootImpl.performTraversals(),从而触发了绘制,到此View绘制得以触发。

2. ViewRootImpl.performTraversals()详细分析

performTraversals

performTraversals函数是进行View的遍历的核心函数。该函数逻辑很清晰,主要分为4大步骤:

  • Step 1. 创建Surface,并打通native层(relayoutWindow)
  • Step 2. 计算视图大小(performMeasure)
  • Step 3. 布局,将视图放置在合适位置(performLayout)
  • Step 4. 绘制(performDraw)

在relayoutWindow之前还有很长一段代码,这段代码到底做了什么呢,下面我们就先分析下。

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
    private void performTraversals() {
        // cache mView since it is used so much below...
        final View host = mView;

        ……
        1. 初始化attchInfo
        final View.AttachInfo attachInfo = mAttachInfo;

        ……

        Rect frame = mWinFrame;
        2. 判断是否是第一次遍历
        if (mFirst) {
            mFullRedrawNeeded = true;
            mLayoutRequested = true;

            ……
            3. 如果是第一次遍历,就调用dispatchAttachedToWindow,所有子视图都把attachInfo的值复制到自己的mAttachInfo中
            host.dispatchAttachedToWindow(attachInfo, 0);
            attachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
            mFitSystemWindowsInsets.set(mAttachInfo.mContentInsets);
            host.fitSystemWindows(mFitSystemWindowsInsets);
            //Log.i(TAG, "Screen on initialized: " + attachInfo.mKeepScreenOn);

        } else {
            desiredWindowWidth = frame.width();
            desiredWindowHeight = frame.height();
            4. 如果不是第一次,判断窗口大小是否有变化,如果有,则会将下面三个变量置为true。
            - mFullRedrawNeeded = true,需要全部重绘
            - mLayoutRequested = true,需要重新布局即重新为视图指定位置
            - windowSizeMayChange = true,窗口大小可能改变
            if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
                if (DEBUG_ORIENTATION) Log.v(TAG,
                        "View " + host + " resized to: " + frame);
                mFullRedrawNeeded = true;
                mLayoutRequested = true;
                windowSizeMayChange = true;
            }
        }
        5. 如果visibility发生变化,将调用host.dispatchWindowVisibilityChanged将这个变化通知给所有的子视图
        if (viewVisibilityChanged) {
            attachInfo.mWindowVisibility = viewVisibility;
            host.dispatchWindowVisibilityChanged(viewVisibility);
            if (viewVisibility != View.VISIBLE || mNewSurfaceNeeded) {
                destroyHardwareResources();
            }
            if (viewVisibility == View.GONE) {
                // After making a window gone, we will count it as being
                // shown for the first time the next time it gets focus.
                mHasHadWindowFocus = false;
            }
        }

        // Execute enqueued actions on every traversal in case a detached view enqueued an action
        getRunQueue().executeActions(attachInfo.mHandler);

        boolean insetsChanged = false;

        boolean layoutRequested = mLayoutRequested && !mStopped;
        if (layoutRequested) {

            ……

            // Ask host how big it wants to be
            6. 测量判断window的大小是否需要改变
            windowSizeMayChange |= measureHierarchy(host, lp, res,
                    desiredWindowWidth, desiredWindowHeight);
        }

        ……

            boolean hwInitialized = false;
            boolean contentInsetsChanged = false;
            boolean hadSurface = mSurface.isValid();

            try {
                if (DEBUG_LAYOUT) {
                    Log.i(TAG, "host=w:" + host.getMeasuredWidth() + ", h:" +
                            host.getMeasuredHeight() + ", params=" + params);
                }

                final int surfaceGenerationId = mSurface.getGenerationId();
                7. 重新分配窗口大小,创建Surface,并打通native层
                relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
                ……
            }
    }

Step1. 重新分配窗口大小,创建Surface,并打通native层(relayoutWindow)

在relayoutWindow调用中会去调用WMS的relayoutWindow来调整窗口属性并将上次Surface对象与底层Surface打通。我们可以从下图中清晰地看出打通的过程:
  1. 创建底层surface对象
  2. 通过copyFrom将native层和上层的Surface对象关联
    surface创建与关联过程

Step2. 计算视图大小(performMeasure)

performMeasure
首先,Android系统希望程序员能够理解画布(Canvas)是没有边界的,即无穷大。程序员可以在画布上绘制任意多任意大的东西(只要内存足够)。我们在配置视图布局的时候可以配置具体的值(比如250dp),可以配置相对值(比如WRAP_CONTENT、MATCH_PARENT),measure过程就是把视图布局中的相对值转换为具体值,最后保存在mMeasuredWidth和mMeasureHeight中。
measure过程中一个关键调用就是View.measure,该方法是final的,因此不能被重载,该函数会回调onMeasure方法,如果我们自定义的View,这边就是MyView,是一个View的子类,则执行MyView的onMeasure或者View的onMeasure,如果MyView没有重写这个方法;如果MyView是一个ViewGroup的对象则在重写onMeasure方法的时候需要调用ViewGroup的measureChildWithMargins来对它的子视图进行计算。
View.measure方法调用时候的两个参数widthMeasureSpec和heightMeasureSpec对应MeasureSpec的是视图大小计算时候的说明,视图的大小由父视图和子视图共同决定,而这个说明里面包含了父视图的大小和specMode。specMode有如下三种:
- MeasureSpec.EXACTLY:“确定的”,父视图希望子视图的大小是specSize中指定的值。
- MeasureSpec.AT_MOST:“最多”,子视图的大小最多是specSize的值
- MeasureSpec.UNSPECIFIED:“没有限制”,View的设计者可以根据自身特性设置视图大小

Step 3. 布局,将视图放置在合适位置(performLayout)

performLayout

layout的目的就是父视图按照子视图的大小和布局参数,将子视图放置到合适的位置上。布局过程主要是通过调用View.layout实现的。该函数主要做了三件事情:
1. 调用setFrame将位置的参数保存起来。如果这些值跟以前的相同则什么也不做,如果不同则进行重新赋值,并在赋值前,会给mPrivateFlags添加PFLAG_DRAWN的标识,同时调用invalidate告诉系统View视图原先占用的位置需要重绘。
2. 回调onLayout,View本身的onLayout什么也不做,提供此方法是为了方便ViewGroup进行重写来对它的子视图进行布局。需要了解详细onLayout实现可以看下LinearLayout、RelativeLayout等ViewGroup的onLayout实现。

Step 4. 绘制(performDraw)

performDraw

绘制顾名思义就是把视图View对象绘制到屏幕上。**每次重绘的时候并不会重新绘制每个View树的视图,而只是绘制那些“需要重绘”的,也就是mPrivateFlags中含有PFLAG_DRAWN标识的视图**
由上图我们可以看出绘制过程经过一系列的调用和准备,其中之一就是检查Surface的有效性,此Surface就是前文relayoutWindow过程中创建的Surface,最终会调用到mView.draw,该函数主要做了6件事:
1. 绘制背景,每个视图都有一个背景,可以是一个颜色值,也可以是一幅图片,甚至是任何Drawable对象,
2. 如果需要,保存画布的层准备用于渐变
3. 绘制View的内容,对于TextView而言,内容就是文字,对于ImageButton而言,内容就是一副图片;程序会在onDraw方法中绘制具体的内容
4. 绘制View的子视图
5. 如果需要,绘制渐变框和恢复画布层,渐变框作为是为了让视图View看起来更有层次感,其本质是一个Shader对象
6. 绘制装饰(比如滚动条)
posted @ 2014-08-29 15:53  Leslie Guan  阅读(1259)  评论(0编辑  收藏  举报