10.2 使用程序性能分析器 - 《.NET 2.0面向对象编程揭秘》 - 免费试读 - book.csdn.net

【登录】 【免费注册】 首页新闻论坛群组Blog文档下载读书Tag网摘搜索开源FAQ第二书店博文视点程序员
频道:研发数据库中间件信息化视频.NETJava游戏移动服务:人才外包培训厂商专区BEA专区
iAnywhere
IBM专区
Intel专区
IJS专区
SAP专区
Telelogic
SUN专区
定制邮局
人邮出版
微软专区


精品连载 书友会 图书指数榜 收藏秀 特色书架 出版圈子 读书讨论群 读书博客 社区银行
图书品种:235680种
书友会 连载 Tag 用Google进行全文检索 用百度进行全文检索
热门搜索: ASP.NET Ajax Spring Hibernate Java

.NET 2.0面向对象编程揭秘 10.2 使用程序性能分析器
http://book.csdn.net/ 2007-6-19 15:33:00
图书导读 当前章节:10.2 使用程序性能分析器·8.3 软件体系结构设计方案·8.4 软件开发过程·10.1 程序为何运行得如此之慢[分享]未来开发趋势!
四大主题 Web开发2.0 企业开发2.0 系统开发2.0 语言工具2.0
www.sd2china.cn/defa...
IT 游戏 开发人才专业招聘网站
每日上万个IT简历更新,快捷有效的招聘求职
www.jobg.cn
贝尔实验室上课用的C语言教材
有代码实例 看技术图书,就上CSDN读书频道
book.csdn.net/bookfi...

10.2 使用程序性能分析器
Visual Studio 2005提供了一个方便易用的程序性能分析器,从“工具”菜单中选择“性能工具”子菜单,即可启动一个“性能向导”,通过此向导可完成对程序分析器的设置工作。
本节通过一个实例介绍如何使用Visual Studio 2005提供的程序性能分析器。
10.2.1 读懂示例程序代码
首先请读者运行一下配套光盘中本章的示例项目PerformanceTest,程序运行界面如图10-1所示。

图10-1 示例项目PerformanceTest
示例项目完成的功能是给文本加上行号。请准备一个拥有数百行的长文本作为测试文本,并事先复制到剪贴板上。
如图10-1所示,单击“粘贴”按钮将准备好的长文本粘贴到文本框中,用鼠标选择文本,或单击“全选”按钮选中全部文本,再单击“插入行号”按钮即可给选中的文本插入行号。
示例项目PerformanceText有两个版本,请先运行一下原始版本,再运行一下性能优化后的版本,就可以直观地感受到程序性能的提高。
程序中使用RichTextBox控件,插入行号时保持原文本的格式。
下面介绍一下RichTextBox控件的基础知识,了解这些知识有助于读者读懂示例代码。
RichTextBox控件接收一种RTF格式的文本,其文字可以拥有字体、字号、颜色等特性,类似于Word。RichTextBox控件的Text属性代表其内部的所有文本,RTF属性代表Text属性对应的RTF格式的数据。
RichTextBox内部将文本看成是行的集合,其Lines属性是一个字串数组,通过在Lines数组中指定一个索引,即可访问或修改特定行。
RichTextBox另一个重要的属性是CharIndex,它代表从文档开头到某个字符的索引,即文档中的某个字符“到文档开头有多远”。
如图10-2所示为CharIndex和Lines属性的示意图。

图10-2 RichTextBox的两个属性示意图
请注意区分Lines数组中的索引与CharIndex属性。在示例代码中将使用这两个属性来完成插入行号的工作。
现在分析一下原始版示例程序的代码。
//获取每行第一个字符的索引
//lineIndex为整个文档中每行的行号,从0开始
private int GetFirstCharIndexFromLine(int lineIndex)
{
//第一行第一个字符肯定是0
if (lineIndex == 0)
return 0;
//以下计算行号大于0的行的首字符索引
int charIndex = 0;
//累加前面的整行
for (int i = 0; i < lineIndex; i++)
//每行最后要多加一个字符的长度
charIndex += this.richTextBox1.Lines[i].Length + 1;
return charIndex;
}
以下代码完成插入行号的功能。
//插入行号
private void InsertNumber()
{
richTextBox1.WordWrap = false;
// 开始行,结束行
int beginLine =
richTextBox1.GetLineFromCharIndex(richTextBox1.SelectionStart);
int endLine =
richTextBox1.GetLineFromCharIndex(richTextBox1.SelectionStart +
richTextBox1.SelectionLength - 1);
//获取选中的总行数
int totalLines = endLine - beginLine + 1;
for (int i = 0; i < totalLines; i++)
{
//将插入点光标移到每行开头
richTextBox1.SelectionStart = GetFirstCharIndexFromLine(
i + beginLine);
richTextBox1.SelectionLength = 0;
richTextBox1.SelectionColor = Color.Black;
//插入行号
richTextBox1.SelectedText = (i + 1).ToString() + " ";
}
}
由于必须在插入行号后保持原有文字的格式,所以,不能直接修改Lines数组,因为里面只包含了文本信息。而是采用插入文本的方法,将光标先移到每行开头再插入。
10.2.2 对示例程序进行性能分析
从“工具”菜单中选择“性能工具”à“性能向导”命令,打开“性能向导”的第1页(见图10-3)。

图10-3 性能向导(1)
如图10-3所示,在下拉列表框中选择分析对象。Visual Studio 2005性能分析器可以分析.EXE、.DLL和ASP.NET页面,当前解决方案中的项目会在下拉列表中出现。
在本章示例中由于只有一个Windows应用程序项目,所以只可选择PerformanceTest。
选择好分析的目标之后,单击“下一步”按钮,进入“性能向导”的第2页(见图10-4)。

图10-4 性能向导(2)
性能向导的第2页要求指定分析方法,Visual Studio 2005支持两种分析方法,即采样与检测。
采样是定期中断程序的执行,记录并分析其函数调用情况。比较适合于找出整个程序中的性能瓶颈。
检测可提供更多信息,主要用于比对特定函数代码优化前后的程序性能。
一般来说,第一次运行性能分析器时建议选取“采样”分析方法,以确定好要分析的函数。以后则采用“检测”分析方法以开始优化代码。
选择好分析方法后,单击“下一步”按钮显示“性能向导”第3页(见图10-5)。

图10-5 性能向导(3)
运行性能向导结束后,一个性能资源管理器面板出现在屏幕上(见图10‑6)。
在“性能资源管理器”中单击“启动” 按钮即可启动程序,性能分析器自动在后台监控程序运行。
这时,可以按照一般的习惯操作程序,当程序退出时,性能分析器会自动整理收集到的信息并生成一个报告。
现在,请读者通过“性能资源管理器”启动程序,并向其中粘贴一个较长的文本(我的示例有140行),全选,然后单击“插入行号”按钮,耐心等待插入工作完成后,结束整个程序的运行。则性能分析器会自动生成一个报告文件,如图10-7所示。

图10-6 性能资源管理器 图10-7 自动生成性能分析报告
双击图10-7中所示的报告节点PerformanceTest061217.vsp,Visual Studio 2005会在主工作区中显示此报告的内容(见图10-8)。

图10-8 性能分析报告
如图10-8所示为采用“采样”方法分析的结果,可以看到其中列出了执行时间最长的采样函数列表。
大多数情况下我们不关心CLR的运行情况(如图10-8中所示的mscorwks.dll与mscoree.dll),也不关心系统提供给我们的“官方控件”的运行情况(如图10-8中所示的RichEd20.DLL),因为这些组件都不是我们所能控制的。我们只关心自己代码的运行情况。
为此,在图10-8中单击“函数”按钮,转入“函数”视图(见图10-9)。

图10-9 性能分析报告的“函数”视图
在图10-9中找到“PerformanceTest.exe”节点并展开,点击“Inclusive Percent”列进行降序排列。
“Inclusive Percent”数值反映了某函数及其调用的子函数占用取样样本的比例,数值越大表明其占用CPU时间越多,因而对程序性能影响越大。
在图10-9中可以清晰地看到,Main()函数高居榜首,这是肯定的,因为它是程序入口点,自然执行时间最长。
紧随其后的有3个函数:btnInsertLine_Click()、InsertNumber()和GetFirstCharIndex FromLine(),都占用了大量的CPU时间。
现在我们来看看这些函数的调用情况。
在图10-9中的“InsertNumber”节点上单击鼠标右键,从弹出菜单中选择“显示调用InsertNumber的函数”命令,将会自动转到“调用方/被调用方”视图(见图10-10)。

图10-10 “调用方/被调用方”视图
从图10-10中可以很清禁地看到,btnInsertLine_Click()函数调用InsertNumber()函数,占用的样本数均为89.648,这说明这两个函数“速度几乎是一样快的”。所以,InsertNumber()是影响btnInsertLine_Click()函数的主要因素。
再看下方显示的InsertNumber函数调用的子函数列表,GetFirstCharIndexFromLine()以86.766的数值高居“榜首”,这说明GetFirstCharIndexFromLine()函数是影响InsertNumber()函数运行速度的“罪魁祸首”。
根据上述分析,可以很有把握地推断,程序性能问题就出在InsertNumber()函数调用的GetFirstCharIndexFromLine()函数上。
在图10-10中的GetFirstCharIndexFromLine()节点上单击鼠标右键,从弹出菜单中选择“查看源”命令,则自动打开GetFirstCharIndexFromLine()函数的代码,如下所示。
01 private int GetFirstCharIndexFromLine(int lineIndex)
02 {
03 //……
04 //以下计算行号大于0的行的首字符索引
05 int charIndex = 0;
06 //累加前面的整行
07 for (int i = 0; i < lineIndex; i++)
08 //每行最后要多加一个字符的长度
09 charIndex += this.richTextBox1.Lines[i].Length + 1;
10 return charIndex;
11 }
在上面代码中可以看到第7句有一个循环。
再打开InsertNumber函数的代码,发现第8句也有一个循环语句。
01 //插入行号
02 private void InsertNumber()
03 {
04 //……
05 for (int i = 0; i < totalLines; i++)
06 {
07 //……
08 richTextBox1.SelectionStart = GetFirstCharIndexFromLine(i +
beginLine);
09 //……
10 }
11 }
这是一个典型的“大循环套小循环”,初步估计是由于循环的次数过多影响了程序运行性能。
为了获取更多的信息,将“性能资源管理器”中的分析方法由“采样”改为“检测”(见图10-11)。

图10-11 转换分析方法
之后,再次运行性能分析会话,查看GetFirstCharIndexFromLine()函数的执行时间(见图10-12)。

图10-12 获取函数执行时间(1)
为了能方便地查看特定函数的调用次数与执行时间,我们可以隐藏图10-12中的部分列。请在“性能分析报告”的表头上单击鼠标右键,在弹出的窗口中选择希望显示的列(见图10-13)。
如图10-13所示,“Number of Calls”列显示函数的调用次数,“Elapsed Inclusive Time”列显示函数(包括它所调用的子函数)所执行的时间。

图10-13 选择要查看的列
经过调整的性能分析报告如图10-14所示。

图10-14 获取函数执行时间(2)
可以看到,GetFirstCharIndexFromLine()被调用140次,执行了3866.757339毫秒。InsertNumber()函数调用1次,执行了4301.149911毫秒。“插入行号”按钮单击事件响应代码btnInsertLine_Click()函数执行4303.952901毫秒。很明显,时间都耗在执行GetFirstCharIndexFromLine()函数上了。
因此,我们找到了程序运行缓慢的原因,主要就是被多次调用的GetFirstCharIndex FromLine()函数性能太低,此函数就是整个程序的“性能瓶颈”。
10.2.3 优化代码
在找到程序的性能瓶颈之后,即可动手优化代码。
1.初次优化——使用Win32 API消除循环
首先想到的就是GetFirstCharIndexFromLine()函数能否运行得更快?查看源代码,发现其中有循环语句,能否消除?
经过上网查询各种资料,发现.NET Framework提供的RichTextBox控件其实是由以前的ActiveX控件RichEdit包装而来,于是想到了使用Win32 API函数SendMessage()向其发送特定的消息来直接获取每行的第一个字符索引。
这时,需要使用平台调用技术,写出一个新的函数GetFirstCharIndexFromLine2()取代老的GetFirstCharIndexFromLine()函数。
//通过调用Win32 API来获取每行第一个字符的CharIndex值
[System.Runtime.InteropServices.DllImport("user32.DLL", EntryPoint =
"SendMessageA", SetLastError = true)]
private static extern int SendMessage (System.IntPtr hwnd , UInt32 wMsg ,
int wParam , int lParam );
private const uint EM_LINEINDEX = 0xBB;
//第二种方法,速度快,直接调用Win32 API获取每行的首字符索引
private int GetFirstCharIndexFromLine2(int lineIndex)
{
return SendMessage(this.richTextBox1.Handle,EM_LINEINDEX,lineIndex,0);
}
修改InsertNumber()函数如下:
//插入行号
private void InsertNumber()
{
//……
for (int i = 0; i < totalLines; i++)
{
richTextBox1.SelectionStart = GetFirstCharIndexFromLine2(i+beginLine);
//……
}
}
现在开始检测代码优化后的程序性能。
从“性能资源管理器”中启动程序,执行与上一次相同的工作任务,向一个140行的文档中插入行号,会明显感到速度快了很多,其性能分析报告如图10-15所示。

图10-15 使用Win32 API后的程序性能分析报告
从图10-15中可以看到,GetFirstCharIndexFromLine2()函数现在执行了1.799031毫秒,而原先的GetFirstCharIndexFromLine()函数执行了3866.757339毫秒;相应地,InsertNumber()函数现在执行390.064911毫秒(原来为4301.149911毫秒),btnInsertLine_Click()函数现在执行392.692781毫秒(原来为4303.952901毫秒),性能的提升幅度是惊人的。
对应地,由于示例文档有140行,所以,GetFirstCharIndexFromLine2()函数与SendMessage()函数均执行了140次。调用次数还是多了些,能否进一步优化?
2.再次优化——引入局部变量以减少函数调用次数
由于可以访问RichTextBox的Lines数组,所以,可以只获取选中区域的第一行第一个字符的CharIndex值,以后只需加上本行字串的长度即可得到下一行第一个字符的CharIndex值。
修改后的代码如下:
//插入行号
private void InsertNumber()
{
richTextBox1.WordWrap = false;
// 开始行,结束行
//……
int begin =GetFirstCharIndexFromLine2(beginLine);
for (int i = 0; i < totalLines; i++)
{
//将插入点光标移到每行开头
richTextBox1.SelectionStart = begin;
richTextBox1.SelectionLength = 0;
richTextBox1.SelectionColor = Color.Black;
//插入行号
richTextBox1.SelectedText = (i + 1).ToString() + " ";
begin += richTextBox1.Lines[i+beginLine ].Length + 1;
}
}
再次运行,性能分析报告如图10-16所示。

图10-16 使用Lines数组减少函数调用次数后的性能分析报告
从图10-16中可以看到,GetFirstCharIndexFromLine2()函数与SendMessage()函数均执行了1次,调用次数大幅度减少。前者的执行时间为0.352875毫秒(原来为1.799031毫秒),快了5倍左右。
然而,很奇怪的是,btnInsertLine_Click()函数现在执行的时间是479.148527毫秒,而在引入局部变量begin之前,其执行时间为392.692781毫秒,以前的版本速度更快。而我们的程序速度主要就体现在btnInsertLine_Click()函数上,因为它是调用者,显然我们画蛇添足了,新的代码不如旧的好。
思索一下:
为何被调用的函数执行快了,调用次数少了,调用它的函数反而执行慢了?
对比前后所修改的地方,发现新的InsertNumber()函数就比原来的老函数多增加了一个局部变量begin,并多加了一句代码:
begin += richTextBox1.Lines[i+beginLine].Length + 1;
问题一定就在这里。因为修改前后两个代码只有这些不同。



提示:读者现在一定体会到“步步为营”的重要性了,在修改代码时,一次只改动一点,然后马上检测这些改动的效果如何,这样即使出错,也能很快地定位错误。
Visual Studio 2005性能分析工具还提供了更多的手段来分析程序性能问题。
在“性能资源管理器”的“性能对话”节点PerformanceTest上单击鼠标右键,从弹出菜单中选择“属性”命令(见图10-17)。

图10-17 设定性能会话的属性
在图10-17中,选中“收集.NET对象分配信息”复选框,然后再次对比InsertNumber()函数的两种代码(一种是直接调用140次GetFirstCharIndexFromLine2()函数的版本,另一种是引入一个局部变量begin后的版本),其结果如图10-18所示。

图10-18 未引入局部变量的内存分配情况
请注意未引入局部变量begin时,btnInsertLine_Click()函数占1604274字节的空间。再看看引入了局部变量后的内存占用情况(见图10-19)。

图10-19 引入局部变量后的内存分配情况
引入局部变量begin后,btnInsertLine_Click()函数占用13328198字节的空间,是原来的8倍多。
内存占用量的增大和函数执行速度的减慢,估计是受以下这句代码的影响。
begin += richTextBox1.Lines[i + beginLine].Length + 1;
对此现象的分析涉及到RichTextBox控件内部的实现代码,C#编译器生成的IL指令以及CLR深层技术内幕,由于资料的匮乏笔者不能对此现象进行解释,但读者至少可以知道,笔者“自作聪明”地引入一个局部变量begin以减少对GetFirstCharIndexFromLine2()函数的调用次数,这种做法并不能达到让程序运行得更快的预期目的。
3.再次尝试——使用RichTextBox自身方法取代Win32API
经过前面的试验,我们已经得到了比较理想的方案,这就是使用Win32 API来编写一个GetFirstCharIndexFromLine2()函数,并且不引入新的局部变量。
但仔细查看Visual Studio 2005文档,发现.NET Framework 2.0中的RichTextBox控件比1.1版增加了一个方法,名字就叫GetFirstCharIndexFromLine(),如果用RichTextBox控件自身的方法取代自己编写的GetFirstCharIndexFromLine2()函数,效果如何呢?
请读者自行进行此试验。
我的结果是:
调用RichTextBox.GetFirstCharIndexFromLine()方法的程序运行速度与调用函数GetFirstCharIndexFromLine2()(未引入局部变量begin)相比慢一些(但仅有10多毫秒的差异),内存占用则相同。
由此我推测,RichTextBox.GetFirstCharIndexFromLine()方法的内部实现代码也是调用Win32 API函数SendMessage()实现的。
推荐使用RichTextBox自身方法来取代Win32 API,一方面代码更少而运行速度并未慢多少,而且更重要的是,这将使我们编写的代码不再依赖于特定的Windows平台API函数,而是由.NET Framework标准控件RichTextBox帮我们封装好的“纯的”.NET代码,从而提高了我们代码的可移植性。
4.小结
根据这个示例项目,可以得出以下结论。
(1)需要提高程序运行速度时,应该首先想办法确定程序的性能瓶颈所在,这时可以通过性能分析工具的“采样”方法来找出占用CPU时钟周期最多的函数,从而为找到最影响程序性能的瓶颈提供线索,这样才能“打蛇打在七寸上,一击致命”。
(2)找到了影响程序性能的主要函数之后,即可通过阅读源码对代码进行优化,利用性能分析工具的“检测”方法,比对代码修改前后的程序运行性能,从而最终确定优化方案。
进行性能分析需注意的问题:
当程序中有许多项功能时,每次只分析一个功能。而且保证每次分析时使用的数据与所进行的操作都是一样的。
本章介绍的实例是一个单独的EXE程序。Visual Studio 2005还支持直接对DLL的性能分析,这时,需要指定一个外部的EXE文件,其方法是给DLL项目创建一个性能会话,然后点击DLL性能会话节点,从弹出菜单中选择“属性”命令(见图10-20)。

图10-20 分析独立的DLL文件
图10-20所示的PublicUIComponent为一个类库项目,通过指定一个外部的调用此DLL的EXE文件,Visual Studio 2005就可以对DLL进行性能分析。
如果需要进行操作系统内核级别的分析,则可以在进行性能分析时加入操作系统提供的计数器(见图10-21)。

图10-21 选择计数器
如图10-21所示为性能会话节点的属性页,由于笔者电脑的CPU为Pentium IV,所以可以使用专为Pentium IV提供的计数器。
总之,Visual Studio 2005新引入的程序性能分析器是一个很强大的工具,掌握这一工具,有助于开发出性能优异的应用程序。但前提还是得要求程序员自身对于技术要有很好的掌握,不然,使用再强大的工具也写不出好程序来。
扩充阅读
调用DLL中非托管函数的一般方法
首先,应该在C#语言源程序中声明外部方法,其基本形式是:
[DLLImport(“DLL文件”)]
修饰符 extern 返回变量类型 方法名称 (参数列表)
其中:
● DLL文件:指被调用的非托管函数所在的DLL文件名。
q 修饰符:访问修饰符,除了abstract以外,在声明方法时可以使用的修饰符,如public、private。
q 返回变量类型:在DLL文件中需调用方法的返回变量类型。
q 方法名称:在DLL文件中需调用方法的名字。
q 参数列表:列出在DLL文件中需调用方法的参数,可查询表10-1将C/C++的数据类型转换为.NET的数据类型。
表10-1 非托管函数中的数据类型与.NET的数据类型对照表
Wtypes.h中的非托管类型非托管C语言类型.NET Framework中对应的托管类名
HANDLEvoid*System.IntPtr
BYTEunsigned charSystem.Byte
SHORTshortSystem.Int16
WORDunsigned shortSystem.UInt16
INTintSystem.Int32
UINTunsigned intSystem.UInt32
LONGlongSystem.Int32
BOOLlongSystem.Int32
DWORDunsigned longSystem.UInt32
ULONGunsigned longSystem.UInt32
CHARcharSystem.Char
LPSTRchar*System.String或System.Text.StringBuilder
LPCSTRConst char*System.String或System.Text.StringBuilder
LPWSTRwchar_t*System.String或System.Text.StringBuilder
LPCWSTRConst wchar_t*System.String或System.Text.StringBuilder
FLOATFloatSystem.Single
DOUBLEDoubleSystem.Double

注意:
(1)需要在程序声明中使用System.Runtime.InteropServices命名空间。
(2)DllImport只能放置在方法声明上。
(3)DLL文件必须位于程序当前目录或系统定义的查询路径中(即:系统环境变量中Path所设置的路径)。
(4)返回的变量类型、方法名称、参数列表一定要与DLL文件中的原始定义相一致。
(5)若要使用其他函数名,则可以使用EntryPoint属性设置。例如:
[DllImport("user32.dll", EntryPoint="MessageBoxA")]
static extern int MsgBox(int hWnd, string msg, string caption, int type);
事实上,在.NET中调用非托管函数还有许多复杂的问题,更详细的技术细节请在Visual Studio 2005文档中查询“平台调用”。
上一页 首页 【查看所有评论(11)条】
最近评论


[热] renyanbinnet 发表评论:是一本好书,我读过,书上的写的的很深入,凭自己的理解和经验写出来的,绝对不是那些照着Msdn或者一些文档照抄袭书. 时间:2007-10-17 20:45:36 来自:125.33.123.*PK Zone支持反对[热] renyanbinnet 发表评论:是一本好书,我读过,书上的写的的很深入,凭自己的理解和经验写出来的,绝对不是那些照着Msdn或者一些文档照抄袭书. 时间:2007-10-17 20:45:36 来自:125.33.123.*PK Zone支持反对[热] CSDN 网友 发表评论:支持
[引用] 来自 218.201.144.* 的 CSDN 网友 发表于2007-10-08 19:24:53[PK Zone]

希望用VB.NET重写这本书 时间:2007-10-08 19:06:55 来自:218.201.144.*PK Zone支持反对[热] CSDN 网友 发表评论:支持
[引用] 来自 218.201.144.* 的 CSDN 网友 发表于2007-10-08 19:24:53[PK Zone]

希望用VB.NET重写这本书 时间:2007-10-08 19:06:55 来自:218.201.144.*PK Zone支持反对[热] CSDN 网友 发表评论:希望用VB.NET重写这本书 时间:2007-10-08 19:24:53 来自:218.201.144.*PK Zone支持反对热点评论VC 娣卞叆璇﹁В [评论19条]
余晟:精通正则表达式�?�?Tags= ,余晟:精通正则表达式第5讲, [评论17条]
电子商务和数字神经系统(白皮书) [评论14条]
SQL Server 2005鏁版嵁鎸栨帢涓庡晢涓氭櫤鑳藉畬鍏ㄨВ鍐虫柟妗? [评论10条]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值