View的绘制流程学习总结

本文深入探讨了Android中View的绘制流程,包括Measure、Layout和Draw三个阶段。Measure阶段确定View的尺寸,Layout阶段确定View的位置,Draw阶段将View呈现在屏幕上。整个流程始于ViewRootImpl的performTraversals方法,通过measure()、layout()和draw()进行。MeasureSpec在测量阶段起关键作用,定义了三种测量模式:UNSPECIFIED、EXACTLY和AT_MOST。对于ViewGroup,measure流程会遍历并测量所有子View。布局类如RelativeLayout、LinearLayout等有自己的onMeasure实现,通过measureChild或measureChildWithMargins计算子View的MeasureSpec。最后,draw阶段通过dispatchDraw绘制子View,完成View的显示。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


为什么需要学习View的绘制流程?Android提供了很多的基本View组件,如TextView、EditText、Button等等,这些组件在日常开发中经常要用到,也满足了大部分的需求。然而,有时候用户的要求很特殊,需要的效果基本组件根本实现不了,这时我们就要自定义View,于是View的绘制流程就派上用场了,不学习View的绘制流程,根本写不出符合需求的View,相反,学好了绘制流程,无论什么样的View你都可以实现,只有需求想不出,没有你写不出的自定义View。

简介

View的绘制流程可分为三大阶段,Measure(测量)、Layout(定位)、Draw(绘制),这三个阶段依次进行。Measure的本质就是确定组件的宽和高,Layout的本质是确定组件4个顶点的位置,Draw的本质就是把测量好的、定好位的组件画出来。为什么是这样的顺序?简单举个例子,画画的时候,下笔之前要先想好要画多大、画在哪里吧,画多大就是测量、画在哪就是定位,画这个动作就是绘制。

那么在是在哪调用这3个流程的呢?这里只需要知道入口是ViewRootImpl的performTraversals方法,再上面就是ActivityThread里的代码了,这里不需深究。performTraversals方法会依次调用performMeasure()、performLayout()、performDraw()来完成整个View树的绘制流程。看看其工作流程图。
在这里插入图片描述
注意这里并不是说performXXX()方法是ViewGroup里面的,它突出的重点是流程,比如View的measure阶段对应3个方法,preformMeasure->measure->onMeasure,在ViewGroup的onMeasure方法中又遍历子View调用它的measure流程,这样measure就从父View传到子View,子View重复父View的动作,直到反复遍历整个View树,measure阶段结束了就到layout阶段,以此类推。View经历完3个流程才会显示到界面上。

可以看看ViewRootImpl的部分源码,验证上面的内容。

private void performTraversals() {
	// 省略代码,通常是一些条件判断
	performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
	// 省略代码,通常是一些条件判断
	performLayout(lp, mWidth, mHeight);
	// 省略代码,通常是一些条件判断
	performDraw();
}

performTraversals()方法的代码很长,全部贴出来不实际,可以自己在AndroidStudio看ViewRootImpl的源码进行验证。前提是你的AndroidStudio下载了源码文件。

下面分别详细讲解三大流程。

measure

要理解measure流程,就要先理解MeasureSpec,因为measure流程其实就是不断的计算MeasureSpec,并传递子View,子View再根据传来的MeasureSpec给某些变量赋值。

MeasureSpec
MeasureSpec是一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。可以把MeasureSpec简单理解为一个对象,里面有两个属性SpecMode和SpecSize,但为了节省空间,把它包装成一个32位的int值。

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    public static final int AT_MOST     = 2 << MODE_SHIFT;

	//可以理解为创建对象(传入两个属性size和mode)
	public static int makeMeasureSpec(int size,int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    // 下面对应两个get方法
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}    

需要注意的是,MeasureSpec确实是一个类,但是measure流程使用的MeasureSpec是通过makeMeasureSpec返回的一个int值,并不是MeasureSpec类。

SpecMode

  • UNSPECIFIFD
    父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部(平时用的基础View和自定义View几乎没有这种情况)。
  • EXACTLY
    父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值。如下面所示。
android:layout_width="match_parent"
android:layout_width="100dp"
  • AT_MOST
    父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应与LayoutParams中的wrap_content。
android:layout_width="wrap_content"

注意:
这里并非说一个View在xml文件定义的属性是match_parent,它的SpecMode就是EXACTLY,还有视父View的SpecMode而决定的。所以,一个View的MeasureSpec是由父View的MeasureSpec和自身的LayoutParams属性所决定的。既然如此,那最顶层View的MeasureSpec是怎样得到的呢?下面进行分析。

上面提到measure流程的入口是performTraversals()方法,在里面依次调用了performXXX()函数开始三大流程,可能细心的人已经发现,performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);已经传入了MeasureSpec供后面的View使用。再看ViewRootImpl源码,查看childWidthMeasureSpec是怎么来的(childHeightMeasureSpec与其类似,一个View通常有两个MeasureSpec,分别代表宽和高)。

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// 省略代码
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

可以看到这两个MeasureSpec是通过getRootMeasureSpec()方法得到的,看方法名就知道,这是获取根部MeasureSpec的方法,即顶层View的MeasureSpec。进入该方法查看。

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

通过上面的代码,DecorView的MeasureSpec的产生过程就很明确了,makeMeasureSpec()在上文已经分析过,就是用来创建MeasureSpec的,再总结一下,其遵守如下规则,根据它的LayoutParams中的参数划分。

  • LayoutParams.MATCH_PARENT:MeasureSpec的SpecMode为EXACTLY,SpecSize就是窗口的大小。
  • LayoutParams.WRAP_CONTENT:MeasureSpec的SpecMode为AT_MOST,SpecSize就是窗口大小(这个SpecSize并不是DecorView的真实大小,这里只是表明了在这种模式下,DecorView的真实大小不能超过SpecSize这个值)。
  • 固定大小(比如100dp):MeasureSpec的SpecMode为EXACTLY,SpecSize就是LayoutParams所指定的大小。

顶层View的MeasureSpec后,就调用performMeasure()了,进入该方法的内部。

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

看到没有,调用了mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);,这样measure流程就由顶层View传递给下层View,并且印证了上面的说法,performMeasure()->measure(),这时进入View.measure(),可以预测它里面肯定调用了onMeasure()

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
	//省略代码
	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;
            }
        // 省略代码
}

measure方法确实调用了onMeasure方法,它其实是对MeasureSpec做一些调整,再调用onMeasure进行真正的赋值。它是一个final修饰的方法,意味着子类不能重写这个方法,所以无论当前View是ViewGroup还是View,measure()都可以简单理解为调用了onMeasure()。

所以我们现在的重点onMeasure方法,先看看View的onMeasure()。
View的onMeasure()

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

代码很简洁,但看起来又很复杂。其实就一个函数setMeasuredDimension(int,int),只不过它的两个参数都用了函数来获取,但很多情况下,函数得出来的结果就是onMeasure方法中的参数widthMeasureSpec和heightMeasureSpec中的SpecSize值。因此,可以简单理解为setMeasuredDimension(widthMeasureSpec.getSize(),heightMeasureSpec.getSize());,在这个函数里面就是一些赋值操作,赋值完后,该View的measure流程就结束了。再来分析getDefaultSize(),看完它你就知道为什么上面说可以简化了。

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的specMode是UNSPECIFIED的时候,返回的结果才是getSuggestedMinimumWidth()得到的值,其余情况都是返回specSize,前面又提到UNSPECIFIED是用在系统内部的,自定义或者基本组件很少出现这种模式,一般来说不需要关注此模式,所以知道上面为什么说可以简化了吧。

到这里,View的measure流程就结束了,但上面分析的是最底层的View,是View树的叶子节点,那如果是ViewGroup呢,DecorView就是一个ViewGroup(DecorView继承自FrameLayout,FrameLayout继承自ViewGroup),ViewGroup的onMeasure又是如何工作的呢?

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks 
public class FrameLayout extends ViewGroup

ViewGroup的measure流程
在ViewGroup搜索onMeasure方法,没找到?什么情况,没找到那只能说明是继承View的onMeasure方法咯,但View中onMeasure只测量了自己,它没有测量子View,因为如果是View类的话就不存在子View的说法了。然而,ViewGroup可不能只测量自己吧,所以ViewGroup的onMeasure方法是什么回事?答案是,onMeasure的实现在ViewGroup的具体子类中,例如我们常用的布局类RelativeLayout、LinearLayout、FrameLayout等,这些子类都重写了onMeasure方法,每种布局的onMeasure方法都不一样,但肯定都会遍历子View,计算子View的MeasureSpec并调用子View的measure方法,将流程传递下去。所以,如果我们自定义一个ViewGroup,要重写onMeasure方法,如果不重写,就是继承了View的onMeasure方法,这样就测量不了子View导致出现一些异常(一般来说很少自定义一个ViewGroup,多数情况是继承布局类进行功能的扩展)。

你可能看过其他人的文章,他们在讲解ViewGroup的measure过程时,可能会提到ViewGroup类中的一个measureChildren方法,但他们没说这个方法是如何被调用的,搞得不明不白。其实在View和ViewGroup中都没有调用这个方法,这个方法是在ViewGroup的子类如上面提到的几个布局类中被调用,用来测量子View。所以建议大家学习的时候自己多看看源码,看方法是怎样调用的,只看别人的博客很难弄明白整个流程。看看measureChildren的源码。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

可以看到这里遍历子View,执行measureChild方法,印证上面的说法。进入measureChild方法。

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

在measureChild方法中,先用getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width);计算子View的MeasureSpec,从参数可以看出子View的MeasureSpec的确跟父View和自身的LayoutParams属性相关(前面有提及过),计算好View的MeasureSpec后传递给child.measure方法,然后子View就根据这个MeasureSpec进行操作了,具体流程上面有详细分析。

除了上面的measureChild方法,ViewGroup还有一个measureChildWithMargins方法,从名字可以看出后者把margins也考虑进来了,其他和measureChild区别不大。看看其源码。

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

值得一提的是,这些方法在ViewGroup的具体实现类中不是一定要使用的,比如measureChildren这个方法在上面提到的几个布局类中都没有使用,它们都自己实现了遍历子View的方式,所以可以理解为,这些方法是提供给ViewGroup子类使用,但使不使用由子类决定。

再来分析getChildMeasureSpec,看看父View的MeasureSpec和子类LayoutParams具体是怎样共同决定子类MeasureSpec的。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                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.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        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.
                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
                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
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

上面的代码可以用一个二维表格表示。

childLayoutParams/parentSpecModeEXACTLYAT_MOSTUNSPECIFIED
dp/dxEXACTLY childSizeEXACTLY childSizeEXACTLY childSize
MATCH_PARENTEXACTLY parentSizeAT_MOST parentSizeUNSPECIFIED 0
WRAP_CONTENTAT_MOST parentSizeAT_MOST parentSizeUNSPECIFIED 0

需要再次说明,AT_MOST状态下的SpecSize不是真实的的大小,只是表明不能超过这个值。

到此,三大流程中的measure阶段就分析完毕了,接下来就是layout阶段。

layout

和measure类似,layout阶段也是从performLayout开始的,performLayout调用View的layout方法。看看其源码。

public void layout(int l, int t, int r, int b) {
        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;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }
    //省略代码
    }    

可以看到上面的代码调用了setFrame或者setOpticalFrame,setOpticalFrame内部也是调用setFrame,这个方法给View的4个顶点赋值,这里就确定了View在父容器里的位置。赋值完毕后就调用onLayout,onLayout方法可以理解为父View计算子View的位置,然后调用子View的layout方法确定位置(就是前面说的赋值),这样layout流程就从父View传到子View了。

onLayout在View类中是一个空方法,子类可根据需求重写;在ViewGroup中是一个抽象方法,子类必须给出具体实现。可以这样理解,View的位置在layout方法中已经确定了,而onLayout是父View计算子View位置的方法,叶子节点已经没有子View了,所以onLayout方法为空,是否重写根据具体需求;而ViewGroup下面还有子View,所以必须重写onLayout方法计算子View的位置(ViewGroup子类的onLayout中一定会遍历子View调用其layout方法)。每种ViewGroup的onLayout方法都有不同的逻辑。所以,一般通过继承View实现的自定义组件,很少重写onLayout方法。

draw

Draw过程就相对简单了,它的作用是将View绘制到屏幕上。View的绘制过程遵循下面几步:
(1)绘制背景background.draw(canvas)。
(2)绘制自己 onDraw(canvas)。
(3)绘制children (dispatchDraw)
(4)绘制装饰
这一点可以从源码中明显看出来。

public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        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)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        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;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            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;
        }
        // 省略代码
    }    

draw过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历子View并调用子View的draw方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值