WindowManagerService Inset(5)

WindowManagerService Inset(5)

简介

窗口中有一个Inset的概念,比较常见的主要是状态栏,导航栏,输入法。我们先来说说Inset的作用,前面我们已经了解了WMS树是怎么管理窗口信息的,我们平时在使用app时,点击输入框后弹出输入法,输入法窗口可以把我们app窗口往上抬,前面我们知道输入法窗口所在的节点是13,14层,而app窗口所在节点是第2层,并不是父子关系或者直接的兄弟节点。这种关联如果仅仅靠之前树的结构不太好实现,Inset就是用来实现处理这种场景的。

Inset在框架侧的主要数据结构

WMS中每个DisplayContent通过一个InsetsStateController来管理该屏幕上所有的Insets,而InsetsStateController又通过InsetsState来管理所有Insets,有两个InsetsState,一个是当前用于显示的,一个是在更改的(类似双缓冲)。
InsetsStateController还有一个InsetsSourceProvider,他持有一个InsetsSource。InsetsState中则记录的所有的InsetsSource。
InsetsSource表示一个Insets的元数据。而InsetsSourceProvider则包含了一些额外的计算逻辑以及一些动画处理。
在这里插入图片描述

Inset创建过程

Inset是在添加窗口时创建的,我们用原生的SystemUi里面NavigationBar来举例。
在通过addView添加窗口时,我们需要提供一个LayoutParams,而Inset就是通过设置LayoutParams的providedInsets实现的,我们来看看NavigationBar的这个变量的设置。

1.1 getBarLayoutParamsForRotation

private WindowManager.LayoutParams getBarLayoutParamsForRotation(int rotation) {
    // ...
    // 详见1.2
    lp.providedInsets = getInsetsFrameProvider(insetsHeight, userContext);
    // ...
}

1.2 getInsetsFrameProvider
该方法返回了一个InsetsFrameProvider数组,其中每一个InsetsFrameProvider都代表需要创建的一个Inset,可以看到这里会构建非常多个InsetsFrameProvider
其中比较重要的是每个InsetsFrameProvider都会通过setInsetsSize设置这个Inset的大小,注意这里上下左右并不是说这个Inset上下左右所在的位置,而是说这个窗口上下左右分别要预留出多少作为Inset,后面我们会看到它的计算方式。
这里我们就盯着navBarProvider来看吧,这里仅仅给他设置了一个bottom为insetsHeight,left,top,right都为0.
InsetsFrameProvider除了一个InsetsSize以外,还有一个通过setInsetsSizeOverrides来设置的InsetsSizeOverrides,这个是针对特定类型窗口计算Insets占位时,Insets有特定大小。

private InsetsFrameProvider[] getInsetsFrameProvider(int insetsHeight, Context userContext) {
    // 构造navBarProvider
    final InsetsFrameProvider navBarProvider =
            new InsetsFrameProvider(mInsetsSourceOwner, 0, WindowInsets.Type.navigationBars())
                    .setInsetsSizeOverrides(new InsetsFrameProvider.InsetsSizeOverride[] {
                            new InsetsFrameProvider.InsetsSizeOverride(
                                    TYPE_INPUT_METHOD, null)});
    if (insetsHeight != -1 && !mEdgeBackGestureHandler.isButtonForcedVisible()) {
        // 设置navBarProvider的InsetSize
        navBarProvider.setInsetsSize(Insets.of(0, 0, 0, insetsHeight));
    }
    final boolean needsScrim = userContext.getResources().getBoolean(
            com.android.internal.R.bool.config_navBarNeedsScrim);
    navBarProvider.setFlags(needsScrim ? 0 : FLAG_SUPPRESS_SCRIM, FLAG_SUPPRESS_SCRIM);
    // ...
    return new InsetsFrameProvider[] {
            navBarProvider,
            tappableElementProvider,
            mandatoryGestureProvider,
            new InsetsFrameProvider(mInsetsSourceOwner, 0, WindowInsets.Type.systemGestures())
                    .setSource(InsetsFrameProvider.SOURCE_DISPLAY)
                    .setInsetsSize(Insets.of(gestureInsetsLeft, 0, 0, 0))
                    .setMinimalInsetsSizeInDisplayCutoutSafe(
                            Insets.of(gestureInsetsLeft, 0, 0, 0)),
            new InsetsFrameProvider(mInsetsSourceOwner, 1, WindowInsets.Type.systemGestures())
                    .setSource(InsetsFrameProvider.SOURCE_DISPLAY)
                    .setInsetsSize(Insets.of(0, 0, gestureInsetsRight, 0))
                    .setMinimalInsetsSizeInDisplayCutoutSafe(
                            Insets.of(0, 0, gestureInsetsRight, 0))
    };
}

1.3 WindowManagerService.addWindow
中间addView添加窗口过程上一章介绍过,这里直接看addWindow方法中和Inset相关部分的逻辑。
addWindowLw里面会添加Inset,updateAboveInsetsState会更新Insets,而这里参数false表示不要通知app侧Inset的变更,因为我们前面看过这里addWindow之一把WindowState添加的窗口树下,并没有做窗口的大小都测量,所以这里是没有准确的窗口大小的,那么Insets的计算依赖于窗口大小,我们一会会看到。

public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
        int displayId, int requestUserId, @InsetsType int requestedVisibleTypes,
        InputChannel outInputChannel, InsetsState outInsetsState,
        InsetsSourceControl.Array outActiveControls, Rect outAttachedFrame,
        float[] outSizeCompatScale) {
    // ...详见1.4
    displayPolicy.addWindowLw(win, attrs);
    // ...
    displayContent.getInsetsStateController().updateAboveInsetsState(
                        false /* notifyInsetsChanged */);
    // ...
    // 回传InsetsState给app
    outInsetsState.set(win.getCompatInsetsState(), true /* copySources */);
    // 构建InsetsSourceControl数组给app,它包含了Insets和回传的目标窗口关联的一些信息。  
    getInsetsSourceControls(win, outActiveControls);
}

1.4 DisplayPolicy.addWindowLw

void addWindowLw(WindowState win, WindowManager.LayoutParams attrs) {
    switch (attrs.type) {
        case TYPE_NOTIFICATION_SHADE:
            mNotificationShade = win;
            break;
        case TYPE_STATUS_BAR:
            mStatusBar = win;
            break;
        case TYPE_NAVIGATION_BAR:
            mNavigationBar = win;
            break;
    }
    // 前面看到这个providedInsets是app侧layoutParams带过来的,是InsetsFrameProvider数组,这里遍历这个数组,构建Insets
    if (attrs.providedInsets != null) {
        for (int i = attrs.providedInsets.length - 1; i >= 0; i--) {
            final InsetsFrameProvider provider = attrs.providedInsets[i];
            // 这里获取的是一个回调函数,详见1.4.1
            final TriFunction<DisplayFrames, WindowContainer, Rect, Integer> frameProvider =
                    getFrameProvider(win, i, INSETS_OVERRIDE_INDEX_INVALID);
            final InsetsFrameProvider.InsetsSizeOverride[] overrides =
                    provider.getInsetsSizeOverrides();
            final SparseArray<TriFunction<DisplayFrames, WindowContainer, Rect, Integer>>
                    overrideProviders;
            if (overrides != null) {
                overrideProviders = new SparseArray<>();
                for (int j = overrides.length - 1; j >= 0; j--) {
                    overrideProviders.put(
                            overrides[j].getWindowType(), getFrameProvider(win, i, j));
                }
            } else {
                overrideProviders = null;
            }
            // 这里会构建一个Inset所关联的InsetSourceProvider,并调用setWindowContainer将InsetSourceProvider和WindowContainer相互关联。详见1.5
            mDisplayContent.getInsetsStateController().getOrCreateSourceProvider(
                    provider.getId(), provider.getType()).setWindowContainer(
                            win, frameProvider, overrideProviders);
            mInsetsSourceWindowsExceptIme.add(win);
        }
    }
}

1.4.1 getFrameProvider
这个方法会根据display包含的信息和窗口自身信息,以及Inset来计算出最终窗口大小信息。displayFrames主要包含一些和屏幕相关的信息,水平或者垂直,屏幕可见区域,可见区域剪去屏幕的cutout区域剩余区域。cutout是指例如刘海屏的刘海区域。
最终计算出Inset的实际区域存储在inOutFrame里。这个回调方法在窗口大小变化的时候会调用,因为Insets区域和窗口是有关联的,所以当窗口大小发生了变化,就需要重新计算Insets区域,而Insets变化后,可能又会引起其他窗口变化,所以WMS对窗口的测量的方法可能会重复执行,后续我们会专门介绍这个流程。

private static TriFunction<DisplayFrames, WindowContainer, Rect, Integer> getFrameProvider(
        WindowState win, int index, int overrideIndex) {
    return (displayFrames, windowContainer, inOutFrame) -> {
        final LayoutParams lp = win.mAttrs.forRotation(displayFrames.mRotation);
        final InsetsFrameProvider ifp = lp.providedInsets[index];
        final Rect displayFrame = displayFrames.mUnrestricted;
        final Rect safe = displayFrames.mDisplayCutoutSafe;
        boolean extendByCutout = false;
        // 根据type不同选择源大小是窗口大小还是屏幕大小。
        switch (ifp.getSource()) {
            case SOURCE_DISPLAY:
                inOutFrame.set(displayFrame);
                break;
            case SOURCE_CONTAINER_BOUNDS:
                inOutFrame.set(windowContainer.getBounds());
                break;
            case SOURCE_FRAME:
                extendByCutout =
                        (lp.privateFlags & PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT) != 0;
                break;
            case SOURCE_ARBITRARY_RECTANGLE:
                inOutFrame.set(ifp.getArbitraryRectangle());
                break;
        }
        final Insets insetsSize = overrideIndex == INSETS_OVERRIDE_INDEX_INVALID
                ? ifp.getInsetsSize()
                : ifp.getInsetsSizeOverrides()[overrideIndex].getInsetsSize();

        if (ifp.getMinimalInsetsSizeInDisplayCutoutSafe() != null) {
            sTmpRect2.set(inOutFrame);
        }
        // inOutFrame上面设置成源大小,insetsSize是Inset的大小。详见1.4.2
        calculateInsetsFrame(inOutFrame, insetsSize);
        // extendByCutout表示需要适配cutout区域。
        if (extendByCutout && insetsSize != null) {
            WindowLayout.extendFrameByCutout(safe, displayFrame, inOutFrame, sTmpRect);
        }

        if (ifp.getMinimalInsetsSizeInDisplayCutoutSafe() != null) {
            calculateInsetsFrame(sTmpRect2, ifp.getMinimalInsetsSizeInDisplayCutoutSafe());
            WindowLayout.extendFrameByCutout(safe, displayFrame, sTmpRect2, sTmpRect);
            if (sTmpRect2.contains(inOutFrame)) {
                inOutFrame.set(sTmpRect2);
            }
        }
        return ifp.getFlags();
    };
}

1.4.2 calculateInsetsFrame
计算逻辑很简单,比如依照我们前面navBar的Inset,只有bottom不为0,则将inOutFrame的top设置成inOutFrame.bottom - insetsSize.bottom,其余的不变,所以可以看到我们前面app侧传Inset的时候四个值都只有一个值不为0.
举个具体的数字例子:我们这里inOutFrame是屏幕大小,比如是(0,0,1000,2000),然后navBar传的bottom是200,结果就是(0,1800,1000,2000),相当于就是屏幕最底部高为200的区域作为导航键。

private static void calculateInsetsFrame(Rect inOutFrame, Insets insetsSize) {
    if (insetsSize == null) {
        return;
    }
    if (insetsSize.left != 0) {
        inOutFrame.right = inOutFrame.left + insetsSize.left;
    } else if (insetsSize.top != 0) {
        inOutFrame.bottom = inOutFrame.top + insetsSize.top;
    } else if (insetsSize.right != 0) {
        inOutFrame.left = inOutFrame.right - insetsSize.right;
    } else if (insetsSize.bottom != 0) {
        inOutFrame.top = inOutFrame.bottom - insetsSize.bottom;
    } else {
        inOutFrame.setEmpty();
    }
}

1.5 InsetsSourceProvider.setWindowContainer
将窗口和InsetsSourceProvider相互关联。这样窗口也持有InsetsSourceProvider,后续当窗口大小变化时,可以通过InsetsSourceProvider来重新计算窗口Insets。

void setWindowContainer(@Nullable WindowContainer windowContainer,
        @Nullable TriFunction<DisplayFrames, WindowContainer, Rect, Integer> frameProvider,
        @Nullable SparseArray<TriFunction<DisplayFrames, WindowContainer, Rect, Integer>>
                overrideFrameProviders) {
    if (mWindowContainer != null) {
        if (mControllable) {
            mWindowContainer.setControllableInsetProvider(null);
        }
        mWindowContainer.cancelAnimation();
        mWindowContainer.getInsetsSourceProviders().remove(mSource.getId());
        mSeamlessRotating = false;
    }
    // 记录windowContainer,frameProvider(1.4.2返回的一个方法)
    mWindowContainer = windowContainer;
    mFrameProvider = frameProvider;
    mOverrideFrames.clear();
    mOverrideFrameProviders = overrideFrameProviders;
    if (windowContainer == null) {
        setServerVisible(false);
        mSource.setVisibleFrame(null);
        mSource.setInsetsRoundedCornerFrame(false);
        mSourceFrame.setEmpty();
    } else {
        // 这里的mSource就是InsetsSource,可以看出来InsetsSource和InsetsSourceProvider是一一对应的
        mWindowContainer.getInsetsSourceProviders().put(mSource.getId(), this);
        if (mControllable) {
            mWindowContainer.setControllableInsetProvider(this);
            if (mPendingControlTarget != null) {
                updateControlForTarget(mPendingControlTarget, true /* force */);
                mPendingControlTarget = null;
            }
        }
    }
}

Inset变化后通知应用流程

在这里插入图片描述

2.1 RootWindowContianer.performSurfacePlacementNoTrace
这个方法是WMS的一个核心方法,WMS就是通过这个方法来对所有窗口进行测量计算的,我们后面会专门来将这个方法,这里只关注通知Inset变化的一条链路。

void performSurfacePlacementNoTrace() {
    // ...
    // 里面会调用DisplayContent的applySurfaceChangesTransaction,我们直接看这个,详见2.2
    applySurfaceChangesTransaction();
    // ...
}

2.2 DisplayContent.applySurfaceChangesTransaction
里面有调用InsetsStateController.onPostLayout

void applySurfaceChangesTransaction() {
    // ...
    // 详见2.3
    mInsetsStateController.onPostLayout();
    // ...
}

2.3 InsetsStateController.onPostLayout
调用没有InsetsSourceProvider都调用onPostLayout重新计算Inset的区域。
同时会判断InsetsState是否有更新,是否需要用心的InsetsState进行计算。

void onPostLayout() {
    Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "ISC.onPostLayout");
    for (int i = mProviders.size() - 1; i >= 0; i--) {
        // 调用没有InsetsSourceProvider都调用onPostLayout布局计算,详见2.4。
        mProviders.valueAt(i).onPostLayout();
    }
    // 前面提到过InsetsStateController有两个InsetsState,这里判断如果两个不一样,说明Insets有更新,要通知所有窗口重新计算
    if (!mLastState.equals(mState)) {
        mLastState.set(mState, true /* copySources */);
        notifyInsetsChanged();
    }
    Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}

2.4 InsetsSourceProvider.onPostLayout
根据依附的窗口计算Insets位置区域等是否变化了,如果变化了则调用InsetsStateController.notifyControlChanged通知app Insets发生变化了

void onPostLayout() {
    if (mWindowContainer == null) {
        return;
    }
    // 根据依附窗口的可见性更新Insets可见性。
    WindowState windowState = mWindowContainer.asWindowState();
    boolean isServerVisible = windowState != null
            ? windowState.wouldBeVisibleIfPolicyIgnored() && windowState.isVisibleByPolicy()
            : mWindowContainer.isVisibleRequested();
    setServerVisible(isServerVisible);
    if (mControl != null) {
        boolean changed = false;
        final Point position = getWindowFrameSurfacePosition();
        // 依附的窗口位置变化了  
        if (mControl.setSurfacePosition(position.x, position.y) && mControlTarget != null) {
            changed = true;
            if (windowState != null && windowState.getWindowFrames().didFrameSizeChange()
                    && windowState.mWinAnimator.getShown() && mWindowContainer.okToDisplay()) {
                windowState.applyWithNextDraw(mSetLeashPositionConsumer);
            } else {
                Transaction t = mWindowContainer.getSyncTransaction();
                if (windowState != null) {
                    final AsyncRotationController rotationController =
                            mDisplayContent.getAsyncRotationController();
                    if (rotationController != null) {
                        final Transaction drawT =
                                rotationController.getDrawTransaction(windowState.mToken);
                        if (drawT != null) {
                            t = drawT;
                        }
                    }
                }
                mSetLeashPositionConsumer.accept(t);
            }
        }
        if (mServerVisible && !mLastSourceFrame.equals(mSource.getFrame())) {
            final Insets insetsHint = mSource.calculateInsets(
                    mWindowContainer.getBounds(), true /* ignoreVisibility */);
            // 计算出来的Insets区域变化了
            if (!insetsHint.equals(mControl.getInsetsHint())) {
                changed = true;
                mControl.setInsetsHint(insetsHint);
                mInsetsHint = insetsHint;
            }
            mLastSourceFrame.set(mSource.getFrame());
        }
        if (changed) {
            // 通知app Insets更新,详见2.5
            mStateController.notifyControlChanged(mControlTarget);
        }
    }
}

2.5 InsetsStateController.notifyControlChanged

void notifyControlChanged(InsetsControlTarget target) {
    mPendingControlChanged.add(target);
    // 详见2.6
    notifyPendingInsetsControlChanged();
}

2.6 notifyPendingInsetsControlChanged
这里通知到Target一般是处在前台的窗口,所以其实每次更新只会直接通知到前台相关的几个窗口
其余的后台app是在需要切换到前台做performTraversals时候,通过relayout到WMS侧,WMS做窗口测量更新之后会把Insets返回给app侧。

private void notifyPendingInsetsControlChanged() {
    if (mPendingControlChanged.isEmpty()) {
        return;
    }
    mDisplayContent.mWmService.mAnimator.addAfterPrepareSurfacesRunnable(() -> {
        for (int i = mProviders.size() - 1; i >= 0; i--) {
            final InsetsSourceProvider provider = mProviders.valueAt(i);
            provider.onSurfaceTransactionApplied();
        }
        // 这里的Target一般是处在前台的窗口,所以其实每次更新只会直接通知到前台相关的几个窗口
        // 其余的后台app是在需要切换到前台做performTraversals时候,通过relayout到WMS侧,WMS做窗口测量更新之后会把Insets返回给app侧。
        final ArraySet<InsetsControlTarget> newControlTargets = new ArraySet<>();
        for (int i = mPendingControlChanged.size() - 1; i >= 0; i--) {
            final InsetsControlTarget controlTarget = mPendingControlChanged.valueAt(i);
            // 一般到app侧的是WindowState.notifyInsetsControlChanged,详见2.7
            controlTarget.notifyInsetsControlChanged();
            if (mControlTargetProvidersMap.containsKey(controlTarget)) {
                newControlTargets.add(controlTarget);
            }
        }
        mPendingControlChanged.clear();

        for (int i = newControlTargets.size() - 1; i >= 0; i--) {
            onInsetsModified(newControlTargets.valueAt(i));
        }
        newControlTargets.clear();
    });
}

2.7 WindowState.notifyInsetsControlChanged
通过WindowState里面mClient binder到app侧,mClient是前面添加窗口流程提到的ViewRootImpl里面的W类的client侧代理。

public void notifyInsetsControlChanged() {
    // 窗口还没有移除才通知
    if (mRemoved) {
        return;
    }
    final InsetsStateController stateController =
            getDisplayContent().getInsetsStateController();
    try {
        // 这里的mClient是我们前面添加窗口流程提到的ViewRootImpl里面的W类的client侧代理,这里会binder到app,详见2.8  
        mClient.insetsControlChanged(getCompatInsetsState(),
                stateController.getControlsForDispatch(this));
    } catch (RemoteException e) {
        Slog.w(TAG, "Failed to deliver inset control state change to w=" + this, e);
    }
}

2.8 ViewRootImpl$W.insetsControlChanged
这里的viewAncestor就是ViewRootImpl,这个方法就是调用了ViewRootImpl.dispatchInsetsControlChanged

public void insetsControlChanged(InsetsState insetsState,
        InsetsSourceControl[] activeControls) {
    final ViewRootImpl viewAncestor = mViewAncestor.get();
    if (viewAncestor != null) {
        // 详见2.9
        viewAncestor.dispatchInsetsControlChanged(insetsState, activeControls);
    }
}

2.9 ViewRootImpl.dispatchInsetsControlChanged
如果不是其他进程call进来,要对activeControls参数拷贝一份,防止修改原数据。
如果兼容模式对窗口有一些缩放,这里需要对Insets也做矫正。
发送消息MSG_INSETS_CONTROL_CHANGED

private void dispatchInsetsControlChanged(InsetsState insetsState,
        InsetsSourceControl[] activeControls) {
    // ...如果不是其他进程call进来,要对activeControls参数拷贝一份,防止修改原数据

    // 这个mTranslator前面添加窗口的流程中有提过,如果兼容模式对窗口又一些缩放等就会通过mTranslator实现,Insets也需要做同样的变化
    if (mTranslator != null) {
        mTranslator.translateInsetsStateInScreenToAppWindow(insetsState);
        mTranslator.translateSourceControlsInScreenToAppWindow(activeControls);
    }
    // ...
    SomeArgs args = SomeArgs.obtain();
    args.arg1 = insetsState;
    args.arg2 = activeControls;
    详见2.10
    mHandler.obtainMessage(MSG_INSETS_CONTROL_CHANGED, args).sendToTarget();
}

2.10 handleMessageImpl
调用InsetsController.onStateChanged更新端侧的Insets记录。
app侧对Insets管理结构和WMS侧差不多,ViewRootImpl里面持有一个InsetsController,里面包含两个InsetsState,一个是当前本地的InsetsState,另一个是从WMS获取更新的InsetsState,InsetsState和WMS侧的InsetsState是同一个类。
这里后续就是更新Insets,以及处理Insets变化引起的布局变化,启动变化动画等。我们主要是介绍Insets等框架以及通路,后面就不看了。

private void handleMessageImpl(Message msg) {
    // ...
    case MSG_INSETS_CONTROL_CHANGED: {
        SomeArgs args = (SomeArgs) msg.obj;
        // 调用onStateChanged,其他场景获取到Insets更新也会调用这个onStateChanged。
        mInsetsController.onStateChanged((InsetsState) args.arg1);
        InsetsSourceControl[] controls = (InsetsSourceControl[]) args.arg2;
        if (mAdded) {
            mInsetsController.onControlsChanged(controls);
        } else if (controls != null) {
            for (InsetsSourceControl control : controls) {
                if (control != null) {
                    control.release(SurfaceControl::release);
                }
            }
        }
        args.recycle();
        break;
    }
    // ...
}

小结

本节主要介绍了Insets的添加时机,以及添加后如何通知app,还有WMS和app侧对Insets管理对框架。
addWindow时候,通过layoutParams带过去Insets相关的信息,在addWindow时候会添加Insets。而当WMS出发窗口测量的方法,发现Insets有变化,就会通过binder通知当前前台的一些窗口。(这个测量方法是WMS的核心方法,我们下一节会对这个方法专门进行介绍)
WMS侧通过一个InsetsStateController对窗口进行管理,里面有两个InsetsState,相当于双缓存机制,每一个Insets的基础信息存在InsetsSource里,还有一些类用于辅助Insets和窗口的关联信息计算。app侧每个ViewRootImpl(即一个窗口)通过一个InsetsControoler进行管理,里面也是两个InsetsState,结构和WMS类似。
app侧还有一些View层的Insets计算,有比较多的细节比较繁琐,不是我们这个章节关注点,就不介绍了。
下一节会对WMS的测量窗口方法进行介绍,这个是WMS的核心方法,其中有一些计算逻辑会和Insets有关,所以我们这一节先介绍的Insets。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值