• 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. 绘制装饰(比如滚动条)
  • 相关阅读:
    Shell入门教程:命令替换 $() 和 ``
    CentOS启用sudo,禁用root远程登录
    .htaccess 基础教程(四)Apache RewriteCond 规则参数
    .htaccess 基础教程(三)RewriteCond标志符,RewriteRule适用的标志符
    .htaccess 基础教程(二)
    .htaccess 基础教程(一)
    phpMyAdmin 个性化设置,字体大小设置,去掉“以树形显示数据库”,禁用“发送错误报告”
    PHP的$_SERVER['PHP_SELF']造成的XSS漏洞攻击及其解决方案
    PHP变量作用域(花括号、global、闭包)
    获取PHP文件绝对地址$_SERVER['SCRIPT_FILENAME'] 与 __FILE__ 的区别
  • 原文地址:https://www.cnblogs.com/GMCisMarkdownCraftsman/p/3945138.html
Copyright © 2020-2023  润新知