Android绘制原理【通透版】

news2024/12/23 23:54:34

概述

Android 的显示过程可以简单概括为:Android 应用程序把经过测量、布局、绘制后的surface 缓存数据,通过 SurfaceFlinger 把数据渲染到显示屏幕上,通过 Android 的刷新机制 来刷新数据。也就是说应用层负责绘制,系统层负责渲染,通过进程间通信把应用层需要 绘制的数据传递到系统层服务,系统层服务通过刷新机制把数据更新到屏幕。 通过阅读 Android 系统的源码可以了解显示的流程,Android 的图形显示系统采用的是 Client/Server 架构。SurfaceFlinger(Server)由 C++代码编写。Client 端代码分为两部分,一 部分是由 Java 提供给应用使用的 API,另一部分则是由 C++写成的底层具体实现。

Android的绘图原理浅析

Android屏幕绘制

关于绘制,就要从performDraw()说起,我们来看一下这个流程到底是怎么绘制的.

//ViewRootImpl
//1
 private void performDraw() {
    try {
        draw(fullRedrawNeeded);
    } finally {
        mIsDrawing = false;
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
 }
​
 //2
  private void draw(boolean fullRedrawNeeded) {
    Surface surface = mSurface;
    if (!surface.isValid()) {
            return;
        }
​
     if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
          return;
      }
  }
​
 //3
    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {
​
     Canvas canvas = mSurface.lockCanvas(dirty);
 }        

看代码执行流程,1—>2->3, 最终拿到了Java层的canvas,然后进行一系列绘制操作.而canvas是通过Suface.lockCanvas()得到的.

那么Surface又是一个什么呢?在这里Surface只是一个抽象,在APP创建窗口时,会调用WindowManager向WMS服务发起一个请求,携带上surface对象,只有他被分配完一段屏幕缓冲区才能真正对应屏幕上的一个窗口.

来看一下Framework中的绘图架构.更好的理解Surface

Surface本质上仅仅代表了一个平面,绘制不同图案显然是一种操作,而不是一段数据,Android使用了Skia绘图驱动库来进行平面上的绘制,在程序中使用canvas来表示这个功能.

双缓冲技术的介绍

在ViewRootImpl中,我们看到接收到绘制消息后,不是立刻绘制而是调用scheduleTraversals,在scheduleTraversals调用Choreographer.postCallback(),这又是因为什么呢?这其实涉及到屏幕绘制原理(除了Android其他平台也是类似的).

我们都知道显示器以固定的频率刷新,比如 iPhone的 60Hz、iPad Pro的 120Hz。当一帧图像绘制完毕后准备绘制下一帧时,显示器会发出一个垂直同步信号(VSync),所以 60Hz的屏幕就会一秒内发出 60次这样的信号。

并且一般地来说,计算机系统中,CPU、GPU和显示器以一种特定的方式协作:CPU将计算好的显示内容提交给 GPU,GPU渲染后放入帧缓冲区,然后视频控制器按照 VSync信号从帧缓冲区取帧数据传递给显示器显示.

但是如果屏幕的缓冲区只有一块,那么这个VSync同步信号发出时, 开始刷新屏幕,那么你看到的屏幕就是一条一条的数据在变化.为了让屏幕看上去是一帧一帧的数据,一般都有两块缓冲区(也被成为双缓冲区).当数据要刷新时,直接替换另一个缓冲区的数据.

双缓冲技术里面,如果不能特定时间刷新完的话(如果60HZ的话,就是16ms内)把这个缓冲区数据刷新完成,屏幕发出VSync同步信号,无法完成两个缓冲区的切换,那么就会造成卡顿现象.

回到scheduleTraversals()上,这个地方就是使用了双缓冲技术(或者三缓冲技术),Choreographer接收VSync的同步信号,当屏幕刷新来时,开始屏幕的刷新操作.

Android的Activity界面从何处开始绘制

  • Activity启动之后,灰溜溜的会去跑onCreate,onStart,onResume等生命周期方法,那么从framework里面,让我们看看Activity在onResume之前都在干些啥? 在ActivityThread中的handleResumeActivity方法中先调用了performResumeActivity方法,而performResumeActivity中就会去回调Activity中的onResume方法,在performResumeActivity后,又做了很多事,所以如果在onResume中去做一些耗时操作是无法加速页面启动的,因为这时候页面还没展示出来呢,这里额外提一句,接着唠,performResumeActivity执行完了之后
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 (r.mPreserveWindow) {
                    a.mWindowAdded = true;
                    r.mPreserveWindow = false;
                    // Normally the ViewRoot sets up callbacks with the Activity
                    // in addView->ViewRootImpl#setView. If we are instead reusing
                    // the decor view we have to notify the view root that the
                    // callbacks may have changed.
                    ViewRootImpl impl = decor.getViewRootImpl();
                    if (impl != null) {
                        impl.notifyChildRebuilt();
                    }
                }
                if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        wm.addView(decor, l);
                    } else {
                        // The activity will get a callback for this {@link LayoutParams} change
                        // earlier. However, at that time the decor will not be set (this is set
                        // in this method), so no action will be taken. This call ensures the
                        // callback occurs with the decor set.
                        a.onWindowAttributesChanged(l);
                    }
                }
​
            // If the window has already been added, but during resume
            // we started another activity, then don't yet make the
            // window visible.
            }

从普遍的意义上来讲,以上这段代码就是设置WindowManager和DecorView的关系,继续看:

if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        wm.addView(decor, l);
                    } else {
                        // The activity will get a callback for this {@link LayoutParams} change
                        // earlier. However, at that time the decor will not be set (this is set
                        // in this method), so no action will be taken. This call ensures the
                        // callback occurs with the decor set.
                        a.onWindowAttributesChanged(l);
                    }
                }

wm.addView(decor, l)这句就是把DecorView加入到WindowManager当中,那么WindowManager是什么东西呢?在这句代码打了一个断点,然后层层跟进来,WindowManagerImpl:

public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

再继续执行了WindowManagerGlobal的addView,此时addView中第一个参数View就是DecorView,由于代码太长,只贴关键部分:

root = new ViewRootImpl(view.getContext(), display);
​
            view.setLayoutParams(wparams);
​
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
​
            // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }

这里创建了一个ViewRootImpl对象root,然后调用root的setView方法,这个方法也太长,但是我可以负责任的告诉你,setView方法中调用了requestLayout()方法:

@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

到这里,好了,界面绘制从这里开始:

void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

这个异常是不是经常碰到?这里就是要求界面绘制必须在主线程中进行更新mThread就是主线程,所以如果当前运行的线程不是主线程,那么就直接报这个异常,接下去是mLayoutRequested设置成true,避免在界面又layout请求的时候又重复收到一次layout请求,然后再进行界面的layout,那不就乱套了,然后就是真正的界面绘制了:

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

核心在这里:

mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

调用了Choreographer中的方法,方法中的mTraversalRunnable如下:

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
​
void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
​
            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }
​
            performTraversals();
​
            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

最终会调用performTraversals方法,然后,同学们,然后就开始View的measure了,在ViewRootImpl中是去measure前面提到的DecordeView

// Ask host how big it wants to be
            windowSizeMayChange |= measureHierarchy(host, lp, res,
                    desiredWindowWidth, desiredWindowHeight);

measure阶段

measureHierarchy中的第一个参数host就是DecordeView对象,后面两个参数是屏幕的宽和高,然后根据lp,也就是LayoutParams的属性,例如lp.width,lp.height来求出View的宽和高,调用performMeasure方法:

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;
        boolean windowSizeMayChange = false;
​
        if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(mTag,
                "Measuring " + host + " in display " + desiredWindowWidth
                + "x" + desiredWindowHeight + "...");
​
        boolean goodMeasure = false;
        if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            // On large screens, we don't want to allow dialogs to just
            // stretch to fill the entire width of the screen to display
            // one line of text.  First try doing the layout at a smaller
            // size to see if it will fit.
            final DisplayMetrics packageMetrics = res.getDisplayMetrics();
            res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
            int baseSize = 0;
            if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
                baseSize = (int)mTmpValue.getDimension(packageMetrics);
            }
            if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": baseSize=" + baseSize
                    + ", desiredWindowWidth=" + desiredWindowWidth);
            if (baseSize != 0 && desiredWindowWidth > baseSize) {
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured ("
                        + host.getMeasuredWidth() + "," + host.getMeasuredHeight()
                        + ") from width spec: " + MeasureSpec.toString(childWidthMeasureSpec)
                        + " and height spec: " + MeasureSpec.toString(childHeightMeasureSpec));
                if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                    goodMeasure = true;
                } else {
                    // Didn't fit in that size... try expanding a bit.
                    baseSize = (baseSize+desiredWindowWidth)/2;
                    if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": next baseSize="
                            + baseSize);
                    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured ("
                            + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
                    if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                        if (DEBUG_DIALOG) Log.v(mTag, "Good!");
                        goodMeasure = true;
                    }
                }
            }
        }
​
        if (!goodMeasure) {
            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                windowSizeMayChange = true;
            }
        }
​
        if (DBG) {
            System.out.println("======================================");
            System.out.println("performTraversals -- after measure");
            host.debug();
        }
​
        return windowSizeMayChange;
    }
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

上面的代码中调用getRootMeasureSpec()方法来获取根MeasureSpec,这个根MeasureSpec代表了对decorView的宽高的约束信息。继续分析之前,我们先来简单地介绍下MeasureSpec的概念。 MeasureSpec是一个32位整数,由SpecMode和SpecSize两部分组成,其中,高2位为SpecMode,低30位为SpecSize。SpecMode为测量模式,SpecSize为相应测量模式下的测量尺寸。View(包括普通View和ViewGroup)的SpecMode由本View的LayoutParams结合父View的MeasureSpec生成。 SpecMode的取值可为以下三种:

  • MeasureSpec.EXACTLY,对应LayoutParams.MATCH_PARENT,表示后View需要一个确切的尺寸
  • MeasureSpec.AT_MOST,对应LayoutParams.WRAP_CONTENT,View的尺寸是自适应的,但是不能超过给定的SpecSize
  • MeasureSpec.UNSPECIFIED这个在View的measure过程中没有用到,看注解的意思是这种模式是对View没有限定,尺寸可以任意
 /**
         * 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;
​
​
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;
    }

在performMeasure方法中调用了View的measure方法,到这也就比较熟悉了,measure会根据条件判断是否需要layout:

  • forceLayout为true:这表示强制重新布局,可以通过View.requestLayout()来实现;
  • needsLayout为true,这需要specChanged为true(表示本次传入的MeasureSpec与上次传入的不同),而且需要以下三个条件满足一个:
  1. sAlwaysRemeasureExactly为true,该系统变量默认为false
  2. isSpecExactly为false,这个变量表示此次测量需要精确测量MeasureSpec.EXACTLY,也就是View的属性设置为LayoutParams.MATCH_PARENT
  3. matchesSpecSize为false,该变量表示此次测量出来的宽高信息和上一次的不一样,也就是说View的尺寸发生了改变 如果满足测量条件,那就会调用onMeasure方法,而对于DecorView来说,是调用了FrameLayout的onMeasure:
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
​
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();
​
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;
​
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }
​
        // Account for padding too
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
​
        // Check against our minimum height and width
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
​
        // Check against our foreground's minimum height and width
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }
​
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
​
        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
​
                final int childWidthMeasureSpec;
                if (lp.width == LayoutParams.MATCH_PARENT) {
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }
​
                final int childHeightMeasureSpec;
                if (lp.height == LayoutParams.MATCH_PARENT) {
                    final int height = Math.max(0, getMeasuredHeight()
                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                            - lp.topMargin - lp.bottomMargin);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            height, MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                }
​
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

对于FrameLayout的onMeasure而言,会遍历所有子View,如果是可见的那么就调用measureChildWithMargins方法测量子View,然后会考虑margin和padding属性,考虑FrameLayout的背景图,计算出maxWidth和maxHeight,这个尺寸就是FrameLayout的能完全摆放所有子View的最大尺寸,所以一个ViewGroup需要把所有的子View都测量完成之后才能得出自身的尺寸,再来看下measureChildWithMargins都做了什么:

        int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
​
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
​
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

对于ViewGroup来说调用此方法就是测量它的子View,传进去的参数就是对子View的尺寸的限制 对于普通的View来说,普通的View的measure过程最终也会调用到onMeasure方法:

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

仅仅是把测量的宽高信息设置以下,所以ViewGroup的measure操作最终会是的子View都完成measure操作,包括ViewGroup本身在所有子View完成measure操作之后,也会最终确定自身的尺寸并设置

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.AT_MOST和MeasureSpec.EXACTL模式,都是返回specSize作为结果,所以在View中我们可以控制View的width和height属性,对应wrap_content和match_parant 到这,View的measure过程就结束了,接下去是View的layouut过程,还是在performTraversals方法中

layout阶段

对View进行measure完成之后(measureHierarchy方法)会再执行performLayout(lp, mWidth, mHeight):

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        mLayoutRequested = false;
        mScrollMayChange = true;
        mInLayout = true;

        final View host = mView;
        if (host == null) {
            return;
        }
        if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
            Log.v(mTag, "Laying out " + host + " to (" +
                    host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
        }

        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

            mInLayout = false;
            int numViewsRequestingLayout = mLayoutRequesters.size();
            if (numViewsRequestingLayout > 0) {
                // requestLayout() was called during layout.
                // If no layout-request flags are set on the requesting views, there is no problem.
                // If some requests are still pending, then we need to clear those flags and do
                // a full request/measure/layout pass to handle this situation.
                ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
                        false);
                if (validLayoutRequesters != null) {
                    // Set this flag to indicate that any further requests are happening during
                    // the second pass, which may result in posting those requests to the next
                    // frame instead
                    mHandlingLayoutInLayoutRequest = true;

                    // Process fresh layout requests, then measure and layout
                    int numValidRequests = validLayoutRequesters.size();
                    for (int i = 0; i < numValidRequests; ++i) {
                        final View view = validLayoutRequesters.get(i);
                        Log.w("View", "requestLayout() improperly called by " + view +
                                " during layout: running second layout pass");
                        view.requestLayout();
                    }
                    measureHierarchy(host, lp, mView.getContext().getResources(),
                            desiredWindowWidth, desiredWindowHeight);
                    mInLayout = true;
                    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

                    mHandlingLayoutInLayoutRequest = false;

                    // Check the valid requests again, this time without checking/clearing the
                    // layout flags, since requests happening during the second pass get noop'd
                    validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
                    if (validLayoutRequesters != null) {
                        final ArrayList<View> finalRequesters = validLayoutRequesters;
                        // Post second-pass requests to the next frame
                        getRunQueue().post(new Runnable() {
                            @Override
                            public void run() {
                                int numValidRequests = finalRequesters.size();
                                for (int i = 0; i < numValidRequests; ++i) {
                                    final View view = finalRequesters.get(i);
                                    Log.w("View", "requestLayout() improperly called by " + view +
                                            " during second layout pass: posting in next frame");
                                    view.requestLayout();
                                }
                            }
                        });
                    }
                }

            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        mInLayout = false;
    }

自然就调用了View的host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()),host就是DecorView,layout操作是在measure完成的基础上进行的会用到刚才View进行measure后的宽和高的信息host.getMeasuredWidth(), host.getMeasuredHeight(),在layou方法中会回调到onLayout方法,此方法默认是空实现,对于父控件来说,可以在onLayout方法中去操作子View的layout操作,而对于DecorView来说,onLayout:

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();

        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;

                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }

                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }

                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

上面所做的事就是根据FrameLayout的布局属性来遍历所有子View的位置信息,然后调用child.layout(childLeft, childTop, childLeft + width, childTop + height)触发子View的layout操作,然后就执行View的layout方法

draw阶段

draw操作在performTraversals方法里交给performDraw去执行,然后会在调用DecorView的draw方法:

if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);
​
            // Step 4, draw the children
            dispatchDraw(canvas);
​
            drawAutofilledHighlight(canvas);
​
            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }
​
            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);
​
            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);
​
            if (debugDraw()) {
                debugDrawFocus(canvas);
            }
​
            // we're done...
            return;
        }

对于ViewGroup来说dispatchDraw就是去绘制子 View,具体实现在drawChild方法:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

draw的过程比较复杂,又涉及到硬件加速,绘制缓存等操作。

以上是Android开发中的绘制原理解析,更多Android技术可前往《Android核心技术手册》查看了解学习。里面技术积累了30多个技术文档;上千个小技术点。

总结

  • Activity的绘制流程在onResume之后开始,切入点是ViewRootImpl类的performTraversals方法
  • View的绘制流程是measure,layout,draw
  • ViewGroup在绘制过程中会先遍历子View然后才会绘制自身,例如先遍历measure子View,然后再计算出自身的尺寸

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/136369.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

豪掷千亿的卡塔尔,为什么建造可回收的足球场?

作者&#xff1a;木环 来自世界六大洲的八强&#xff0c;正为大力神杯做最后的角逐。在精彩的比赛之外&#xff0c;这届世界杯还留下了很多破圈元素&#xff1a;卡塔尔的两千亿、逗笑的饺子皮表情包、推迟到冬季举办的赛事、围困啤酒赞助商的禁酒令。 以及&#xff0c;一个标…

Jackson 自定义序列化器的使用

注意&#xff1a;从简单的例子去延伸使用&#xff0c;内容紧凑&#xff0c;排版就这样了哈 举例&#xff1a;自定义序列化器&#xff0c;让输出json的指定字段金额10 public class testMain {public static void main(String[] args) throws JsonProcessingException {Bill b…

【BP靶场portswigger-服务端5】业务逻辑漏洞-11个实验(全)

目录 一、业务逻辑漏洞 1、意义 2、业务逻辑漏洞的产生 3、产生的影响 二、过度信任客户端控件 1、简述 实验1&#xff1a;过度信任客户端控件 三、未处理非常规输入 1、简述 实验2&#xff1a;高级逻辑漏洞 实验5&#xff1a;低级逻辑缺陷 实验6&#xff1a;异常输入处…

服务器防勒索病毒经验分享

场景描述 互联网飞速发展&#xff0c;各企业均搭建了自己的服务器。包括文件服务器、代码服务器、业务系统服务器、数据库服务器等。目前大多数企业使用的服务器系统以Windows Server和Linux&#xff08;Centos&#xff09;居多&#xff0c;但随着国产操作系统的崛起&#xff…

Altium Designer 20 凡亿教育视频学习-03

第三部分学习 封装具备的元素 紫色部分就是阻焊部分&#xff0c;阻焊作用&#xff1a;防止绿油覆盖 封装制作 Tab键调出属性菜单 当放置焊盘的时候&#xff0c;按Tab键&#xff0c;可以打开属性面板&#xff0c;并且更改是贴片还是插孔的 测量距离 Ctrl M 测量功能 测量…

Day 17-其它 Composition API_ 依赖注入provide / inject

Provide / Inject 当我们需要从父组件向子组件传递数据时&#xff0c;我们使用 props可以实现&#xff01; 想象一下这样的结构&#xff1a;有一些深度嵌套的组件&#xff0c;而深层的子组件只需要父组件的部分内容。在这种情况下&#xff0c;如果仍然将 props 沿着组件链逐级…

CORS 跨域资源共享

目录 1. 接口的跨域问题 2. 使用 cors 中间件解决跨域问题 3. 什么是 CORS 4. CORS 的注意事项 5. CORS 响应头部 - Access-Control-Allow-Origin 6. CORS 响应头部 - Access-Control-Allow-Headers 7. CORS 响应头部 - Access-Control-Allow-Methods 8. CORS请求的…

SpringMVC程序详解

1.什么是 Spring MVC&#xff1f; Spring Web MVC 是基于 Servlet API 构建的原始 Web 框架&#xff0c;从⼀开始就包含在 Spring 框架中。它的正式名称“Spring Web MVC”来⾃其源模块的名称(Spring-webmvc)&#xff0c;但它通常被称为“Spring MVC”。 从上述定义我们可以得…

消息服务 + Serverless 函数计算如何助力企业降本提效?

作者&#xff1a;柳下 背景介绍 消息队列服务&#xff08;下文均以 Message Service 命名&#xff09;作为云计算 PaaS 领域的基础设施之一&#xff0c;其高并发、削峰填谷的特性愈发受到开发者关注。Message Service 对上承接消息生产者服务的请求&#xff0c;对下连接消费者…

指令格式与寻址方式

指令与指令系统 指令: 控制计算机完成某种操作的命令。 指令系统&#xff1a; 处理器所能识别的所有指令的集合。 指令的兼容性&#xff1a; 同一系列机的指令都是兼容的。 汇编语言&#xff1a; 指令助记符。 指令格式 例如&#xff1a; 寻址方式 操作数可能的来源或…

SpringCloud(11)— 微服务保护(Sentinel)

SpringCloud&#xff08;11&#xff09;— 微服务保护(Sentinel) 一 认识Sentinel 1.雪崩问题及其解决方案 微服务调用链路中的某个服务出现问题&#xff0c;引起整个链路中所有的微服务都不可用&#xff0c;这就是我们常说的雪崩问题。 如何解决雪崩问题&#xff1f; 常见…

Qt之实现自定义控件的两种方式——插件法

文章目录前言一、需求二、实现1、新建项目2、自定义控件类3、编译插件4、拖拽使用&#xff08;1&#xff09;在designer.exe中直接拖拽&#xff08;2&#xff09;在Qt Creator的设计师中直接拖拽5、在项目中正常使用前言 可以通过Qt设计师拖拽原生控件进行界面开发&#xff0c;…

【Linux】操作系统及进程概念

大家好我是沐曦希&#x1f495; 文章目录一、冯诺依曼体系结构二、操作系统OS三、系统调用和库函数概念四、进程1.概念2.描述进程-PCB3.查看进程4.查看系统调用5.查看进程调用6. 通过系统调用创建进程-fork初识一、冯诺依曼体系结构 我们常见的计算机&#xff0c;如笔记本。我…

外贸业务员该如何拓客?

最近几个月&#xff0c;做外贸的朋友都来吐槽&#xff0c;外贸干不下去了。一个B2B店铺和Google推广要花很多钱&#xff0c;竞争太激烈了。投入和产出极不成比例&#xff0c;就问我能不能解决。我也是行业出身。我问他们有没有听说过以下五种实惠高效的营销方式。然后我就想告诉…

JWT的组成以及工作原理

什么是 JWT? JWT &#xff08;JSON Web Token&#xff09; 是目前最流行的跨域认证解决方案&#xff0c;是一种基于 Token 的认证授权机制。 从 JWT 的全称可以看出&#xff0c;JWT 本身也是 Token&#xff0c;一种规范化之后的 JSON 结构的 Token。 JWT 自身包含了身份验证…

SpringBoot项目中基本常用依赖分享(个人向)

目录 &#xff08;1&#xff09;spring-boot-starter-web &#xff08;2&#xff09;mysql-connector-java &#xff08;3&#xff09;mybatis-spring-boot-starter &#xff08;4&#xff09;mapper-spring-boot-starter &#xff08;5&#xff09;mybatis-plus-boot-st…

数组string

数组 目录&#xff1a;数组一维数组一维数组的定义和初始化一维数组数据元素的访问练习1 找小猪练习2 算平均值比较两个数组是否相等一维数组冒泡法排序一维数组操作一维数组查找元素**普通查找**&#xff1a;二分查找&#xff1a;二分查找 最小下标最大下标查找元素个数一维数…

如何优化 MySQL

为什么要对 SQL 进行优化 有时候数据库会出现性能低、执行时间太长、等待时间太长、SQL 语句欠佳&#xff08;连接查询&#xff09;、索引失效等问题&#xff0c;这些问题会严重拖慢一个系统的速度&#xff0c;因此需要对 SQL 进行优化。 SQL 的编写过程和解析过程并非是一致…

Qt样式表语法

样式规则每个样式规则由选择器和声明组成。选择器&#xff1a;指定受该规则影响的部件。声明&#xff1a;指定这个部件上要设置的熟悉如&#xff1a;QPushButton{color:red} QPushButton是选择器&#xff1b;{color:red}是声明&#xff1b;color是属性&#xff1b;red是值。选择…

使用prometheus监控ES

下载elasticsearch_exporter wget 下载二进制包并解压、运行&#xff1a; wget https://github.com/prometheus-community/elasticsearch_exporter/releases/download/v1.3.0/elasticsearch_exporter-1.3.0.linux-amd64.tar.gz tar -xvf elasticsearch_exporter-1.3.0.linux-a…