zoukankan      html  css  js  c++  java
  • 源码分析篇

       performTraversals方法会经过measure、layout和draw三个流程才能将一帧View需要显示的内容绘制到屏幕上,用最简化的方式看ViewRootImpl.performTraversals()方法,如下。

     private void performTraversals() {
         ... 
         performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
         ...
         performLayout(lp, mWidth, mHeight); 
         ...
         performDraw();
        . .. 
     }

    首先来说这三个流程的意义:

    performMeasure():从根节点向下遍历View树,完成所有ViewGroup和View的测量工作,计算出所有ViewGroup和View显示出来需要的高度和宽度;

    performLayout():从根节点向下遍历View树,完成所有ViewGroup和View的布局计算工作,根据测量出来的宽高及自身属性,计算出所有ViewGroup和View显示在屏幕上的区域;

    performDraw():从根节点向下遍历View树,完成所有ViewGroup和View的绘制工作,根据布局过程计算出的显示区域,将所有View的当前需显示的内容画到屏幕上。

    再来具体分析这三个流程:

    1. performMeasure()流程

      在ViewRootImpl.java中调用performMeasure()方法传入的参数为childWidthMeasureSpec和childHeightMeasureSpec。在performTraversals能找到它们初始化的地方如下。

      int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
      int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

      通过getRootMeasureSpec能够计算出一个MeasureSpec值,该值一般是父布局调用子布局的measure()是传入的参数,这个参数是由父布局的宽高和子布局的LayoutParams参数计算得到的,这两个参数用于子布局的measure过程,很大程度上决定着子View的宽高。上面代码出计算出来的childWidthMeasureSpec和childHeightMeasureSpec则是根据Window相关属性得到的MeasureSpec,因为DecorView本身没有父布局,所以传入给DecorView进行measure的MeasureSpec值是由Window的尺寸(mWidth、mHeigth)和DecorView的LayoutParams得到的;而对于普通的View,它的MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定。

      方法中的lp.width和lp.heigth表示布局类型,这里指的是DecorView的布局类型。布局类型举例来说,写布局xml时android:layout_width="wrap_content"中设置的wrap_content值,共有三种类型MATCH_PARENT、WRAP_CONTENT以及直接写入了确定大小。进入getRootMeasureSpec()方法。

        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;
        }

      实际上getRootMeasureSpec()方法就是根据传入的窗口大小和类型,通过MeasureSpec.makeMeasureSpec()方法合并成一个int型的measureSpec值。该值代表一个32位 int 值,高2位代表 SpecMode(测量模式),由传入的布局类型转化而来,有三种类型,MeasureSpec.EXACTLY:确定大小,parent view为child view指定固定大小;MeasureSpec.AT_MOST:最大大小;child view在parent view中取值;MeasureSpec.UNSPECIFIED:无限制,parent view不约束child view的大小。该值低30位代表 SpecSize,指在某个测量模式下的规格大小。后面的方法中会通过MeasureSpec.getMode()和MeasureSpec.getSize()方法来解析出这两个值。

      从这里就可以看到childWidthMeasureSpec, childHeightMeasureSpec实际上表示的就是DecorView的宽高和布局类型。进入到performMeasure()方法。

        private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
            try {
                mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
        }

      该方法设置完TraceView的起始点和结束点后直接便是进入到了mView.measure()方法,进入对应View.measure()方法代码。

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
         ...

    //是否有重新Layout的标志
    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT; // Optimize layout by avoiding an extra EXACTLY pass when the view is // already measured as the correct size. In API 23 and below, this // extra pass is required to make LinearLayout re-distribute weight.
         
    //与上一次的MeasureSpec进行对比确定是否需要重新绘制
         final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec; final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY; final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec) && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec); final boolean needsLayout = specChanged && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize); if (forceLayout || needsLayout) { // first clears the measured dimension flag mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded();        //key的值是由widthMeasureSpec和heightMeasureSpece
    //在MeasureCache中查看在当前传入的宽高的MeasureSpec是否已经执行过onMeasure计算
            //如果已经执行过,则直接取出结果通过setMeasuredDimemsionRaw()设置测量出的相关参数
    //如果没有执行过,才会调用onMeasure()方法进行测量工作
    //mPrivateFlags3的设置,在后面介绍的View.layout()方法中会用到
    //在后面layout()方法中会判断该flag,如果此时没有调用,则那layout会再调用measure()
    int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } else { long value = mMeasureCache.valueAt(cacheIndex); // Casting a long to int drops the high 32 bits, no mask needed setMeasuredDimensionRaw((int) (value >> 32), (int) value); mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } // flag not set, setMeasuredDimension() was not invoked, we raise // an exception to warn the developer if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { (2) throw new IllegalStateException("View with id " + getId() + ": " + getClass().getName() + "#onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; }      ... }

      实际的measure过程是在onMeasure()方法中完成的,这里进入到调用到View.onMeasure()方法。

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

      大家注意这里要注意performMeasure()方法是final类型的,即是不能被父类重载的,所以无论是对于任何一种Layout(父类为ViewGroup.java,ViewGroup.java父类为View.java)的measure()方法调用,或是任何一个控件比如TextView的measure()方法调用,执行代码都是View.java中的measure()方法;而不同的控件measure过程的区别是通过重写onMeasure()方法来实现的。所以在measure()方法里调用的onMeasure()方法并未直接走到View.onMeasure()方法,而是走到了View的父类中重写的onMeasure()方法。但是,这里我们要注意到上一段代码的(2)处,这段代码是在调用后onMeasure()方法之后的一个判断,上面的注释的意思是如果到现在还没有调用过setMeasuredDimension()方法,就会抛出下面的异常,所以在View的父类重写onMeasure()方法时,一定要执行一次setMeasuredDimension()方法。那这个方法做了什么事呢?进入View.setMeasuredDimension()方法代码。

        protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
         //根据自己与父布局的android:layoutMode的值调整传入参数
    boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int opticalWidth = insets.left + insets.right; int opticalHeight = insets.top + insets.bottom; measuredWidth += optical ? opticalWidth : -opticalWidth; measuredHeight += optical ? opticalHeight : -opticalHeight; } setMeasuredDimensionRaw(measuredWidth, measuredHeight); }

      直接进入setMeasuredDimensionRaw()方法。

        private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
            mMeasuredWidth = measuredWidth;
            mMeasuredHeight = measuredHeight;
    
            mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
        }

      这里我们可以看到setMeasuredDimension()实际上就是将传入的参数设置到View的变量mMeasuredWidth和mMeasuredHeight中。这也是实际上measure流程所要完成的任务,即是调用到布局树上的所有ViewGroup和View的measure(),让所有的ViewGroup和View计算出对应的宽、高的值保存到自己的mMeasuredWidth、mMeasuredHeight变量中。

      我们继续来分析DecorView的measure流程,进入DecorView.onMeasure()方法。

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
            final boolean isPortrait =
                    getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;
    
            final int widthMode = getMode(widthMeasureSpec);
            final int heightMode = getMode(heightMeasureSpec);
    
            boolean fixedWidth = false;
            mApplyFloatingHorizontalInsets = false;

         //如果SpecMode不是EXACTLY的,则需要在这里调整为EXACTLY
    if (widthMode == AT_MOST) { final TypedValue tvw = isPortrait ? mWindow.mFixedWidthMinor : mWindow.mFixedWidthMajor; if (tvw != null && tvw.type != TypedValue.TYPE_NULL) { final int w;
    //根据DecorView属性,计算出DecorView需要的宽度
    if (tvw.type == TypedValue.TYPE_DIMENSION) { w = (int) tvw.getDimension(metrics); } else if (tvw.type == TypedValue.TYPE_FRACTION) { w = (int) tvw.getFraction(metrics.widthPixels, metrics.widthPixels); } else { w = 0; } if (DEBUG_MEASURE) Log.d(mLogTag, "Fixed " + w); final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    //根据上面计算出来的需要的宽度生成新的MeasureSpec用于DecorView的测量流程
    if (w > 0) { widthMeasureSpec = MeasureSpec.makeMeasureSpec( Math.min(w, widthSize), EXACTLY); fixedWidth = true; } else { widthMeasureSpec = MeasureSpec.makeMeasureSpec( widthSize - mFloatingInsets.left - mFloatingInsets.right, AT_MOST); mApplyFloatingHorizontalInsets = true; } } } mApplyFloatingVerticalInsets = false; if (heightMode == AT_MOST) {   ... //逻辑同上 } ...super.onMeasure(widthMeasureSpec, heightMeasureSpec); ... }

      由于DecorView.java父类是FrameLayout.java,所以调用super.onMeasure()时进入FrameworkLayout.onMeasure()方法。

       @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int count = getChildCount();
    
         //如果宽、高的MeasureSpec的Mode有一个不是EXACTLY,这里就是true
    final boolean measureMatchParentChildren = MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY || MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; mMatchParentChildren.clear(); int maxHeight = 0; int maxWidth = 0; int childState = 0;
         //遍历子View
    for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (mMeasureAllChildren || child.getVisibility() != GONE) {
              //子View的测量,方法内会调用到child.measure(),后文详解 measureChildWithMargins(child, widthMeasureSpec,
    0, heightMeasureSpec, 0); final LayoutParams lp = (LayoutParams) child.getLayoutParams();
              //计算出所有子布局中宽度和高度最大的值
              //由于子布局占用的尺寸除了自身宽高之外,还包含了其距离父布局的边界的值,所以需要加上左右Margin值 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());
              //当前的FrameLayout的MeasureSpec不都是EXACTLY,且其子View为MATCH_PARENT,
              //则子View保存到mMatchParentChildren中,后面重新测量
             //DecorView不会走这个逻辑,因为进过了DecorView的onMeasure()流程,MeasureSpec一定都为EXACTLY
              //会走到下面流程的情况举例:用户自布局一个FrameLayout属性为WRAP_CONTENT是,但子布局为MATCH_PARENT
    if (measureMatchParentChildren) { if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) { mMatchParentChildren.add(child); } } } }
         //最后计算得到的maxWidth和maxHeight的值需要保证能够容纳下当前Layout下所有子View,所以需要对各类情况进行处理
         //所以有以下的加上Padding值,用户设置的Mini尺寸值的对比,设置了背景图片情况的图片大小对比
         
    // 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()); }
         //设置测量结果,相当于完成自己View的measure 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) {
                //根据当前FrameLayout已经测量出来的mMeasureWidth,计算出MATCH_PARENT的子View的宽度值
                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); } ... //childHeigthMeasureSpe的设置,逻辑同上一段 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } }

      首先是对于maxHeight和maxWith的宽度的计算,为何能通过所有子布局中最大的子布局的尺寸来决定自己的尺寸呢?首先要回顾下FrameLayout的布局模式,简单来说,就是所有放在布局里的控件,都按照层次堆叠在屏幕的左上角,后加进来的控件覆盖前面的控件。正是因为要实现这样的布局模式,才决定了FrameLayout的measure算法,所有的子View都是堆叠在屏幕左上角,自然只需要根据最大的子View的尺寸来设置自己(还需要加一些特殊情况的处理),即可能够放下所有的子View。如果是LinearLayout的onMeasure()方法,则又会实现不同的测量逻辑,在测量时则会考虑到多个View依次摆放的问题。

      再来关注代码中第一个加粗的方法measureChildWithMargins(),这里逻辑是依次调用该方法完成FrameLayout所有子View的measure工作。该方法在父类ViewGroup.java类中实现的,进入ViewGroup.measureChildWithMargins()方法代码。

       protected void measureChildWithMargins(View child,
                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);
        }

      这个方法比较简单,首先关注下widthUsed和heithUsed的意义,表示的是父布局已经占用的大小,上面的FrameLayout.onMeasure()代码中调用时传入的是0。为何是0?因为FrameLayout的逻辑是全部堆叠在右上角,所以这个子View放到FrameLayout中时,不会被其它子View提前使用了FrameLayout的空间。

      重点关注下getChildMeasureSpec()方法,通过该方法能够得到子View的MeasureSpec,所以才能调用子View的measure()方法,完成子View的测量。注意,这里传入的lp.width表示的是布局中的android:layout_width对应的值,可能是确定的值,可能是LayoutParams.MATCH_PARENT或LayoutParams.WRAP_CONTENT。进入ViewGroup.getChildMeasureSpec()方法。

        public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
         //父布局的SpecMode和Size
    int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec);
         //父布局中剩下的能够提供给子View使用的尺寸,通过后面计算得到子View需要多少
    int size = Math.max(0, specSize - padding);
         //用于保存子布局的进行measure的MeasureSpec的两个参数
    int resultSize = 0; int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us
         //如果父布局是Mode是EXACTY
         case MeasureSpec.EXACTLY: if (childDimension >= 0) {
              //子View赋值了固定大小
              //则子View的SpecSize就是自己想要的大小
    //则子View的SpecMode是EXACTY resultSize
    = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it.
              //子View是MATCH_PARENT
              //子View想要父布局所有大小,则把父布局剩余的大小都给子View
              //子View的SpecMode是EXACTLY
              resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us.
              //子View是WRAP_CONTENT
              //子View想要在后面自己计算自己需要多少大小
              //则把父布局剩余的大小存入SpecSize,但SpecMode为AT_MOST
    //表示子布局在后面measure自己大小的同时不能超过SpecSize的值
              resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us
         //父布局Mode为AT_MOST case MeasureSpec.AT_MOST: if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us.
    //子View是MATCH_PARENT
    //由于父布局没有确定大小,所以子布局在确定自己需要多少大小前不能给出确定大小
    //则把父布局剩余的大小存入SpecSize,但SpecMode为AT_MOST resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent asked to see how big we want to be case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be
    //如果子View是MATFH_PARENT
              //由于父布局没有限定子布局大小,则设置SpecSize值为0 (需要View支持这种模式)
              //设置SpecMode类型还是为UNSPECIFIED,这样最后计算出有多大就给多大,没有父布局的限制(下同) resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //noinspection ResourceType
    //生成MeasureSpec
         return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }

      在计算出用于子View进行measure的高和宽的MeasureSpec后,便会进入到子View的measure()方法。如果子View是ViewGroup类型,则会继续调用到其子View的measure()方法,并调用setMeasuredDimension()方法设置mMeasureHeight和mMeasureWidth完成自己的measure;如果是子View是一个控件(例如TextView),则会根据自己onMeasure()的实现完成自身的测量,也是调用setMeasuredDimension()方法设置变量。通过这样的方式便个遍历完View树上所有的ViewGroup中间节点和View叶节点,完成measure流程。

    2. performLayout()流程

      进入ViewRootImpl.performLayout()源码。

       private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
                int desiredWindowHeight) {
           
         ...
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout"); try { host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());   
           ...    

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

      这里调用的到DecorView的layout方法,该方法是实际上是调用到到了ViewGroup的layout()方法。上面介绍measure流程时我们知道了两个相关方法measure()和onMeasure(),其中measure()方法在所有视图到基类View中实现的,且不能被重写;而onMeasure()方法实现了实际的测量过程,需要由继承了View的视图子类重写该方法从而实现了自己的测量逻辑。而对于Layout流程,也有着两个相关方法layout()和onLayout(),进入View.java代码看这连个方法。View.layout()方法后面详讲。

     public void layout(int l, int t, int r, int b) {
            ...
            onLayout(changed, l, t, r, b);
         ...
     }

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

      首先这里的layout()方法没有final修饰符,表示layout()方法可以被重写,另外onLayout()方法是一个空方法,所以需要View的子类根据自己需求来重写该方法来完成layout流程。再来看下继承自View的ViewGroup类的layout()和onLayout()方法。

       @Override
        public final void layout(int l, int t, int r, int b) {
            if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
                if (mTransition != null) {//过渡动画相关
                    mTransition.layoutChange(this);
                }
                super.layout(l, t, r, b);
            } else {
                // record the fact that we noop'd it; request layout when transition finishes
                mLayoutCalledWhileSuppressed = true;
            }
        }
    
        @Override
        protected abstract void onLayout(boolean changed,
                int l, int t, int r, int b);

      ViewGroup的layout()方法加上了final修饰符,而onLayout()方法则是抽象类型的,所以所有继承自ViewGroup类的布局类(例如FrameLayout.java)都需要实现onLayout()方法,而不能重写layout()方法。并且ViewGroup中的layout()的方法只是加上一些对于过渡动画逻辑的处理,本质上是调回了View的layout()方法。

      在ViewRootImpl.performLayout()方法中调用到了DecorView的layout()的方法,根据上方代码super.layout()实际调用到了View的layout()方法。注意这里传入的四个参数,分别为0,0, host.getMeasuredWidth(), host.getMeasuredHeight(),后两个值就是在measure流程中计算出来的mMeasuredWidth和MeasuredHeigth,用于表示需要的宽度和高度。进入View.layout()方法代码。

       public void layout(int l, int t, int r, int b) {
         //mPrivateFlag3记录了measure过程是否被跳过,如果被跳过则这时候再调用一次measure(),前文有提
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight;
         //layoutoutMode为Optical则会调到setOpticalFrame()
    //setOpticalFrame()会对传入的参数进行调整,但还是调用到setFrame()方法
    boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
         //如果View位置发生了变化或已经设置了重新Layout的标志
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b);        ... } mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; }

      首先会调用setFrame()方法,方法的返回值标志了布局与上一次是否发生了变化。传入的四个参数的分别代表了,布局左、顶部、右、底部的值,这四个值指示了一个矩形区域。

        protected boolean setFrame(int left, int top, int right, int bottom) {
            boolean changed = false;
    
            if (DBG) {
                Log.d("View", this + " View.setFrame(" + left + "," + top + ","
                        + right + "," + bottom + ")");
            }
    
            if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
                changed = true;
    
                // Remember our drawn bit
                int drawn = mPrivateFlags & PFLAG_DRAWN;
    
                int oldWidth = mRight - mLeft;
                int oldHeight = mBottom - mTop;
                int newWidth = right - left;
                int newHeight = bottom - top;
                boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
    
                // Invalidate our old position
                invalidate(sizeChanged);
    
                mLeft = left;
                mTop = top;
                mRight = right;
                mBottom = bottom;
                mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
    
                mPrivateFlags |= PFLAG_HAS_BOUNDS;
    
           //会回调onSizeChanged()方法
                if (sizeChanged) {
                    sizeChange(newWidth, newHeight, oldWidth, oldHeight);
                }
    
                ...
            }
            return changed;
        }

      这个方法比较简单,主要是将父类传入的区域保存到View的mLeft、mTop、mRight、mBottom。在执行完setFrame()之后便会执行到onLayout()方法。这里我们通过插入一段TextView.java类的onLayout()方法,来探究下layout这个流程究竟是要完成什么工作?TextView的layout也是从其父类传入四个参数调用到View.layout()方法(上面代码),然后setFrame(),然后就会进入到进入TextView.onLayout()代码。

        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            super.onLayout(changed, left, top, right, bottom);
            if (mDeferScroll >= 0) {
                int curs = mDeferScroll;
                mDeferScroll = -1;
                bringPointIntoView(Math.min(curs, mText.length()));
            }
        }

      可以看到这里只是在调用了View.onLayout()后添加了一个DeferScroll的逻辑处理。而回想下View.onLayout()方法其实什么都没有做,但这样TextView的layout流程就结束了。那对于TextView来说,layout流程到底做了什么事呢?其实主要就是设置了mLeft、mTop、mRight、mBottom这四个变量,这四个变量指示了这个TextView在屏幕上应该出现的坐标位置区域;而这四个变量,是由TextView的父布局View计算好了,再调用到TextView.layout()方法时传入的。所以整个layout过程,实际遍历整个View树,根据measure过程计算出的View需要的宽度和高度值结合自己的LayoutParam属性,计算出所有View在相对于自己父布局View的边界的位置,并保存到mLeft、mTop、mRight、mBottom变量中,用于后面的绘制操作。

      上面可以知道TextView的layout计算出来的现实区域直接时父布局传入的,所以较为复杂的计算逻辑是处于Layout类onLayout()源码中的。DecorView继承自FrameLayout,DecorView.onLayout()方法会调用到其父类FrameLayout类的onLayout()方法,进入FrameLayout.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();
    
         //计算当前Layout的边界padding值
    final int parentLeft = getPaddingLeftWithForeground(); final int parentRight = right - left - getPaddingRightWithForeground(); final int parentTop = getPaddingTopWithForeground(); final int parentBottom = bottom - top - getPaddingBottomWithForeground();
    //遍历该Layout的所有子View
    //结合子View的measure值,即自己的属性,计算出子View的layout区域,并调用子View的layout()方法
    for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams();
              //获得该子布局measure出来的宽、高值
    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;          

              //该Layout水平方向的gravity属性各种情况下的处理
    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; }
              //该Layout垂直方法的gravity属性各种情况的处理
    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; }
              //调用子View的layout方法,这里传入的参数就是父布局计算好的子View的区域 child.layout(childLeft, childTop, childLeft
    + width, childTop + height); } } }

      这样随着Layout的onLayout()调用到所有子View的layout()方法,而子View的layout()又会继续往下遍历直至遍历到View树到所有节电,这样就完成了整个layout的流程。

    3. perfromDraw()流程

      执行完performLayout()方法,便会调用到performDraw()方法,进入到ViewRootImpl.performDraw()方法。

        private void performDraw() {
            if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
                return;
            }
    
         //是否需要全部重绘的标志
    final boolean fullRedrawNeeded = mFullRedrawNeeded; mFullRedrawNeeded = false; mIsDrawing = true; Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw"); try { draw(fullRedrawNeeded); } finally { mIsDrawing = false; Trace.traceEnd(Trace.TRACE_TAG_VIEW); } ... }

      这里主要关注调用到了draw()方法,进入ViewRootImpl.draw()代码。  

       private void draw(boolean fullRedrawNeeded) {
            ...
    
         //如果是第一次绘制,则会回调到sFirstDrawHandlers中的事件
    //在ActivityThread.attch()方法中有将回调事件加入该队列
    //回调时会执行ActivityThread.ensureJitEnable来确保即时编译相关功能
    if (!sFirstDrawComplete) { synchronized (sFirstDrawHandlers) { sFirstDrawComplete = true; final int count = sFirstDrawHandlers.size(); for (int i = 0; i< count; i++) { mHandler.post(sFirstDrawHandlers.get(i)); } } }      
         //滚动相关处理,如果scroll发生改变,则回调dispatchOnScrollChanged()方法 scrollToRectOrFocus(
    null, false); if (mAttachInfo.mViewScrollChanged) { mAttachInfo.mViewScrollChanged = false; mAttachInfo.mTreeObserver.dispatchOnScrollChanged(); }
    //窗口当前是否有动画需要执行
    boolean animating = mScroller != null && mScroller.computeScrollOffset();
         ... //scroll相关处理
         
         
    final Rect dirty = mDirty;
         ...
         
         
    if (fullRedrawNeeded) { mAttachInfo.mIgnoreDirtyState = true; dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f)); } ...

         
    if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
    //mAttachInfo.mHardwareRenderer不为null,则表示该Window使用硬件加速进行绘制
    //执行ViewRootImpl.set()方法会判断是否使用硬件加速

    //若判断使用会调用ViewRootImpl.enableHardwareAcceleration()来初始化mHardwareRenderer
           //该View设置为使用硬件加速,且当前硬件加速处于可用状态
    if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { ...
    //使用硬件加速绘制方式 mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo,
    this); } else {// If we get here with a disabled & requested hardware renderer, something went // wrong (an invalidate posted right before we destroyed the hardware surface // for instance) so we should just bail out. Locking the surface with software // rendering at this point would lock it forever and prevent hardware renderer // from doing its job when it comes back. // Before we request a new frame we must however attempt to reinitiliaze the // hardware renderer if it's in requested state. This would happen after an // eglTerminate() for instance.
    //如果当前View要求使用硬件加速,但硬件加速处于disable状态
    //可能是由于硬件加速在销毁之前的surface实例时会发出无效的宣告导致的
              if (mAttachInfo.mHardwareRenderer != null && !mAttachInfo.mHardwareRenderer.isEnabled() && mAttachInfo.mHardwareRenderer.isRequested()) { try {
    //尝试重新初始化当前window的硬件加速 mAttachInfo.mHardwareRenderer.initializeIfNeeded( mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets); }
    catch (OutOfResourcesException e) { handleOutOfResourcesException(e); return; } mFullRedrawNeeded = true; scheduleTraversals(); return; }
              //使用软件渲染绘制方式
    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { return; } } } ... }

      mDirty表示的是当前需要更新的区域,即脏区域。经过一些scroll相关的处理后,如果脏区域不为空或者有动画需要执行时,便会执行重绘窗口的工作。有两种绘制方式,硬件加速绘制方式和软件渲染绘制方式,在创建窗口流程的ViewRootImpl.setView()中,会根据不同情况,来选择是否创mAttachInfo.mHardwareRenderer对象。如果该对象不为空,则会进入硬件加速绘制方式,即调用到ThreadedRenderer.draw();否则则会进入软件渲染的绘制方式,调用到ViewRootImpl.drawSoftware()方法。但是无论哪种方式,都会走到mView.draw()方法,即DecorView.draw()方法。该方法是实际调用到到是View.draw(Canvas canvas)方法。

      在调用View.draw()方式时调用该方法时会传入canvas参数,硬件加速和软件渲染这两种方式会创建出不同的Canvas用以执行View的draw流程。Canvas顾名思义就是画布的意思,这个类一方面代表了当前用于绘制的区域及区域属性相关信息,同时也提供了各类接口用于在这片区域上画出给类图形,比如调用canvas.drawCircle(...)方法就根据传入参数在屏幕上画出一个特定的圆圈。硬件加速和软件渲染这两种方式对于Canvas有着不同的底层实现,带来的效果和对系统性能及内存这些的影响也不一样,这里我们不再展开介绍。直接看来View.draw()方法。

        @CallSuper
        public void draw(Canvas canvas) {
            final int privateFlags = mPrivateFlags;
            final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                    (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
            mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    
            /*
             * 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)
             */
            //根据上面注释可以知道draw的过程分为5步
    //1.画出背景background //2.判断是否需要画边缘的渐变效果
    //3.画出当前View需要显示的内容,调用onDraw()来实现
    //4.调用dispatchDraw()方法,进入子视图的draw逻辑
    //5.如果需要花边缘渐变效果,则在这里画
    //6.绘制装饰(如滚动条)

         // Step 1, draw the background, if needed int saveCount; if (!dirtyOpaque) {
           //画背景 drawBackground(canvas); }
    // skip step 2 & 5 if possible (common case)
         //判断是否需要绘制边缘渐变效果(水平方向、垂直方向)
    final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;//是否有 boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
         //如果不需要绘制边缘渐变效果,跳过了step5
    if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content
    //绘制自己View的内容
           if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children
           //调起子View的Draw过程 dispatchDraw(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); // we're done... return; } /* * Here we do the full fledged routine... * (this is an uncommon case where speed matters less, * this is why we repeat some of the tests that have been * done above) */ ... //有边缘渐变效果的处理// 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 ... //画出边缘渐变效果// 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); }

      注释已经很清楚,View的draw过程分为了五个步骤,如果没有边缘渐变效果则会跳过第五步。这里我们关注onDraw()方法实现了自己View内容的绘制和dispatchDraw()方法调起了自己的子View的绘制过程。首先,在调用View中调用onDraw()时,由于走的时DecorView的流程,所以会调用到DecorView.onDraw()方法,代码如下。

        @Override
        public void onDraw(Canvas c) {
            super.onDraw(c);
    //DecorView设置了一个BackgroundFallback,该对象用于应用没有设置window背景时,会显示该对象指示的背景 mBackgroundFallback.draw(mContentRoot, c, mWindow.mContentParent); }

      这里实际调用到的是父类的onDraw()方法,但是在DecorView的父类FrameLayout及更上一层的ViewGroup上都没有实现该方法,所以super.onDraw()实际调用到了View.onDraw()方法。

        protected void onDraw(Canvas canvas) {
        }

      但是View.onDraw()方法是一个空实现,这里可以看出来,onDraw()方法是用于父类进行重写来实现画出自己内容的方法,而ViewGroup及其各种Layout子类都没有实现该方法,因为对于布局来说本身就是用来放置控件的,自己是没有什么内容好绘制的,所以在onDraw()是并未执行什么内容。自然作为FrameLayout子类的DecorView也没有什么好画出来的,除了画出一个FallbackBackground。但如果当前的View不是DecorView,也不是其它布局View,而是一个控件类型的View,那在onDraw()时就会根据自己的属性及自己需要显示的区,通过调用传入的Canvas对象的各种方法,来在屏幕上画出自己需要显示的内容。

      再来看View.draw()方法里的Step4调用到disptachDraw()方法,在View.dispatchDraw()方法还是一个空实现,说明是希望View的子类来实现的。而一般的非ViewGroup的控件子View类则不会实现该方法,因为这里View是没有子View的,所以当遍历到控件类型的View时,这一步实际上是什么都没有做的。我们继续来看DecorView的draw流程,这里调用到dispatchDraw()实际调用到到是ViewGroup.dispatchDraw()方法,进入该代码。

        @Override
        protected void dispatchDraw(Canvas canvas) {
            boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
            final int childrenCount = mChildrenCount;
            final View[] children = mChildren;
            int flags = mGroupFlags;
    
    //如果当前的ViewGroup需要执行Layout级别的动画
    if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) { final boolean buildCache = !isHardwareAccelerated(); for (int i = 0; i < childrenCount; i++) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { final LayoutParams params = child.getLayoutParams();
                //将需要执行的动画设置到子View的对应属性上 attachLayoutAnimationParameters(child, params, i, childrenCount); bindLayoutAnimation(child); } }
    final LayoutAnimationController controller = mLayoutAnimationController; if (controller.willOverlap()) { mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE; }
           //启动mLayoutAnimationControlle中设置的动画 controller.start(); mGroupFlags
    &= ~FLAG_RUN_ANIMATION; mGroupFlags &= ~FLAG_ANIMATION_DONE;
           //动画启动时的回调
    if (mAnimationListener != null) { mAnimationListener.onAnimationStart(controller.getAnimation()); } } int clipSaveCount = 0; final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
         //如果当前的ViewGroup设置了Padding的属性
    if (clipToPadding) { clipSaveCount = canvas.save();
    //将父视图传入的canvas裁剪去padding的区域 canvas.clipRect(mScrollX
    + mPaddingLeft, mScrollY + mPaddingTop, mScrollX + mRight - mLeft - mPaddingRight, mScrollY + mBottom - mTop - mPaddingBottom); } ...
    for (int i = 0; i < childrenCount; i++) {        ... //TransientView的处理

           
    final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } }
         ...

         // Draw any disappearing views that have animations
         //绘制mDisappearingChildren列别中的子视图,指正在处于消失动画状态的子View if (mDisappearingChildren != null) { final ArrayList<View> disappearingChildren = mDisappearingChildren; final int disappearingCount = disappearingChildren.size() - 1; // Go backwards -- we may delete as animations finish for (int i = disappearingCount; i >= 0; i--) { final View child = disappearingChildren.get(i); more |= drawChild(canvas, child, drawingTime); } }
    ...

         //检查动画是否完成,如果完成则发送一个异步消息,通知应用程序
    if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 && mLayoutAnimationController.isDone() && !more) { // We want to erase the drawing cache and notify the listener after the // next frame is drawn because one extra invalidate() is caused by // drawChild() after the animation is over mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER; final Runnable end = new Runnable() { @Override public void run() { notifyAnimationListener(); } }; post(end); } }

      在dispatch中会遍历当前ViewGroup的子视图,然后调用drawChild()方法来依次调起子视图的绘制过程,进入ViewGroup.drawChild()代码。

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

      这里直接就调用到了child.draw()方法,但是注意,这里的draw()方法与我们之前分析过的View.draw()方法参数不一样,不仅传入了父布局视图的画布,还把父布局视图视图自身作为参数传入了。进入有三个参数的View.draw()方法的代码。

       /**
         * This method is called by ViewGroup.drawChild() to have each child view draw itself.
         *
         * This is where the View specializes rendering behavior based on layer type,
         * and hardware acceleration.
         */
       //这个方法是专门用于ViewGroup来调其子View的绘制过程的方法
    //方法会传入当前View的父布局View,用来进行父布局canvas画布的区域移动和裁剪工作
    //该方法会根据绘制类型(硬件加速、软件渲染)结合View属性进行一些canvas和painter属性设置工作
       boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
         ...
      
         draw(canvas);
         ...
    }

      这个方法很长,但都是逻辑算法类型的,不影响到后面的流程,方法的作用见上访注释。我们回想一下之前的流程,从canvas的创建,到视图使用canvas进行绘制,并未涉及到canvas的裁剪动作。那是是因为我们是从DecorView进行分析的,在我们略过的canvas初始化动作时就根据DecorView在layout布局时计算出来的显示区域,进行了canvas画布区域的实质;而对于其它的ViewGroup在调起其子View的绘制动作时,就会调用到上方有三个参数的View.draw()方法,在该方法中结合layout过程中的计算结果,完成canvas画布的裁剪,然后再调用View.draw(Canvas)方法(之前介绍过的包含五个步骤的方法)来完成当前View的绘制过程。通过这样一个流程,便能够调用到视图树上所有ViewGroup和控件View的draw()方法,画出了所有View需要显示的内容,完成了一次绘制过程。

      

  • 相关阅读:
    canvas 文本坐标(0,0)显示问题
    canvas 图片跨域处理
    canvas 文字换行
    什么是柯理化函数?
    记录一下学习webpack原理的过程
    pika和kombu实现rpc代码
    pika和rabbitMQ实现rpc代码
    docker部署rabbitMQ
    rabbitMQ和pika模块
    ubuntu搭建关于amd64或arm64,armhf架构的本地apt源
  • 原文地址:https://www.cnblogs.com/tiger-wang-ms/p/6543418.html
Copyright © 2011-2022 走看看