Unity优化总结(持续更新)

工欲善其事,必先利其器。优先利用性能分析工具快速找出性能瓶颈,从瓶颈入手分析性能问题产生原因,可以事半功倍。

尽量减少占用的内存(资源体积)和CPU(计算量),首先着重减少总量才能更好的进行后续细节的优化。总量降低后,性能依旧有问题,那么可以考虑时间空间转换的手段。

一般情况下,GPU比CPU富余,内存比CPU富余,磁盘比内存富余,分线程比主进程富余。所以一般都是GPU换CPU,内存换CPU,磁盘换内存,利用多线程分担主进程的压力。

比如利用GPU Instance可以减少CPU的压力。利用对象池缓存,可以省略加载资源、实例化、销毁实例、卸载资源的步骤,可以明显降低CPU的消耗。

利用Loading进度条按需加载资源,可以减少内存峰值,大量节约内存。利用分线程进行计算,可以分担主进程的压力。

 

根据自己实践优化总结,这个持续更新,遇到新的东西会记录起来


设置相关

1.垂直同步

关闭垂直同步ProjectSetting-> Quality  VSync Count选成Don't Sync

2.帧率设置

设置最高帧率Application.targetFrameRate

3.物理更新

可以设置Fixed timestep减少物理更新

4.分辨率:

适当降低分辨率

很多2k分辨率手机,我们可以动态设置为1080,尤其对那些屏幕分辨率高,cpu,gpu性能差的手机提升明显

5.碰撞层检测

ProjectSetting-> physics里面有个layer collision matrix,根据项目实际情况勾选可生效的层碰撞

6.增量gc

这个是在2019.4版本有,就在playersetting设置,这个gc就会很平缓,不会突然卡一下那种


纹理相关(3d)

1.纹理格式

效果可以的情况下,android使用etc,etc2,ios使用pvrtc

如果效果达很差也不能用rgba32,虽然可以使用rgb16,但是遇到渐变的时候会出问题,这时可以尝试astc这种格式(我项目在用,抛弃了低端机类似5s这些)

2.纹理尺寸:

在移动设备上的贴图最大要控制在1024和512大小,可少量使用2048大小的贴图,以1024、512大小贴图为主。

角色的纹理需要比较精细,可以把头发,衣服,皮肤,武器分离适合的尺寸纹理

可以使用这个工具查看纹理“场景检查工具

3.关闭read/write功能

一般没用到换装功能基本不会用到这个,而且内存会大一倍

4.小物件的纹理合并,并使用同一个材质球

5.Mipmap

Mipmap 会增加游戏包体的大小和占用一定量的内存,但在游戏中Mipmap的渲染可以减少显存带宽,降低渲染压力,随着相机的推远贴图会随之切换成低像素的体贴,从而节省资源开支

5.texture streaming

streaming流加载贴图mipmap,意思就是用到哪一级mipmap只加载这一级的,上面说到使用mipmap会增加内存,也有团队为了减少内存,从而关闭了mipmap。这就会导致另一个问题,美术的贴图非常精细,在UI中近距离观察效果很好,可是一旦摄像机拉远,就会出现“噪点”很难看。 所以mipmap一定要打开。所以unity2018.2中提供了streaming流加载贴图mipmap,意思就是用到哪一级mipmap只加载这一级的,其他级的mipmap不加载具体可以看下面链接mipmap优化

用法:需要动态加载mipmap纹理勾选texture streaming,根据需求动态设置内存大小就ok

6.设置最高质量mipmap

QualitySettings.masterTextureLimit

对于低端机来说可以降低采样率,对于那种ui和场景结合,然后场景模糊的可以设置,这样大大提交性能

//0表示正常尺寸 //1表示降低1/2//2表示降低1/4 //3表示降低1/8//4表示降低1/16

QualitySettings.masterTextureLimit = 4;

7.减少使用透明贴图

透明贴图对gpu消耗大,

具体原理可以看看移动gpu原理性能分析和建议都有,说得很详细,建议看看

https://zhuanlan.zhihu.com/p/112120206


角色场景模型mesh

1.减面

对场景模型减面优化是最常见的优化操作。主要是去掉对模型造型没有影响的面,用尽可能少的面数表达清楚模型的结构和造型。比如:物件非关节点及物件背面、内部不会看见的面删掉,这里提供一个"场景检查工具"

2.合并模型(小范围内才能使用)

合并同一小范围内的非交互类的静态小物件,同时合并小物件的贴图。这样可以减少DRAW CALL的数量。一组大小形状不同的石头,如果同一个物体,然后距离很远,然后合并就没什么意义,可能负优化

3.LOD

建筑和复杂的物件用LOD模型和远处剔除来减少同屏面数。地形的LOD系统也可以对地形的面数做很大的优化。

4.模型的重复利用

相同的多个物件在unity内复制使用,复制的多个物体在引擎计算上算一个物体。但也不可复制太多个,太多会对内存带来很大压力。相同的物件太多,建议把几个合并成一组做为一个Object,多做几组,再进行复制。参考②。

5.地形优化

如果是用unity自带的地形工具制作的地形,可以用T4M插件转化成T4M格式地形,设置一个顶点值转化后可以对地形优化很多。T4M也可以设置lod模型。

6.网格碰撞

尽可能不用mesh collider,要用的时候可以让美术简化一个mesh专做碰撞,而不是再原有基础上加meshcollider

7.关闭模型read/write

粒子系统通常需要动态地修改其粒子的顶点属性,所以用到粒子系统的mesh不能关闭read/write

场景中需要动态合并网格需要read/write

角色中需要换装合并网格需要read/write

8.分场景加载

例如:主界面场景很大可以切割多个场景,通过add方式加载场景,不用的时候卸载


场景相关

1.批处理

Unity提供了三种批次合并的方法,分别是Static Batching,GPU Instancing和Dynamic Batching。它们的原理分别如下:
Static Batching,将静态物体集合成一个大号vbo提交,但是只对要渲染的物体提交其IBO。这么做不是没有代价。比如说,四个物体要静态批次合并前三个物体每个顶点只需要位置,第一套uv坐标信息,法线信息,而第四个物体除了以上信息,还多出来切线信息,则这个VBO会在每个顶点都包括所有的四套信息,毫无疑问组合这个VBO是要对CPU和显存有额外开销的。要求每一次Static Batching使用同样的material,但是对mesh不要求相同。

Dynamic Batching将物体动态组装成一个个稍大的vbo+ibo提交。这个过程不要求使用同样的mesh,但是也一样要求同样的材质。但是,由于每一帧CPU都要将每个物体的顶点从模型坐标空间变换到组装后的模型的坐标空间,这样做会带来一定的计算压力。所以对于Unity引擎,一个批次的动态物体顶点数是有限制的。

GPU Instancing是只提交一个物体的mesh,但是将多个使用同种mesh和material的物体的差异化信息(包括位置,缩放,旋转,shader上面的参数等。shader参数不包括纹理)组合成一个PIA提交。在GPU侧,通过读取每个物体的PIA数据,对同一个mesh进行各种变换后绘制。这种方式相比static和dynamic节约显存,又相比dynamic节约CPU开销。但是相比这两种批次合并方案,会略微给GPU带来一定的计算压力。但这种压力通常可以忽略不计。限制是必须相同材质相同物体,但是不同物体的材质上的参数可以不同。

所以Unity默认策略是优先static,其次gpu instancing,最后dynamic。当然如果顶点数过于巨大(比如渲染它几千颗使用同种mesh的树),那么gpu instancing或许比static batching是一个更加合适的方案。

静态批处理和动态批处理

2.GPU Instancing

提高图形性能的另一个好办法是使用GPU Instancing。GPU Instancing的最大优势是可以减少内存使用和CPU开销。当使用GPU Instancing时,不需要打开批处理,GPU Instancing的目的是一个网格可以与一系列附加参数一起被推送到GPU。要利用GPU Instancing,您必须使用相同的材质,且可以传递额外的参数到着色器,如颜色,浮点数等。

适用于不是静态,没有进行批处理的对象

例如:场景里面有很多箱子,可以被打爆,这种情况可以使用

3.光源“Important”个数

建议1个,一般为方向光。“Important”个数应该越小越少。个数越多,drawcall越多。

4.Pixel Light数目

建议1-2个。

5.场景烘焙

效果可以情况下光照图尽可能小,场景不接受实时光照,实时光照只给主角

6.尽可能地使用prefab

2018版本开始有prefab嵌套的功能,做出prefab防止资源冗余问题,也可以降低内存带宽的负担

7.提高场景资源复用率

动态加载,场景里面所有东西都保存预设,把场景数据保存配置表,加载的时候都是读取配置动态加载预设,唯一好处就是可以实现不同场景公用预设

整个场景打包,把公用的东西都设置标签例如:fbx,材质球

8.场景加载

当前一个场景还未释放的时候,切换到新的场景。这时候由于两个内存叠加很容易达到内存峰值。解决方案是,在屏幕中间遮盖一个Loading场景。在旧的释放完,并且新的初始化结束后,隐藏Loading场景,使之有效的避开内存大量叠加超过峰值

流程:旧场景->空场景(loading场景)->新场景


粒子特效

1.屏幕上的最大粒子数

建议小于200个粒子。

2.每个粒子发射器发射的最大粒子数

建议不超过50个。

3.粒子大小

粒子的size应该尽可能地小。因为Unity的粒子系统的shader无论是alpha test还是alpha blending都是一笔不小的开销。同时,对于非常小的粒子,建议粒子纹理去掉alpha通道。

4.不要开启粒子的碰撞功能

5.粒子特效shader用mobile,特殊需求只能加shader

6.特效显示规则

一个特效里面分开很多部分,根据性能设置来显示部分特效“相关链接

7.贴图和shader选用

能使用Additive和黑色背景的特效图(不透明),取代alpha blend+透明贴图

8.关于粒子制作优化

如果单个粒子其实完全可以不用使用粒子脚本,如果只是简单旋转可以通过脚本进行实现

如果一个特效需要十几张不同的贴图,这就要看看是否真的合理

特效性能是个重灾区

这些很考验特效制作人的功力


动画优化

1.动画压缩格式

Optimal对性能最好,但随着设备的提升,Keyframe Reduction和Optimal的加载效率提升已不十分明显

Optimal压缩方式可能会降低动画的视觉质量,因此,是否最终选择Optimal压缩模式,还需根据最终视觉效果的接受程度来决定。

2.默认不开启Resample Curves选项

Unity官方给的建议是开启,但是开启会增大内存,所以在动画没有问题的情况下建议关闭该选项。

3.减少帧信息的精度

将帧信息精度减少为小数点后四位,网上大多数文章也是给出的是小数点后三位,但是优化到小数点后三位导致我们游戏一些角色的披风抖动时会穿帮,所以精度优化到了小数点后四位。  

4.优化未改变的Rotation、Position序列帧和剔除Scale序列帧

如果需要用到Scale变化就在骨骼节点上加关键字区分。Position、Rotation有很多骨骼节点其实并未发生变化,但是由于我们Animation.Compression这两个优化值设置的比较小,所以这里我们手动优化掉没有变化的Position、Rotation序列帧,只留头尾两帧,保证Position和Rotation初始值正确,这么处理一下会降不少内存

相关工具参考


音频优化

1.格式使用
音效、语音:ogg
背景音乐:mp3

2.导入器设置
ForceToMono:根据项目实际对音乐的效果要求而定,一般语音、音效勾选,长音乐看情况;
CompressionFormat:Vorbis
SampleRateSetting:语音、音效选OverrideSampleRate的22050KHz就足够了,保留初始音质可选PreserveSampleRate

LoadType: 对于内存压力大的项目,首选Streaming

相关链接


UI优化

1.drawcall优化

drawcall不是越少越好,如果ui很多,但是drawcall很少,那就是占用很多显存宽带,也会导致发热

一般都是处理应该合并但是没有合并的ui

优化方向,详细查看这个链接

  • 空的image和透明的image(有自带sprite调颜色透明)都改掉换成EmptyRaycast
  • 尽可能不用mask或者mask2d(有些必须用的没办法),这个会引起drawcall无法合并

  • Ui排版

  • 资源适当冗余可以减少drawcall

  • 图集整理

  • 一些复杂的ui适当加canvas

2.ui重建

什么引起uirebuild,可以理解为整个ui就是一个网格,凡是引起网格改变就要rebuild

例如ui移动位置,大小改变,文字改变这些都会引起rebuild

根据重建的频率来处理,尤其战斗中的小地图,飘字,血条,都是网格重建最多的

这些可以单独抽出一个canvas

3.RayCast

屏蔽没必要的raycast也是性能消耗

4.循环滚动列表

这个参考循环列表加个回调就可以满足大部分需求

参考案例

5.字体相关

字体尽量使用一种,其他特殊字体可以使用图片字

图片字生成工具

6.ui动画优化

用shader实现效果代替animation和dotween,因为dotween和aniamtion都是会引起网格重建,例如旋转,进度条

具体链接

7.尽可能少图集带进战斗里面,战斗中的图集尽量保持两张左右(战斗ui和头像)

8.大图脱离图集使用rawimage(背景图,尺寸偏大的图片)

9.图集尽可能在1024*1024范围内,尽可能使用九宫格

   正常按照1920*1080切图,放到图集里面,整到1024*1024一般问题不大

10.关于冗余图或者图集放的不规范可以使用工具来进行优化

图集工具

11.图集关掉mipmap和read/write功能,这两个都会增大内存

12.图集导入设置相关

 链接

13.运行时动态图集

运行的时候,把需要用到的sprite合成一张图集,这样可以减少宽带,降低drawcall

演示demo

14.ui显示隐藏

1、如果该UI界面开启的频率很低,可考虑直接通过Instantiate/Destroy来进行切换;

2、如果该UI界面使用较为频繁,可尝试通过Active/Deactive来代替Instantiate/Destroy操作,从而降低UI切换时的性能开销;

3、如果该UI界面使用非常频繁,则可尝试直接改变UI界面位置的方式来移进/移出相机视域体,从而来极大提升UI界面的切换效率。

15.ui粒子

使用ui粒子避免层级问题导致不同ui得穿插

ui粒子链接


序列化优化

1.使用protobuf来处理配置表

相对xml,json,lua来说序列化速度快,可以看看这个工具excel转protobuf工具

2.使用性能更好的Json

NewtonJson库的GC量以及耗时最低


效果优化

1.屏幕效果,后处理优化 链接

2.阴影优化,镜面反射优化 链接

3.下雨天效果优化 链接

4.特效扭曲效果  链接

5.深度图优化(景深之类的) 链接


材质球和shader优化

1.pass

减少不必要的pass,单个shader最好不要超过3个

2.减少standard shader

这个shader的变体是个噩梦,占内存,可以考虑根据美术需求写surface shader

如果要使用,只能官网下载对应版本standard,修改一下shader,要关掉没用的shader_feature,比如:_PARALLAXMAP、SHADOWS_SOFT、DIRLIGHTMAP_COMBINED DIRLIGHTMAP_SEPARATE、_DETAIL_MULX2、_ALPHAPREMULTIPLY_ON;另外要去掉多余的pass

3.surface shader

注意关掉不用的功能,比如:noshadow、noambient、novertexlights、nolightmap、nodynlightmap、nodirlightmap、nofog、nometa、noforwardadd等

4.shader变量类型选择

用fixed、half代替float,建立shader统一类型(fixed效率是float的4倍,half是float的2倍)

5.添加宏开关

shader_feature、multi_compile,并将宏开关

如果某些效果真机丢失,其实就是变体丢失,建议使用multi_compile,这个会把变体全部打进去

性能设置的时候可以根据配置开启

6.材质设置属性优化

使用属性块代替直接修改属性 相关链接

7.材质球设置参数会导致材质球无法合并渲染

例如:有个物件爆炸,加入爆炸面片都是单独使用单个相同材质球,这时候如果单个面片执行材质球参数变化会导致全部实例化一个新材质球,导致材质球无法合并

解决方法:实例化一个材质球赋值给所有面片,材质球参数变化只处理实例化的材质球,这样子不会引起材质球不合并问题

8.粒子shader多个pass无法合并

粒子特效上多个pass尽量少用


内存泄漏

资源内存泄漏都是可以找到源头,堆内存泄漏是比较难查,下面是堆内存泄漏的经验

堆内存泄漏


代码优化

1.在Update里面一直new

例子:每次update都new list(keys)来遍历定时器和移除定时器

因为foreach下不能移除字典数据,所以用这种new keys

//错误写法
public class ErrorInvoke
{
    Dictionary<string, InvokeData> dic = new Dictionary<string, InvokeData>();

    void Update()
    {
        List<string> list = new List<string>(dic.Keys);
        for(int i=0;i<list.Count;i++)
        {
            //满足情况就移除字典
        }
    }
}

解决方案:用列表来存储相关定时器,update只遍历列表,字典存储列表的引用就好


//修改之后写法
public class FixInvoke
{
    Dictionary<string, object> dic = new Dictionary<string, object>();
    List<InvokeData> list = new List<InvokeData>();

    void Update()
    {
        for(int i=list.Count;i-->0;)
        {
            //满足条件移除列表移除字典
        }
    }
}

2.太多装箱和拆箱

这个东西不用说太多就是目标类型->object装箱,object->目标类型拆箱

这个用的最多可能就是事件系统,我发送事件里面可以有多个参数,这些参数类型不定,然后到最后消息回调里面进行类型转换,频繁的装箱拆箱会引起内存问题,可以使用避免装箱拆箱的事件系统

我同事写的事件事件系统全部用parma object[],因为战斗和ui之间的通信通过事件来,如果一直update来发送事件,装箱拆箱很频繁,我是不建议这么写事件系统

可以参考这个事件系统https://blog.csdn.net/SnoopyNa2Co3/article/details/84971510

3.遍历列表移除

很多情况我们遍历列表来更新数据,如果数据已经没用的情况要从list移除

很多情况的写法是这样的,把移除的存起来,遍历完再移除

如果数据没有先后顺序问题的情况可以下面优化

存起来移除的代码

public class Temp1
{
    List<datat> list = new List<datat>();

    List<datat> removeList = new List<datat>();

    void Update()
    {
        datat temp;
        for (int i=0;i<list.Count;i++)
        {
            temp = list[i];
            if(temp.Remove)
            {
                removeList.Add(temp);
            }
        }
        if(removeList.Count > 0)
        {
            for(int i=0;i<removeList.Count;i++)
            {
                list.Remove(removeList[i]);
            }
            removeList.Clear();
        }
    }
}

下面是优化过的代码

public class Temp2
{
    List<datat> list = new List<datat>();

    void Update()
    {
        datat temp;
        for (int i = list.Count; i-->0 ;)
        {
            temp = list[i];
            if (temp.Remove)
            {
                list.RemoveAt(i);
            }
        }
    }
}

4.关于反射的使用

反射有什么好处,其实就是少写一些代码,方便一点

就例如:后端返回的协议号和对应的proto,来进行序列化数据

1.游戏初始化的时候进行绑定

2.就是用工具进行绑定代码的生成

反射着东西能不用尽量不用

5.频繁GetComponent

一般来说初始化的时候才会GetComponent,如果在update里面出现GetComponent或者频繁,这代码肯定有问题

尤其UI上面出现这种基本都是要改。拒绝没必要的性能消耗

6.统一update和lateupdate

就是游戏里面只有挂MonoBehaviour脚本,里面的update和lateupdate驱动整个游戏

update里面遍历100次比100个MonoBehaviour脚本 update效率更高

7.尽量减少MonoBehaviour使用

正常游戏逻辑都能代替MonoBehaviour,除非一些用到一些特殊接口,例如onaniamtorik之类的

8.对于不太重要的列表数据,需要update处理,可以用分帧处理

9.关于回调delegate,使用完立刻设null,防止某些写法导致内存泄漏

10.Unity tag对比

if(other.tag == a.tag)改为other.CompareTag(a.tag).因为other.tag为产生180B的GC Allow.

11.对象缓存

我们可以把一些必要的对象缓存起来,在Unity中,类似于GameObject.Find , GetComponent,transform,camera.Main这类的函数,会产生较大的消耗

12.对象池(一般项目都会有)

13.Struct 与 Class 选择

Struct 在栈中不产生 GC,class 在堆中,会产生 GC。对 Struct 的结点修改时,修改完以后记得重新赋值。因为 Struct 赋值是 copy而不是引用,修改完以后,以前的不生效。

 堆栈的空间有限,对于大量的逻辑的对象,创建类要比创建结构好一些。

结构表示轻量对象,并且结构的成本较低,适合处理大量短暂的对象。

在表现抽象和多级别的对象层次时,类是最好的选择。

大多数情况下该类型只是一些数据时,结构是最佳的选择。

14.string的优化

stringbuilder用不习惯,推荐使用zstring零gc,写法简单

传送口https://github.com/871041532/zstring

15.注意log的消耗

debug打印cpu消耗很高,注意加宏或者自己封装一下打印模块

16.应尽量为类或函数声明为sealed

IL2CPP就sealed的类或函数会有优化,变虚函数调用为直接函数调用。详见《IL2CPP OPTIMIZATIONS: DEVIRTUALIZATION》

17.减少Dictionary的冗余访问

我们常习惯编写这样的代码:

if(myDictionary.Contains(oneKey))
{
    MyValue myValue = myDictionary[oneKey];
   // ...
}

但其可减少冗余的哈希次数,优化为:

MyValue myValue;
if(myDictionary.TryGetValue(oneKey, out myValue))
{
    // ...
}

18.应减少UnityEngine.Object的null比较

因为Unity overwrite掉了Object.Equals(),《CUSTOM == OPERATOR, SHOULD WE KEEP IT?》也说过unityEngineObject==null事实上和GetComponent()的消耗类似,都涉及到Engine层面的机制调用,所以UnityEngine.Object的null比较,都会有少许的性能消耗

19.关于实现思路问题

代码实现千千万万种,必须多想想,就怕头脑一热就写代码,需要冷静分析思考

下面给个真实存在的反例,有些代码不看不知道,一看吓一跳

主要功能用管理器管理所有角色部位阴影

看代码就知道又什么问题

1.多了没必要的字符串Contains判断

2.直接update来执行unityaction(用于不同角色阴影位置更新,功能可能不一样)(unityaction性能一般)

改法

管理器只管理阴影面片取出和回收,每个角色都有一个阴影控制器,自己管理自己的东西(可以重载功能)

这样子代码清晰,而且管理方便,这有点类似ecs思想,不过没有狠彻底

    public void RemoveShadow(string roleName)
    {
        List<string> toDelete = new List<string>();
        foreach(KeyValuePair<string,GameObject> pair in shadows)
        {
            if(pair.Key.Contains(roleName))
            {
                if (null != pair.Value)
                {
                    pair.Value.SetActive2(false);
                    notUseShadows.Add(pair.Value);
                }
                toDelete.Add(pair.Key);
            }
        }
        for(int i = 0; i < toDelete.Count; i++)
        {
            shadows.Remove(toDelete[i]);
        }

        updateActions.Remove(roleName);
    }

    public void Update()
    {
        foreach(UnityAction action in updateActions.Values)
        {
            action();
        }
    }

20.使用多线程,线程以及unity交互

可以减少不是非常必要的逻辑在主线程运行

关于线程和unity交互可以看这个链接https://blog.csdn.net/SnoopyNa2Co3/article/details/108546696

21.关于委托Delegate, Action,UnityAction

使用Delegate, Action性能比unityaction快4-5倍,除了UGUI里的EventSystem都是基于unityaction实现的,使用之外,其他地方没有使用的必要

22.关于Input.touches

每次在对其调用时,都会new一个数组touches,从而造成一定的堆内存分配

Allocates temporary variables分配临时变量

避免Input.touches的频繁使用以防造成堆内存的额外占用

23.关于Linq相关函数的调用

Linq在执行过程中会产生一些临时变量,而且会用到委托(lambda 表达式)。如果使用委托作为条件的判定方法,时间开销就会很高,并且会造成一定的堆内存分配。所以在一般的Unity游戏项目开发中不推荐使用Linq相关的函数

24.使用更高效的异步/等待UniTask

Unity中使用协同程序通常是解决某些问题的好方法,但它也带来了一些缺陷:

  • 没有返回值,在处理需要返回值时候,非常容易陷入回调地狱

  • 协调程序使错误处理变得困难

所以在当前的项目中使用了UniTask来代替Unity coroutine使用。

UniTask解决了C#Task异步不能执行某些API的问题,将C# async/awake异步编程模型完美的带入了Unity中(需要Unity2018.3/C#7.0+),在项目中的实践,UniTask带来了简洁高效的体验。

https://github.com/Cysharp/UniTask


 

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值