Flutter完整开发实战详解(二十一、 Flutter 画面渲染的全面解析)

OffsetLayer({ Offset offset = Offset.zero }) : _offset = offset;

@override
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
engineLayer = builder.pushOffset(
layerOffset.dx + offset.dx,
layerOffset.dy + offset.dy,
oldLayer: _engineLayer as ui.OffsetEngineLayer,
);
addChildrenToScene(builder);
builder.pop();
}
···
}

所以到这里 SceneBuilderLayer 通过 EngineLayeraddToScene 方法成功关联起来,而 window.render 提交的 Scene 又是通过 SceneBuilder 构建得到,所以如下图所示, LayerScene 就这样“苟且”到了一起

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对面前面的蓝色小方块代码,如下代码所示,这里修改为使用 Layer 的方式实现,可以看到这样的实现更接近 Flutter Framework 的实现:通过 rootLayer 一级一级 append 构建出Layer 树,而 rootLayer 调用 addToScene 方法后,因为会执行 addChildrenToScene 方法,从而往下执行 child LayeraddToScene

import ‘dart:ui’ as ui;

void main() {
ui.window.onBeginFrame = beginFrame;

ui.window.scheduleFrame();
}

void beginFrame(Duration timeStamp) {
final double devicePixelRatio = ui.window.devicePixelRatio;

///创建一个画板
final ui.PictureRecorder recorder = ui.PictureRecorder();

///基于画板创建一个 Canvas
final ui.Canvas canvas = ui.Canvas(recorder);
canvas.scale(devicePixelRatio, devicePixelRatio);

var centerX = ui.window.physicalSize.width / 2.0;
var centerY = ui.window.physicalSize.height / 2.0;

///画一个 100 的剧中蓝色
canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: 100, height: 100),
new Paint()…color = Colors.blue);

final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();

OffsetLayer rootLayer = new OffsetLayer();

OffsetLayer offsetLayer = new OffsetLayer(offset: Offset(centerX, centerY));
rootLayer.append(offsetLayer);

PictureLayer pictureLayer = new PictureLayer(Rect.zero);
pictureLayer.picture = recorder.endRecording();
offsetLayer.append(pictureLayer);

rootLayer.addToScene(sceneBuilder);

ui.window.render(sceneBuilder.build());
}

四、Layer 的品种

这里额外介绍下 Flutter 中常见的 Layer,如下图所示,一般 Flutter 中 Layer 可以分为 ContainerLayer 和非 ContainerLayer

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

ContainerLayer 是可以具备子节点,也就是带有 append 方法,大致可以分为:

  • 位移类(OffsetLayer/TransformLayer);
  • 透明类(OpacityLayer
  • 裁剪类(ClipRectLayer/ClipRRectLayer/ClipPathLayer);
  • 阴影类 (PhysicalModelLayer)

为什么这些 Layer 需要是 ContainerLayer因为这些 Layer 都是一些像素合成的操作,其本身是不具备“描绘”控件的能力,就如前面的蓝色小方块例子一样,如果要呈现画面一般需要和 PictureLayer 结合

比如 ClipRRect 控件的 RenderClipRRect 内部,在 pushClipRRect 时可以会创建 ClipRRectLayer ,而新创建的 ClipRRectLayer 会通过 appendLayer 方法触发 append 操作添加为父 Layer 的子节点。

而非 ContainerLayer 一般不具备子节点,比如:

  • PictureLayer 是用于绘制画面,Flutter 上的控件基本是绘制在这上面;
  • TextureLayer 是用于外界纹理,比如视频播放或者摄像头数据;
  • PlatformViewLayer 是用于 iOS 上 PlatformView 相关嵌入纹理的使用;

举个例子,控件绘制时的 Canvas 来源于 PaintingContext , 而如下代码所示 PaintingContext 通过 _repaintCompositedChild 执行绘制后得到的 Picture 最后就是提交给所在的 PictureLayer.picture

void stopRecordingIfNeeded() {
if (!_isRecording)
return;
_currentLayer.picture = _recorder.endRecording();
_currentLayer = null;
_recorder = null;
_canvas = null;
}

五、Layer 的内外兼修

了解完 Layer 是如何提交绘制后,接下来介绍的就是 Layer 的刷新和复用。

我们知道当 RenderObjectisRepaintBoundaryture 时,Flutter Framework 就会自动创建一个 OffsetLayer 来“承载”这片区域,而 Layer 内部的画面更新一般不会影响到其他 Layer

Layer 是如何更新?这就涉及了 Layer 内部的 markNeedsAddToSceneupdateSubtreeNeedsAddToScene 这两个方法。

如下代码所示,markNeedsAddToScene 方法其实就是把 Layer 内的 _needsAddToScene 标记为 true ; 而 updateSubtreeNeedsAddToScene 方法就是遍历所有 child Layer,通过递归调用 updateSubtreeNeedsAddToScene() 判断是否有 child 需要 _needsAddToScene ,如果是那就把自己也标记为 true

@protected
@visibleForTesting
void markNeedsAddToScene() {
// Already marked. Short-circuit.
if (_needsAddToScene) {
return;
}

_needsAddToScene = true;
}

@override
void updateSubtreeNeedsAddToScene() {
super.updateSubtreeNeedsAddToScene();
Layer child = firstChild;
while (child != null) {
child.updateSubtreeNeedsAddToScene();
_needsAddToScene = _needsAddToScene || child._needsAddToScene;
child = child.nextSibling;
}
}

是不是和 setState 调用 markNeedsBuild 把自己标志为 _dirty 很像?_needsAddToScene 等于 true 时,对应 LayeraddToScene 才会被调用;而当 Layer_needsAddToScenefalse_engineLayer 不为空时就触发 Layer 的复用

void _addToSceneWithRetainedRendering(ui.SceneBuilder builder) {

if (!_needsAddToScene && _engineLayer != null) {
builder.addRetained(_engineLayer);
return;
}
addToScene(builder);

_needsAddToScene = false;
}

是的,当一个 Layer_needsAddToScenefalse 时 表明了自己不需要更新,那这个 LayerEngineLayer 又存在,那 就可以被复用。举个例子:当一个新的页面打开时,底部的页面并没有发生变化时,它只是参与画面的合成,所以对于底部页面来说它 “Layer” 是可以直接被复用参与绘制。

markNeedsAddToScene 在什么时候会被调用?

如下图所示,当 Layer 子的参数,比如: PictureLayerpictureOffsetLayeroffset 发生变化时,Layer 就会主动调用 markNeedsAddToScene 标记自己为“脏”区域。另外当 LayerengineLayer 发生变化时,就会尝试触发父节点的 Layer 调用 markNeedsAddToScene ,这样父节点也会对应产生变化。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

@protected
set engineLayer(ui.EngineLayer value) {
_engineLayer = value;
if (!alwaysNeedsAddToScene) {
if (parent != null && !parent.alwaysNeedsAddToScene) {
parent.markNeedsAddToScene();
}
}
}

updateSubtreeNeedsAddToScene 是在 buildScene 的时候触发,在 addToScene 之前调用 updateSubtreeNeedsAddToScene 再次判断 child 节点,从而确定是否需要发生改变。

ui.Scene buildScene(ui.SceneBuilder builder) {
List temporaryLayers;
assert(() {
if (debugCheckElevationsEnabled) {
temporaryLayers = _debugCheckElevations();
}
return true;
}());
updateSubtreeNeedsAddToScene();
addToScene(builder);

_needsAddToScene = false;
final ui.Scene scene = builder.build();

return scene;
}

六、Flutter Framework 的 Layer 构成

最后回归到 Flutter Framework ,在 Flutter Framework 中 _window.render 是在 RenderViewcompositeFrame 方法中被调用;而 RenderView 是在RendererBindinginitRenderView 被初始化;initRenderView 是在 initInstances 时被调用,也就是 runApp 的时候。

简单来说就是:runApp 的时候创建了 RenderView ,并且 RenderView 内部的 compositeFrame 就是通过 _window.render来提交 Layer 的绘制。

void compositeFrame() {
Timeline.startSync(‘Compositing’, arguments: timelineWhitelistArguments);
try {
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer.buildScene(builder);
if (automaticSystemUiAdjustment)
_updateSystemChrome();
_window.render(scene);
scene.dispose();
assert(() {
if (debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled)
debugCurrentRepaintColor = debugCurrentRepaintColor.withHue((debugCurrentRepaintColor.hue + 2.0) % 360.0);
return true;
}());
} finally {
Timeline.finishSync();
}
}

所以 runApp 的时候 Flutter 创建了 RenderView,并且在 WindowdrawFrame 方法中调用了 renderView.compositeFrame(); 提交了绘制,而 RenderView 作为根节点,它携带的 rootLayerOffsetLayer 的子类 TransformLayer,属于是 Flutter 中 Layer 的根节点

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里举个例子,如下图所示是一个简单的不规范代码,运行后出现的结果是一个黑色空白页面,这里我们通过 debugDumpLayerTree 方法打印出 Layer 的机构。

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
new Future.delayed(Duration(seconds: 1), () {
debugDumpLayerTree();
});
return MaterialApp(
title: ‘GSY Flutter Demo’,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Container(),
//routes: routers,
);
}
}

打印出的结果如下 LOG 所示,正如前面所说 TransformLayer 作为 rooterLayer 它的 ownerRenderView,然后它有两个 child 节点: child1 OffsetLayer 和 child2 PictureLayer

默认情况下因为 Layer 的形成机制(isRepaintBoundaryture 自动创建一个 OffsetLayer)和 Canvas 绘制需要,至少会有一个 OffsetLayerPictureLayer

I/flutter (32494): TransformLayer#f8fa5
I/flutter (32494): │ owner: RenderView#2d51e
I/flutter (32494): │ creator: [root]
I/flutter (32494): │ offset: Offset(0.0, 0.0)
I/flutter (32494): │ transform:
I/flutter (32494): │ [0] 2.8,0.0,0.0,0.0
I/flutter (32494): │ [1] 0.0,2.8,0.0,0.0
I/flutter (32494): │ [2] 0.0,0.0,1.0,0.0
I/flutter (32494): │ [3] 0.0,0.0,0.0,1.0
I/flutter (32494): │
I/flutter (32494): ├─child 1: OffsetLayer#4503b
I/flutter (32494): │ │ creator: RepaintBoundary ← _FocusMarker ← Semantics ← FocusScope
I/flutter (32494): │ │ ← PageStorage ← Offstage ← _ModalScopeStatus ←
I/flutter (32494): │ │ _ModalScope-[LabeledGlobalKey<_ModalScopeState>#e1be1]
I/flutter (32494): │ │ ← _OverlayEntry-[LabeledGlobalKey<_OverlayEntryState>#95107] ←
I/flutter (32494): │ │ Stack ← _Theatre ←
I/flutter (32494): │ │ Overlay-[LabeledGlobalKey#ceb36] ← ⋯
I/flutter (32494): │ │ offset: Offset(0.0, 0.0)
I/flutter (32494): │ │
I/flutter (32494): │ └─child 1: OffsetLayer#e8309
I/flutter (32494): │ creator: RepaintBoundary-[GlobalKey#bbad8] ← IgnorePointer ←
I/flutter (32494): │ FadeTransition ← FractionalTranslation ← SlideTransition ←
I/flutter (32494): │ _FadeUpwardsPageTransition ← AnimatedBuilder ← RepaintBoundary
I/flutter (32494): │ ← _FocusMarker ← Semantics ← FocusScope ← PageStorage ← ⋯
I/flutter (32494): │ offset: Offset(0.0, 0.0)
I/flutter (32494): │
I/flutter (32494): └─child 2: PictureLayer#be4f1
I/flutter (32494): paint bounds: Rect.fromLTRB(0.0, 0.0, 1080.0, 2030.0)

根据上述 LOG 所示,首先看:

  • OffsetLayercreatorRepaintBoundary,而其来源是 Overlay,我们知道 Flutter 中可以通过 Overlay 做全局悬浮控件,而 Overlay 就是在 MaterialAppNavigator 中创建,并且它是一个独立的Layer
  • OffsetLayer 的 child 是 PageStoragePageStorage 是通过 Route 产生的,也即是默认的路由第一个页面。

所以现在知道为什么 Overlay 可以在 MaterialApp 的所有路由页面下全局悬浮显示了吧。

如下代码所示,再原本代码的基础上增加 Scaffold 后继续执行 debugDumpLayerTree

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
new Future.delayed(Duration(seconds: 1), () {
debugDumpLayerTree();
});
return MaterialApp(
title: ‘GSY Flutter Demo’,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
body: Container(),
),
//routes: routers,
);
}
}

可以看到这里多了一个 PhysicalModelLayerPictureLayerPhysicalModelLayer 是用于设置阴影等效果的,比如关闭 debugDisablePhysicalShapeLayersAppBar 的阴影会消失,而之后的 PictureLayer 也是用于绘制。

I/flutter (32494): TransformLayer#ac14b
I/flutter (32494): │ owner: RenderView#f5ecc
I/flutter (32494): │ creator: [root]
I/flutter (32494): │ offset: Offset(0.0, 0.0)
I/flutter (32494): │ transform:
I/flutter (32494): │ [0] 2.8,0.0,0.0,0.0
I/flutter (32494): │ [1] 0.0,2.8,0.0,0.0
I/flutter (32494): │ [2] 0.0,0.0,1.0,0.0
I/flutter (32494): │ [3] 0.0,0.0,0.0,1.0
I/flutter (32494): │
I/flutter (32494): ├─child 1: OffsetLayer#c0128
I/flutter (32494): │ │ creator: RepaintBoundary ← _FocusMarker ← Semantics ← FocusScope
I/flutter (32494): │ │ ← PageStorage ← Offstage ← _ModalScopeStatus ←
I/flutter (32494): │ │ _ModalScope-[LabeledGlobalKey<_ModalScopeState>#fe143]
I/flutter (32494): │ │ ← _OverlayEntry-[LabeledGlobalKey<_OverlayEntryState>#9cb60] ←
I/flutter (32494): │ │ Stack ← _Theatre ←
I/flutter (32494): │ │ Overlay-[LabeledGlobalKey#ee455] ← ⋯
I/flutter (32494): │ │ offset: Offset(0.0, 0.0)
I/flutter (32494): │ │
I/flutter (32494): │ └─child 1: OffsetLayer#fb2a6
I/flutter (32494): │ │ creator: RepaintBoundary-[GlobalKey#fd46b] ← IgnorePointer ←
I/flutter (32494): │ │ FadeTransition ← FractionalTranslation ← SlideTransition ←
I/flutter (32494): │ │ _FadeUpwardsPageTransition ← AnimatedBuilder ← RepaintBoundary
I/flutter (32494): │ │ ← _FocusMarker ← Semantics ← FocusScope ← PageStorage ← ⋯
I/flutter (32494): │ │ offset: Offset(0.0, 0.0)
I/flutter (32494): │ │
I/flutter (32494): │ └─child 1: PhysicalModelLayer#f1460
I/flutter (32494): │ │ creator: PhysicalModel ← AnimatedPhysicalModel ← Material ←
I/flutter (32494): │ │ PrimaryScrollController ← _ScaffoldScope ← Scaffold ← Semantics
I/flutter (32494): │ │ ← Builder ← RepaintBoundary-[GlobalKey#fd46b] ← IgnorePointer ←
I/flutter (32494): │ │ FadeTransition ← FractionalTranslation ← ⋯
I/flutter (32494): │ │ elevation: 0.0
I/flutter (32494): │ │ color: Color(0xfffafafa)
I/flutter (32494): │ │
I/flutter (32494): │ └─child 1: PictureLayer#f800f
I/flutter (32494): │ paint bounds: Rect.fromLTRB(0.0, 0.0, 392.7, 738.2)
I/flutter (32494): │
I/flutter (32494): └─child 2: PictureLayer#af14d
I/flutter (32494): paint bounds: Rect.fromLTRB(0.0, 0.0, 1080.0, 2030.0)
I/flutter (32494):

最后通过再使用 Navigator 跳到另外一个页面,再新页面打印 Layer 树,可以看到又可以多了个 PictureLayerAnnotatedRegionLayerTransformLayer : 其中多了的 AnnotatedRegionLayer 是用于处理新页面顶部状态栏的显示效果。

I/flutter (32494): TransformLayer#12e21
I/flutter (32494): │ owner: RenderView#aa5c7
I/flutter (32494): │ creator: [root]
I/flutter (32494): │ offset: Offset(0.0, 0.0)
I/flutter (32494): │ transform:
I/flutter (32494): │ [0] 2.8,0.0,0.0,0.0
I/flutter (32494): │ [1] 0.0,2.8,0.0,0.0
I/flutter (32494): │ [2] 0.0,0.0,1.0,0.0
I/flutter (32494): │ [3] 0.0,0.0,0.0,1.0
I/flutter (32494): │
I/flutter (32494): ├─child 1: OffsetLayer#fc176
I/flutter (32494): │ │ creator: RepaintBoundary ← _FocusMarker ← Semantics ← FocusScope
I/flutter (32494): │ │ ← PageStorage ← Offstage ← _ModalScopeStatus ←
I/flutter (32494): │ │ _ModalScope-[LabeledGlobalKey<_ModalScopeState>#43140]
I/flutter (32494): │ │ ← _OverlayEntry-[LabeledGlobalKey<_OverlayEntryState>#46f19] ←
I/flutter (32494): │ │ Stack ← _Theatre ←
I/flutter (32494): │ │ Overlay-[LabeledGlobalKey#af6f4] ← ⋯
I/flutter (32494): │ │ offset: Offset(0.0, 0.0)
I/flutter (32494): │ │
I/flutter (32494): │ └─child 1: OffsetLayer#b6e14
I/flutter (32494): │ │ creator: RepaintBoundary-[GlobalKey#0ce90] ← IgnorePointer ←
I/flutter (32494): │ │ FadeTransition ← FractionalTranslation ← SlideTransition ←
I/flutter (32494): │ │ _FadeUpwardsPageTransition ← AnimatedBuilder ← RepaintBoundary
I/flutter (32494): │ │ ← _FocusMarker ← Semantics ← FocusScope ← PageStorage ← ⋯
I/flutter (32494): │ │ offset: Offset(0.0, 0.0)
I/flutter (32494): │ │
I/flutter (32494): │ └─child 1: PhysicalModelLayer#4fdc6
I/flutter (32494): │ │ creator: PhysicalModel ← AnimatedPhysicalModel ← Material ←
I/flutter (32494): │ │ PrimaryScrollController ← _ScaffoldScope ← Scaffold ←
I/flutter (32494): │ │ ClipDemoPage ← Semantics ← Builder ←
I/flutter (32494): │ │ RepaintBoundary-[GlobalKey#0ce90] ← IgnorePointer ←
I/flutter (32494): │ │ FadeTransition ← ⋯
I/flutter (32494): │ │ elevation: 0.0
I/flutter (32494): │ │ color: Color(0xfffafafa)
I/flutter (32494): │ │
I/flutter (32494): │ ├─child 1: PictureLayer#6ee26
I/flutter (32494): │ │ paint bounds: Rect.fromLTRB(0.0, 0.0, 392.7, 738.2)
I/flutter (32494): │ │
I/flutter (32494): │ ├─child 2: AnnotatedRegionLayer#cbeaf
I/flutter (32494): │ │ │ value: {systemNavigationBarColor: 4278190080,
I/flutter (32494): │ │ │ systemNavigationBarDividerColor: null, statusBarColor: null,
I/flutter (32494): │ │ │ statusBarBrightness: Brightness.dark, statusBarIconBrightness:
I/flutter (32494): │ │ │ Brightness.light, systemNavigationBarIconBrightness:
I/flutter (32494): │ │ │ Brightness.light}
I/flutter (32494): │ │ │ size: Size(392.7, 83.6)
I/flutter (32494): │ │ │ offset: Offset(0.0, 0.0)
I/flutter (32494): │ │ │
I/flutter (32494): │ │ └─child 1: PhysicalModelLayer#edb15
I/flutter (32494): │ │ │ creator: PhysicalModel ← AnimatedPhysicalModel ← Material ←
I/flutter (32494): │ │ │ AnnotatedRegion ← Semantics ← AppBar ←
I/flutter (32494): │ │ │ FlexibleSpaceBarSettings ← ConstrainedBox ← MediaQuery ←
I/flutter (32494): │ │ │ LayoutId-[<_ScaffoldSlot.appBar>] ← CustomMultiChildLayout ←
I/flutter (32494): │ │ │ AnimatedBuilder ← ⋯
I/flutter (32494): │ │ │ elevation: 4.0
I/flutter (32494): │ │ │ color: MaterialColor(primary value: Color(0xff2196f3))
I/flutter (32494): │ │ │
I/flutter (32494): │ │ └─child 1: PictureLayer#418ce
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结:

各行各样都会淘汰一些能力差的,不仅仅是IT这个行业,所以,不要被程序猿是吃青春饭等等这类话题所吓倒,也不要觉得,找到一份工作,就享受安逸的生活,你在安逸的同时,别人正在奋力的向前跑,这样与别人的差距也就会越来越遥远,加油,希望,我们每一个人,成为更好的自己。

  • BAT大厂面试题、独家面试工具包,

  • 资料包括 数据结构、Kotlin、计算机网络、Framework源码、数据结构与算法、小程序、NDK、Flutter

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

rc=“https://img-blog.csdnimg.cn/img_convert/b49aa1febd128cde3ff8cda7fb5f0a28.jpeg” />

总结:

各行各样都会淘汰一些能力差的,不仅仅是IT这个行业,所以,不要被程序猿是吃青春饭等等这类话题所吓倒,也不要觉得,找到一份工作,就享受安逸的生活,你在安逸的同时,别人正在奋力的向前跑,这样与别人的差距也就会越来越遥远,加油,希望,我们每一个人,成为更好的自己。

  • BAT大厂面试题、独家面试工具包,

  • 资料包括 数据结构、Kotlin、计算机网络、Framework源码、数据结构与算法、小程序、NDK、Flutter
    [外链图片转存中…(img-RG8KckAP-1713316543972)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值