zoukankan      html  css  js  c++  java
  • 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. 绘制装饰(比如滚动条)
  • 相关阅读:
    BZOJ 2034 【2009国家集训队】 最大收益
    vijos P1780 【NOIP2012】 开车旅行
    BZOJ 2115 【WC2011】 Xor
    BZOJ 3631 【JLOI2014】 松鼠的新家
    BZOJ 4717 改装
    BZOJ 2957 楼房重建
    BZOJ 4034 【HAOI2015】 T2
    BZOJ 1834 【ZJOI2010】 network 网络扩容
    BZOJ 2440 【中山市选2011】 完全平方数
    BZOJ 2733 【HNOI2012】 永无乡
  • 原文地址:https://www.cnblogs.com/GMCisMarkdownCraftsman/p/3945138.html
Copyright © 2011-2022 走看看