RecycleView的复用和回收分析


以前的我看源码,属于那种沉浸在代码细节里面不可自拔的人,一旦在细节里迷失了,就放弃了,经过大半年的工作和学习,逐渐找到了自己看源码的一些思路。


一、看代码的一些思路?

  1. 带着问题,自顶向下,先从最简单的流程分析。比如看属性动画源码的时候,我先从属性动画类的构造函数开始,找到主要的初始化代码,虽然一开始不知道哪个是主要的,不过可以回来看,然后在查看他的setFloatValues()方法,之后在查看他的start()方法这就是最简单的思路了。
  2. 如果找到某个方法断了,可以尝试搜索这个方法调用的地方,如果调用的地方特别多,就不要用了,比如我看了start的方法,发现属性动画start方法只绘制动画的一帧,就很奇怪,然后我想到了这个方法里面的注册的回调方法,最终找到了doFrame方法,顺腾摸瓜,学习了android的Choreographer类的运作过程,收获颇多。
  3. 实在不行就百度,但是我尽量不百度,因为要培养自己的能力,百度了很不甘心,但是自己是个新手,就先百度吧,然后根据博主讲的内容,自己按照源码分析一下,这样更快,也不一定非要看完博客文章的所有内容。
  4. 看源码切记有的放矢,先了解主干,顺着主干去分析,期间的话可以通过画图的方式去理解,虽然我很少画图,以后慢慢培养自己的能力吧。
  5. 也可以使用调试,demo运行的方式去猜测里面怎么实现的,实在不行就放一放,如果不是很紧急。

二、提出问题,探索问题

1.问题

  1. RecycleView 怎么回收view的,也就是什么情况下View会被回收。
  2. RecycleView怎么复用View的(这里我的第二篇文章再分析吧)。
  3. RecycleView如何实现滑动的效果的。

现在我们从源头开始探索,如果让我设计一个显示List列表的一个控件,我会怎么实现呢?

  1. 首先是继承Linerlayout,然后有多少数据,往里面加入多少控件呗。
  2. 目前这个控件不能滑动,为了解决滑动问题,我根据分发机制,让这个View进行消费手势,根据她滑动的距离,我用Scroller实现就好了,之后抬手的时候加上惯性滑动。
  3. 但是如果数据很多,会造成内存的大量使用而且是有可能会出现oom的情况,再深入思考一下,我们可以只让这个控件显示用户需要看到的部分,其他部分就不需要显示了,对资源合理的应用。
  4. 那控件是在滑动的,不可能只把控件的位置写死,会出现一些控件只显示一部分的情况,那应该怎么办呢,就不能把控件位置写死,内部控件位置,根据手势滑动情况,进行变化,但是显示只显示需要的部分。
  5. 我们可以复用之前已经创建的View,也就是我们不一定要创建显示只用来显示的View,也可以创建一个缓存池,缓存一定量的View,到时候直接从可以优先从里面拿,这样就避免了View的频繁创建。

2.从具体的模型入手

在这里插入图片描述

上图是我用RecycleView显示的列表,带颜色区域是item,item的属性为margin_top :10dp,在这种情况下,假设向上滑动了一段距离,RecycleView该如何显示列表呢。
先简单分析一下:
1假设第一个item的底部最终会离开屏幕,那么就需要对这个item进行回收。
2.假设需要显示第七个item,那就需要调用addView(-1)这个方法,在底部添加View,这个添加的View优先从缓存中拿,如果缓存中不满足,那么就新建ViewHoler,然后添加之后,再检测View需要不需要回收。
3.对滑动的距离进行消费,也就是让item滑动到相应的位置。

三、源码分析

这里我们就分析主要的代码就好了,至于之前的onMeasure方法和onLayout的方法,先不进行分析。
假设我们使用的是LinearLayoutManager,做竖直方向滑动。

3.1 scrollBy方法

 int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
 //RecyclerView.State保存了一些RecycleView的一些状态信息,在这个流程里需要用到这些信息
 //RecyclerView.Recycler是回收逻辑类,回收的具体逻辑在这类里面实现
 //delta是滑动距离,往上滑动delta为正值,反之负值。
        if (getChildCount() == 0 || delta == 0) {
            return 0;
        }
        //如果mLayoutState为空,就新建,说明mLayoutState是单例的。
        ensureLayoutState();
        //设置回收状态为true
        mLayoutState.mRecycle = true;
        //向上滑动为LAYOUT_END,因为是底部View不断上滑,向下滑动为LAYOUT_START,因为是顶部View不断上滑。
        final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
        //absDelta为滑动长度
        final int absDelta = Math.abs(delta);
        //初始化当前mLayoutState的内容,前往3.2查看
        updateLayoutState(layoutDirection, absDelta, true, state);
        //消费的距离,前往3.3查看
        final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);
        if (consumed < 0) {
            if (DEBUG) {
                Log.d(TAG, "Don't have any more elements to scroll");
            }
            return 0;
        }
        //给滑动距离赋值,absDelta和consumed取绝对值小的
        final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
        //这里最终会遍历每个item,调用offsetTopAndBottom进行View的位置设置
        mOrientationHelper.offsetChildren(-scrolled);
        if (DEBUG) {
            Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled);
        }
        mLayoutState.mLastScrollDelta = scrolled;
        return scrolled;
    }

3.2 updateLayoutState 方法

// A code block
 private void updateLayoutState(int layoutDirection, int requiredSpace,
            boolean canUseExistingSpace, RecyclerView.State state) {
        // If parent provides a hint, don't measure unlimited.
        mLayoutState.mInfinite = resolveIsInfinite();
        mLayoutState.mLayoutDirection = layoutDirection;
        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        calculateExtraLayoutSpace(state, mReusableIntPair);
        //一般情况mReusableIntPair数组中的两个元素都为0
        int extraForStart = Math.max(0, mReusableIntPair[0]);
        int extraForEnd = Math.max(0, mReusableIntPair[1]);
        boolean layoutToEnd = layoutDirection == LayoutState.LAYOUT_END;
        mLayoutState.mExtraFillSpace = layoutToEnd ? extraForEnd : extraForStart;
        mLayoutState.mNoRecycleSpace = layoutToEnd ? extraForStart : extraForEnd;
        int scrollingOffset;
        if (layoutToEnd) {
           //主要查看这里的流程
            mLayoutState.mExtraFillSpace += mOrientationHelper.getEndPadding();
            // get the first child in the direction we are going
                //获取列表中,屏幕底部的View
            final View child = getChildClosestToEnd();
            // the direction in which we are traversing children
            mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                    : LayoutState.ITEM_DIRECTION_TAIL;
            mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
                //这里注意mOffset为该View的bottom+marginBottom
            mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
            // calculate how much we can scroll without adding new children (independent of layout)
             //这里注意scrollingOffset 为的mOffset -(RecycleView的高度-RecycleView的paddingBottom)
             //也就是该View距离超出屏幕显示范围的距离
            scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
                    - mOrientationHelper.getEndAfterPadding();

        } else {
            final View child = getChildClosestToStart();
            mLayoutState.mExtraFillSpace += mOrientationHelper.getStartAfterPadding();
            mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
                    : LayoutState.ITEM_DIRECTION_HEAD;
            mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;       
            mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child);
            scrollingOffset = -mOrientationHelper.getDecoratedStart(child)
                    + mOrientationHelper.getStartAfterPadding();
        }
        mLayoutState.mAvailable = requiredSpace;
        if (canUseExistingSpace) {
        //这里注意mAvailable 为滑动距离-scrollingOffset
            mLayoutState.mAvailable -= scrollingOffset;
        }
        mLayoutState.mScrollingOffset = scrollingOffset;
    }

上面分析了三个距离,需要注意scrollingOffset 为底部View的超出屏幕的距离,mAvailable为滑动距离-超出距离,也就是需要填充的View的距离,mOffset为底部View的bottom+marginBottom的距离。
我们用图表示一下:
在这里插入图片描述

3.3 updateLayoutState 方法

//fill 方法是主要方法,会发起回收的逻辑和添加item view的逻辑
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        // max offset we should set is mFastScroll + available
        final int start = layoutState.mAvailable;
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            // TODO ugly bug fix. should not happen
           //如果滑动距离小于mScrollingOffset,那么这里会先进行一次回收的判断
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            //判断回收View的方法,请前往3.4查看
            recycleByLayoutState(recycler, layoutState);
        }
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        //用于添加View方法,结果反馈的类。
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        //如果需要填充的距离大于0并且没有到达列表的极限数量
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        //初始化该类状态
            layoutChunkResult.resetInternal();
            if (RecyclerView.VERBOSE_TRACING) {
                TraceCompat.beginSection("LLM LayoutChunk");
            }
            //添加View方法,前往3.5查看
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if (RecyclerView.VERBOSE_TRACING) {
                TraceCompat.endSection();
            }
            if (layoutChunkResult.mFinished) {
                break;
            }
            //mConsumed 一般是添加进入的View的高度
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            /**
             * Consume the available space if:
             * * layoutChunk did not request to be ignored
             * * OR we are laying out scrap children
             * * OR we are not doing pre-layout
             */
            if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                    || !state.isPreLayout()) {
                    //可填充距离-添加View的高度
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                // we keep a separate remaining space because mAvailable is important for recycling
                remainingSpace -= layoutChunkResult.mConsumed;
            }

            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            //mScrollingOffset+添加View高度
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                //这里一般mScrollingOffset赋值为滑动距离
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                //这里进行回收操作
                recycleByLayoutState(recycler, layoutState);
            }
            if (stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
        }
        if (DEBUG) {
            validateChildOrder();
        }
        //开始的mAvailable-最终的mAvailable
        return start - layoutState.mAvailable;
    }

这里主要做了三个操作,第一个操作时,如果滑动距离小于底部View的mScrollingOffset,那么就做回收操作,如果大于且item的数量没有到达极限,就进入while循环,不停的填充View,并且进行回收操作。

3.4 recycleByLayoutState方法

recycleByLayoutState:

   private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
        if (!layoutState.mRecycle || layoutState.mInfinite) {
            return;
        }
        int scrollingOffset = layoutState.mScrollingOffset;
        int noRecycleSpace = layoutState.mNoRecycleSpace;
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
        } else {
        //向上滑动调用这里,从顶部开始回收
            recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
        }
    }

recycleViewsFromStart:

  private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,
            int noRecycleSpace) {
        if (scrollingOffset < 0) {
            if (DEBUG) {
                Log.d(TAG, "Called recycle from start with a negative value. This might happen"
                        + " during layout changes but may be sign of a bug");
            }
            return;
        }
        // ignore padding, ViewGroup may not clip children.
        //limit 一般赋值为scrollingOffset
        //noRecycleSpace一般是0
        final int limit = scrollingOffset - noRecycleSpace;
        final int childCount = getChildCount();
        if (mShouldReverseLayout) {
            for (int i = childCount - 1; i >= 0; i--) {
                View child = getChildAt(i);
                if (mOrientationHelper.getDecoratedEnd(child) > limit
                        || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                    // stop here
                    recycleChildren(recycler, childCount - 1, i);
                    return;
                }
            }
        } else {
        //一般会执行这里的方法,如果顶部View的Bottom小于limit(滑动距离),需要回收了。
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                if (mOrientationHelper.getDecoratedEnd(child) > limit
                        || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                    // stop here
                    //回收,如果i==0则不回收,其他情况进行回收。
                    recycleChildren(recycler, 0, i);
                    return;
                }
            }
        }
    }

recycleChildren方法会遍历0到i下标的view,调用removeAndRecycleViewAt(i)

removeAndRecycleViewAt:

 public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
            final View view = getChildAt(index);
            //将view从RecycleView中移除
            removeViewAt(index);
            //调用回收方法
            recycler.recycleView(view);
        }

Recycler ::recycleView

 public void recycleView(@NonNull View view) {
         	//获取对应ViewHolder
            ViewHolder holder = getChildViewHolderInt(view);
            if (holder.isTmpDetached()) {
                removeDetachedView(view, false);
            }
            if (holder.isScrap()) {
                holder.unScrap();
            } else if (holder.wasReturnedFromScrap()) {
                holder.clearReturnedFromScrapFlag();
            }
            //这里是主要的回收流程
            recycleViewHolderInternal(holder);
          
            if (mItemAnimator != null && !holder.isRecyclable()) {
                mItemAnimator.endAnimation(holder);
            }
        }

recycleViewHolderInternal:

//这个方法针对于mCachedViews和mRecyclerPool进行回收
 void recycleViewHolderInternal(ViewHolder holder) {
           ...//上面是一些验证操作
            if (forceRecycle || holder.isRecyclable()) {
                if (mViewCacheMax > 0
                        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                        | ViewHolder.FLAG_REMOVED
                        | ViewHolder.FLAG_UPDATE
                        | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                    // Retire oldest cached view
                    int cachedViewSize = mCachedViews.size();
                    //如果cachedViewSize大于2,那么就移除首部的元素
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }

                    int targetCacheIndex = cachedViewSize;
                    if (ALLOW_THREAD_GAP_WORK
                            && cachedViewSize > 0
                            //这里根据lastPrefetchIncludedPosition方法进行判断
                            //mPrefetchRegistry中存在一个保存position的缓存数组
                            //根据position的缓存次数,来选择再mCachedViews中的插入位置
                            //类似于LRU
                            && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                        // when adding the view, skip past most recently prefetched views
                        int cacheIndex = cachedViewSize - 1;
                        while (cacheIndex >= 0) {
                            int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                            if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                                break;
                            }
                            cacheIndex--;
                        }
                        targetCacheIndex = cacheIndex + 1;
                    }
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
                //如果没有加入mCachedViews,就加入到缓存池中
                if (!cached) {
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            } else {
              
                if (DEBUG) {
                    Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
                            + "re-visit here. We are still removing it from animation lists"
                            + exceptionLabel());
                }
            }
            mViewInfoStore.removeViewHolder(holder);
            if (!cached && !recycled && transientStatePreventsRecycling) {
                holder.mOwnerRecyclerView = null;
            }
        }

mRecyclerPool中有一个map,map中的key为ViewHolder的type,value为ScrapData,ScrapData中有一个List列表,保存对应的缓存,最大长度为5。

3.4 layoutChunk方法

layoutChunk:

//这个方法是填充View的方法
  void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
            //获取View,从缓存中获取或者新建,下一节重点讲这个方法。
        View view = layoutState.next(recycler);
        if (view == null) {
            if (DEBUG && layoutState.mScrapList == null) {
                throw new RuntimeException("received null view when unexpected");
            }
            // if we are laying out views in scrap, this may return null which means there is
            // no more items to layout.
            result.mFinished = true;
            return;
        }
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
        //如果为上滑
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                    //底部会调用RecycleView的addView的方法,在底部添加一个View
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
        //根据View的layoutParams和RecycleView的一些属性,调用View的measure方法
        measureChildWithMargins(view, 0, 0);
         //mConsumed 为View的测量高度加上 上下margin的高度,加上view的layoutparams中的 mDecorInsets的bottom和top的值
         //mDecorInsets和RecycleView的中的ItemDecoration的getItemOffsets方法相关。
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        int left, top, right, bottom;
        if (mOrientation == VERTICAL) {
            if (isLayoutRTL()) {
                right = getWidth() - getPaddingRight();
                left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
            } else {
                left = getPaddingLeft();
                right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
            }
            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                bottom = layoutState.mOffset;
                top = layoutState.mOffset - result.mConsumed;
            } else {
                top = layoutState.mOffset;
                bottom = layoutState.mOffset + result.mConsumed;
            }
        } else {
            top = getPaddingTop();
            bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);

            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                right = layoutState.mOffset;
                left = layoutState.mOffset - result.mConsumed;
            } else {
                left = layoutState.mOffset;
                right = layoutState.mOffset + result.mConsumed;
            }
        }
        // We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.
        //调用View的layout方法
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        if (DEBUG) {
            Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
                    + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
                    + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
        }
        // Consume the available space if the view is not removed OR changed
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }
        result.mFocusable = view.hasFocusable();
    }

这个方法主要工作就是填充一个View,然后调用View的measure方法,之后给mConsumed赋值,再调用View的layout方法。

3.4 mAttachedScrap缓存

上面讲到回收的方法,只是针对于mCachedViews和mRecyclerPool的缓存,而mAttachedScrap没有涉及到。下面来讲一下这个缓存
在recycleView的绘制方法中,会调用到LayoutManager的onLayoutChildren方法。

onLayoutChildren:

...
//遍历item,加入到mAttachedScrap中
    detachAndScrapAttachedViews(recycler);
 ...//之后fill填充会用到mAttachedScrap中的缓存

detachAndScrapAttachedViews:

 public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
            final int childCount = getChildCount();
            for (int i = childCount - 1; i >= 0; i--) {
                final View v = getChildAt(i);
                scrapOrRecycleView(recycler, i, v);
            }
        }

scrapOrRecycleView:

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
            final ViewHolder viewHolder = getChildViewHolderInt(view);
            if (viewHolder.shouldIgnore()) {
                if (DEBUG) {
                    Log.d(TAG, "ignoring view " + viewHolder);
                }
                return;
            }
            if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                    && !mRecyclerView.mAdapter.hasStableIds()) {
                removeViewAt(index);
                recycler.recycleViewHolderInternal(viewHolder);
            } else {
                detachViewAt(index);
                //分析该方法
                recycler.scrapView(view);
                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
            }
        }

Recycler :: scrapView:

  void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                    || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
                if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
                    throw new IllegalArgumentException("Called scrap view with an invalid view."
                            + " Invalid views cannot be reused from scrap, they should rebound from"
                            + " recycler pool." + exceptionLabel());
                }
                holder.setScrapContainer(this, false);
                //将ViewHolder加入
                mAttachedScrap.add(holder);
            } else {
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                holder.setScrapContainer(this, true);
                mChangedScrap.add(holder);
            }
        }

总结

经过了上面的分析之后,大概对RecycleView的滑动和回收过程有了一个了解,如果要是让我来做一个RecycleView的缓存,我可能只有一个缓存池,就没了,RecycleView缓存的高明之处就是对缓存的粒度进行一个切分,针对于不同的情境,将回收的内容放入对应的缓存当中,关于缓存的分析,下篇文章会进行深入探索。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值