View的绘制流程(一)--------谈谈setContentView

本文深入探讨Android中setContentView的工作原理,分析如何通过addView动态添加控件。讲解了setContentView如何启动界面显示过程,涉及installDecor、generateDecor和generateLayout等方法,以及LayoutInflater的inflate过程。同时,文章提到了addView的源码分析,展示了View绘制流程中的measure(), layout(), draw()等关键步骤。" 108248443,9099794,树莓派安装OpenCV4.4.0详细教程,"['opencv', 'raspberry pi', 'linux', 'cmake', 'python']

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

       大家都知道,在activity里面如果想往视图上放置布局,可以用setContentView来设置,如果想动态增加控件,可以用addView来添加(其实setContentView方法内部也通过addView方法来实现的),那么View是如何被放在视图上面并且显示的呢,下面我们来分析一下这个过程。

一、setContentView

先从setContentView看起,界面就是从这个方法开始最后显示在界面上的,源码如下:

/**
     * Set the activity content from a layout resource.  The resource will be
     * inflated, adding all top-level views to the activity.
     *
     * @param layoutResID Resource ID to be inflated.
     * 
     * @see #setContentView(android.view.View)
     * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
     */
    public void setContentView(int layoutResID) {
        getWindow().setContentView(layoutResID);
        initActionBar();
}
这个方法很简单,就是通过getWindow,获取到window,然后调用window的setContentView方法,Window是什么,window可以说是view的直接管理者,view视图都是放在了window上面,window是一个抽象类,具体实现是PhoneWindow(位于com.android.internal.policy.impl),进去看看

@Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            installDecor();
        } else {
            mContentParent.removeAllViews();
        }
        mLayoutInflater.inflate(layoutResID, mContentParent);
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
}

首先当mContentParent为空,执行installDecor(),mContentParent是什么呢,通过源码中的声明可以看到,这是一个容器ViewGroup,点进去。

private void installDecor() {
        if (mDecor == null) {
            mDecor = generateDecor();
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
		……
}
}

判断mDecor是否为空,如果为空,那么执行generateDecor方法来获得Decor

protected DecorView generateDecor() {
        return new DecorView(getContext(), -1);
}

可以看到,这返回了一个DecorView,这是个内部类,就是我们的顶级布局,一般来说它的内部包含了标题栏和内部栏,内部栏是一定会存在的,id也是固定为android.R.id.content,继续看,下面同样判断mContentParent是否为空,为空就会执行generateLayout()方法,并且把我们创建好的顶级布局当作参数传进去,源码如下:

protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.
//1、…获取窗口的style属性
        TypedArray a = getWindowStyle();

        if (false) {
            System.out.println("From style:");
            String s = "Attrs:";
            for (int i = 0; i < com.android.internal.R.styleable.Window.length; i++) {
                s = s + " " + Integer.toHexString(com.android.internal.R.styleable.Window[i]) + "="
                        + a.getString(i);
            }
            System.out.println(s);
        }
//…窗口是否浮动
        mIsFloating = a.getBoolean(com.android.internal.R.styleable.Window_windowIsFloating, false);
        int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
                & (~getForcedWindowFlags());
        if (mIsFloating) {
            setLayout(WRAP_CONTENT, WRAP_CONTENT);
            setFlags(0, flagsToUpdate);
        } else {
            setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
        }
//…设置title是否显示
        if (a.getBoolean(com.android.internal.R.styleable.Window_windowNoTitle, false)) {
            requestFeature(FEATURE_NO_TITLE);
        } else if (a.getBoolean(com.android.internal.R.styleable.Window_windowActionBar, false)) {
            // Don't allow an action bar if there is no title.
            requestFeature(FEATURE_ACTION_BAR);
        }
       //. . . . . .        
     // …窗口动画
        if (params.windowAnimations == 0) {
            params.windowAnimations = a.getResourceId(
                    com.android.internal.R.styleable.Window_windowAnimationStyle, 0);
        }
        // The rest are only done if this window is not embedded; otherwise,
        // the values are inherited from our container.
        if (getContainer() == null) {
            if (mBackgroundDrawable == null) {
                if (mBackgroundResource == 0) {
                    mBackgroundResource = a.getResourceId(
                            com.android.internal.R.styleable.Window_windowBackground, 0);
                }
                if (mFrameResource == 0) {
                    mFrameResource = a.getResourceId(com.android.internal.R.styleable.Window_windowFrame, 0);
                }
                if (false) {
                    System.out.println("Background: "
                            + Integer.toHexString(mBackgroundResource) + " Frame: "
                            + Integer.toHexString(mFrameResource));
                }
            }
            mTextColor = a.getColor(com.android.internal.R.styleable.Window_textColor, 0xFF000000);
        }
        // Inflate the window decor.
//2、获取feature值(风格属性),来选择不同的窗口修饰布局文件
        int layoutResource;//布局文件
        int features = getLocalFeatures();
        // System.out.println("Features: 0x" + Integer.toHexString(features));
        if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        com.android.internal.R.attr.dialogTitleIconsDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else {
                layoutResource = com.android.internal.R.layout.screen_title_icons;
            }
            // XXX Remove this once action bar supports these features.
            removeFeature(FEATURE_ACTION_BAR);
            // System.out.println("Title Icons!");
        } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
                && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
            // Special case for a window with only a progress bar (and title).
            // XXX Need to have a no-title version of embedded windows.
            layoutResource = com.android.internal.R.layout.screen_progress;
            // System.out.println("Progress!");
        } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
            // Special case for a window with a custom title.
            // If the window is floating, we need a dialog layout
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        com.android.internal.R.attr.dialogCustomTitleDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else {
                layoutResource = com.android.internal.R.layout.screen_custom_title;
            }
            // XXX Remove this once action bar supports these features.
            removeFeature(FEATURE_ACTION_BAR);
        } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
            // If no other features and not embedded, only need a title.
            // If the window is floating, we need a dialog layout
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        com.android.internal.R.attr.dialogTitleDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
                layoutResource = com.android.internal.R.layout.screen_action_bar;
            } else {
                layoutResource = com.android.internal.R.layout.screen_title;
            }
            // System.out.println("Title!");
        } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
            layoutResource = com.android.internal.R.layout.screen_simple_overlay_action_mode;
        } else {
            // Embedded, so no decoration is needed.
            layoutResource = com.android.internal.R.layout.screen_simple;
            // System.out.println("Simple!");
        }
        mDecor.startChanging();
//3、加载选定好的布局文件,添加至décor中,并且指定contentParent的值
        View in = mLayoutInflater.inflate(layoutResource, null);
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
//contentParent其实就是我们顶级View中的内容栏。我们设置的就是这个//ViewGroup的内容。 
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }
        if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
            ProgressBar progress = getCircularProgressBar(false);
            if (progress != null) {
                progress.setIndeterminate(true);
            }
        }
//4、后面就是设置一些background,title等的属性了
        // Remaining setup -- of background and title -- that only applies
        // to top-level windows.
        if (getContainer() == null) {
            Drawable drawable = mBackgroundDrawable;
            if (mBackgroundResource != 0) {
                drawable = getContext().getResources().getDrawable(mBackgroundResource);
            }
            mDecor.setWindowBackground(drawable);
            drawable = null;
            if (mFrameResource != 0) {
                drawable = getContext().getResources().getDrawable(mFrameResource);
            }
            mDecor.setWindowFrame(drawable);

            // System.out.println("Text=" + Integer.toHexString(mTextColor) +
            // " Sel=" + Integer.toHexString(mTextSelectedColor) +
            // " Title=" + Integer.toHexString(mTitleColor));

            if (mTitleColor == 0) {
                mTitleColor = mTextColor;
            }

            if (mTitle != null) {
                setTitle(mTitle);
            }
            setTitleColor(mTitleColor);
        }
        mDecor.finishChanging();
        return contentParent;
    }

通过这个方法,可以看到大概的步骤如下:

1、 获取窗口的各种style属性,设置title是否显示,窗口是否浮动等

2、 根据获取到的feature值来选择不同的窗口布局文件(窗口修饰类型包括有全屏FullScreen,不含标题栏NoTitleBar等)

PS:选定窗口修饰类型有两种方法,第一种是指定requestFeature()指定窗口修饰符,window对象调用getLocalFeature()方法获取值;第二种是在activity的配置文件中设置主题,android:theme="",这样的话window对象就会通过代码中的getWindowStyle()方法来获取。比如不需要标题栏的时候,我们可以使用requestWindowFeature(Window.FEATURE_NO_TITLE)或者在配置文件中使用android:theme="@android:style/Theme.NoTitleBar."

确定好窗口风格之后,选择对应的布局文件,文件在frameworks/base/core/res/layout/中,例如:

<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2006 The Android Open Source Project

<!--
This is an optimized layout for a screen, with the minimum set of features
enabled.
-->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:fitsSystemWindows="true">
    <!-- Popout bar for action modes -->
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content" />
    <FrameLayout
        android:layout_width="match_parent" 
        android:layout_height="?android:attr/windowTitleSize"
        style="?android:attr/windowTitleBackgroundStyle">
        <TextView android:id="@android:id/title" 
            style="?android:attr/windowTitleStyle"
            android:background="@null"
            android:fadingEdge="horizontal"
            android:gravity="center_vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
    <FrameLayout android:id="@android:id/content"
        android:layout_width="match_parent" 
        android:layout_height="0dip"
        android:layout_weight="1"
        android:foregroundGravity="fill_horizontal|top"
        android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
这是其中screen_title的布局文件,可以看到里面有两个ViewGroup,第一个是id为title的标题布局,第二个就是我们平时使用的id为content的内容布局了

3、 加载前面选择的布局,并放入顶级Décor中,继而获取id为content的View,将其赋值给mContentParent。

PS:LayoutInflater通常用于动态加载布局或者控件

4、设置背景、滚动条以及title的一些属性

回到setContentView方法里面,如果我们的内容栏是null,那么执行上面的流程,如果不是,那么清空里面的View,后面重点来了,通过LayoutInflater的inflate方法来加载我们的布局文件,注意第二个参数是先前说的mContentParent,就是内容栏,下面来看看LayoutInflater的inflate方法源码:

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context)mConstructorArgs[0];
            mConstructorArgs[0] = mContext;
            View result = root;
            try {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }
                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }
                final String name = parser.getName();              
                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }
                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, attrs);
                } else {
                    // Temp is the root view that was found in the xml
                    View temp = createViewFromTag(name, attrs);
                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }
                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }
                    // Inflate all children under temp
                    rInflate(parser, temp, attrs);
                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                InflateException ex = new InflateException(e.getMessage());
                ex.initCause(e);
                throw ex;
            } catch (IOException e) {
                InflateException ex = new InflateException(
                        parser.getPositionDescription()
                        + ": " + e.getMessage());
                ex.initCause(e);
                throw ex;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;
            }

            return result;
        }
}
方法也比较简单,知道Pull解析方法的都能看出来,inflate方法主要是靠android提供的Pull解析方法来解析我们的xml布局文件进行加载的,具体的Pull解析就不再解释了,这里看到25行,判断解析的是否merge标签,第34行,开始解析根布局,createViewFromTag方法,里面又执行了creatView方法

public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Constructor constructor = sConstructorMap.get(name);
        Class clazz = null;

        try {
            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name);
                
                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
                constructor = clazz.getConstructor(mConstructorSignature);
                sConstructorMap.put(name, constructor);
            } else {
                // If we have a filter, apply it to cached constructor
                if (mFilter != null) {
                    // Have we seen this name before?
                    Boolean allowedState = mFilterMap.get(name);
                    if (allowedState == null) {
                        // New class -- remember whether it is allowed
                        clazz = mContext.getClassLoader().loadClass(
                                prefix != null ? (prefix + name) : name);
                        
                        boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                        mFilterMap.put(name, allowed);
                        if (!allowed) {
                            failNotAllowed(name, prefix, attrs);
                        }
                    } else if (allowedState.equals(Boolean.FALSE)) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
            }

            Object[] args = mConstructorArgs;
            args[1] = attrs;
            return (View) constructor.newInstance(args);

        } catch (NoSuchMethodException e) {
            InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class "
                    + (prefix != null ? (prefix + name) : name));
            ie.initCause(e);
            throw ie;

        } catch (ClassNotFoundException e) {
            // If loadClass fails, we should propagate the exception.
            throw e;
        } catch (Exception e) {
            InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class "
                    + (clazz == null ? "<unknown>" : clazz.getName()));
            ie.initCause(e);
            throw ie;
        }
    }
这里面其实就是利用了反射的机制来创建我们的View,这里不再细说,回到主流程上面,在创建完根控件temp之后,通过54行的rInflate方法来循环解析根控件下面的子元素,将刚创建完的根控件temp传入方法内作为parent,rInflate(parser,temp,attrs),

private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs)
            throws XmlPullParserException, IOException {
        final int depth = parser.getDepth();
        int type;

        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            final String name = parser.getName();
            
            if (TAG_REQUEST_FOCUS.equals(name)) {
                parseRequestFocus(parser, parent);
            } else if (TAG_INCLUDE.equals(name)) {
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
            } else {
                final View view = createViewFromTag(name, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflate(parser, view, attrs);
                viewGroup.addView(view, params);
            }
        }
        parent.onFinishInflate();
    }
方法也比较简单,在第25行,根据相同的方法创建了次级View,并且递归调用rInflate方法来解析里面的子元素,完毕之后通过viewGroup的addView方法将解析的View添加进去,或许有的朋友已经注意到了,在上面的inflate方法内,循环解析完之后,在62行,也是通过了addView方法来将解析完的View添加进root中,root就是之前的mContentParent,所以下面来分析ViewGroup的addView方法。

二、addView

废话不说,直接上源码:

public void addView(View child, int index, LayoutParams params) {
    if (DBG) {
        System.out.println(this + " addView");
    }

    // addViewInner() will call child.requestLayout() when setting the new LayoutParams
    // therefore, we call requestLayout() on ourselves before, so that the child's request
    // will be blocked at our level
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}

里面有个requestLayout方法,进去看看

public void requestLayout() {
    if (mMeasureCache != null) mMeasureCache.clear();

    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
        // Only trigger request-during-layout logic if this is the view requesting it,
        // not the views in its parent hierarchy
        ViewRootImpl viewRoot = getViewRootImpl();
        if (viewRoot != null && viewRoot.isInLayout()) {
            if (!viewRoot.requestLayoutDuringLayout(this)) {
                return;
            }
        }
        mAttachInfo.mViewRequestingLayout = this;
    }

    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;

    if (mParent != null && !mParent.isLayoutRequested()) {
        mParent.requestLayout();
    }
    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
        mAttachInfo.mViewRequestingLayout = null;
    }
}
第20行,有个mParent.requestLayout()方法,mParent是什么呢,通过源码可以看到,mParent是个ViewParent,ViewParent是个接口,它的实现类是ViewRoot(2.x版本的时候,后续版本被ViewRootImpl类所替代,为了方便起见,这里分析ViewRoot,ViewRootImel代码比较复杂但是流程原理是一样的,容许我偷个懒。。。),所以进入ViewRoot类中来看看requestLayout方法是什么样的

public void requestLayout() {
    checkThread();
    mLayoutRequested = true;
    scheduleTraversals();
}
代码很简单,看第一个方法checkThread

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}
这个异常好熟悉啊,这里就是在视图改变或者更新的时候需要判断当前线程是否为主线程,如果不是主线程(UI线程),那么就会报上面的异常。

继续往下看scheduleTraversals方法

public void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        sendEmptyMessage(DO_TRAVERSAL);
    }
}

这里可以看到里面发送了一条消息,参数是一个常量(1000),不知道大家注意没,这个ViewRoot的父类是handler,其本身就是一个handler

public final class ViewRoot extends Handler implements ViewParent{...}

所以发送的消息肯定就在它的handleMessage方法内,继续看

@Override
public void handleMessage(Message msg) {
    switch (msg.what) {
    case View.AttachInfo.INVALIDATE_MSG:
        ((View) msg.obj).invalidate();
        break;
    case View.AttachInfo.INVALIDATE_RECT_MSG:
        final View.AttachInfo.InvalidateInfo info = (View.AttachInfo.InvalidateInfo) msg.obj;
        info.target.invalidate(info.left, info.top, info.right, info.bottom);
        info.release();
        break;
    case DO_TRAVERSAL:
        if (mProfile) {
            Debug.startMethodTracing("ViewRoot");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
        break;
. . . 
	}
}

这里我只贴出了部分代码,我们看到相应的case里面执行了performTraversals方法,performTraversals方法源码很长,但是可以用下面的伪代码来表示

private void performTraversals() {
    // cache mView since it is used so much below...
    final View host = mView;
……
	host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
……
	host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
……
	draw(fullRedrawNeeded);
……
}


是不是很熟悉呢,这里面就是View的绘制流程了,我们熟悉的measure(),layout(),draw()方法都在这里依次执行。


源码分析就到这里,走过路过的朋友,欢迎留言指出不足,一起学习共同进步。



















评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值