11.提高应用程序性能

通常,Windows Presentation Foundation (WPF) 应用程序的性能是其最大的问题之一。 我们渲染的数据对象和 UI 包含的视觉层越多,渲染它们所花费的时间就越多,因此我们经常需要保持平衡 在使我们的应用程序具有视觉吸引力和使其性能更好之间。

这种情况可以通过在功能更强大的计算机上运行我们的 WPF 应用程序来改善。 这就解释了为什么这些应用程序在金融行业中最为普遍。 但是,并不是每个人都可以为此更新所有用户的计算机。

幸运的是,我们可以通过多种方式来提高 WPF 应用程序的性能,我们将在此处对其进行研究。 提高应用程序性能的艺术实际上归结为进行许多小的改进,所有这些加起来都会产生明显的差异。

在本章中,我们将探讨如何更好地利用计算机显卡的图形渲染能力并更有效地声明资源。 我们将研究如何通过选择使用更轻量级的 UI 控件、更有效的数据绑定模式以及采用其他技术(例如虚拟化)来提高应用程序的性能。

利用硬件渲染的力量

正如我们已经了解到的,WPF 可以输出的视觉效果虽然很漂亮,但可能会占用大量 CPU,我们在设计视图时经常需要牢记这一点。 但是,我们可以将密集的渲染过程卸载到主机的图形处理单元 (GPU) 上,而不是影响我们的设计。

虽然 WPF 默认使用其软件渲染管道,但它也能够利用硬件渲染管道。 只要主机 PC 安装了 DirectX 版本 7 或更高版本,此硬件管道就可以利用 Microsoft DirectX 的功能。 此外,如果安装的 DirectX 版本是版本 9 或更高版本,则会看到性能提升。

WPF 框架查看安装在运行它的计算机上的图形硬件,并将其归入三个类别之一,具体取决于其功能,例如视频 RAM、着色器和对多纹理的支持。 如果它不支持 DirectX 7 或更高版本,则它被归类为 Rendering Tier 0,根本不会用于硬件渲染。

但是,如果它确实支持 DirectX 版本 7 或更高版本,但低于版本 9,则它被归类为渲染层 1,并将用于部分硬件渲染。 但是,由于几乎所有新显卡都支持高于 9 的 DirectX 版本,因此它们都将归类为渲染层 2,并将用于完整的硬件渲染。

由于 UI 将在渲染期间冻结,因此应注意尽量减少渲染的可视层数。 因此,对于将在具有归类为渲染层 0 的图形硬件并使用软件渲染的计算机上运行的 WPF 应用程序,我们需要格外小心。

但是,如果我们的应用程序可能在较旧的计算机或具有较旧图形硬件的计算机上运行,我们可以使用渲染层检测到这一点,并在这些实例中运行更高效的代码。 我们可以使用 RenderCapability 类的静态 Tier 属性找出主机图形硬件的渲染层。

不幸的是,这个属性的类型不是某种有用的枚举,实际上它是一个整数,其中只有高位字表示层的值,可以是 0、1 或 2。我们 可以通过移动整数中的位以仅从最后两个字节读取值来实现它:

using System.Windows.Media;
...
int renderingTier = RenderCapability.Tier >> 16;

一旦我们知道了主机图形硬件的渲染层,我们就可以相应地编写代码。 例如,假设我们有一个处理器密集型视图,集合中的每个项目都有很多视觉效果。 我们可以将层值设置为一个属性并将其数据绑定到视图,我们可以根据主机的处理能力选择不同的数据模板来使用。 让我们首先创建缺少的枚举来检查这个示例:

namespace CompanyName.ApplicationName.DataModels.Enums
{
   
    public enum RenderingTier
    {
   
        Zero = 0,
        One = 1,
        Two = 2
    }
}

接下来,我们需要将 RenderingTier 类型的属性添加到第 3 章编写的自定义应用程序框架中的 StateManager 类中:

public RenderingTier RenderingTier {
    get; set; }

我们不需要将此属性的任何更改通知 INotifyPropertyChanged 接口,因为它只会在应用程序启动时设置一次。 让我们调整之前的示例:

public App()
{
   
    StateManager.Instance.RenderingTier =
        (RenderingTier)(RenderCapability.Tier >> 16);
}

在将移位整数值转换为我们的 RenderingTier 枚举并将其设置为 StateManager 类中的新 RenderingTier 属性之后,我们可以开始在我们的视图中使用它来确定我们可以使用的可视化级别:

<ListBox ItemsSource="{Binding Products}">
    <ListBox.Style>
        <Style TargetType="{x:Type ListBox}">
            <Setter Property="ItemTemplate"
                    Value="{StaticResource SimpleDataTemplate}" />
            <Style.Triggers>
                <DataTrigger Binding="{Binding
                                      StateManager.Instance.RenderingTier}" Value="One">
                    <Setter Property="ItemTemplate"
                            Value="{StaticResource MoreComplexDataTemplate}" />
                </DataTrigger>
                <DataTrigger Binding="{Binding
                                      StateManager.Instance.RenderingTier}" Value="Two">
                    <Setter Property="ItemTemplate"
                            Value="{StaticResource MostComplexDataTemplate}" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </ListBox.Style>
</ListBox>

在此示例中,我们有一个显示产品集合的 ListBox 控件。 这个想法是我们可以声明三个不同的数据模板来定义每个产品的外观。 我们有一个 SimpleDataTemplate 模板,它可能只提供基于文本的 输出,一个可以包含一些基本视觉效果的 MoreComplexDataTemplate 模板,以及一个可以包含多个视觉效果层的 MostComplexDataTemplate 模板。

在应用于列表框的样式中,我们将默认的 SimpleDataTemplate 模板设置为其 ItemTemplate 属性的值。 使用 StateManager 类的 RenderingTier 属性,我们随后声明了几个数据触发器,以根据主机的渲染层将 ItemTemplate 属性的值切换为更复杂的模板之一。

制作更高效的资源

当我们引用我们的资源时,我们可以使用 StaticResource 或 DynamicResource。 如果您还记得第 5 章,为作业使用正确的控件,StaticResource 只会查找资源的值一次,这与编译时查找相比。 每次请求时,DynamicResource 都会重复查找资源的值,无论它是否已更改,就像运行时查找一样。

出于这个原因,我们应该只在真正需要时才使用 DynamicResource,因为我们可以通过使用 StaticResource 类来获得更好的性能。 如果我们发现需要使用大量 DynamicResource 引用来访问我们的资源,那么我们可以将代码重构为数据绑定到 StateManager 类中的属性而不是资源,以提高性能。

另一种提高资源性能的简单方法是重用它们。 与其在 XAML 中使用它们的地方声明它们是内联的,不如在合适的资源部分声明它们并引用它们。

这样,每个资源只创建一次并共享。 为了进一步扩展这个想法,我们可以在 App.xaml 文件的应用程序资源中定义我们所有的共享资源,并在所有应用程序视图之间共享它们。

想象一下这样一种情况,其中一些画笔资源被声明为与 DataTemplate 元素中的 XAML 内联。 现在假设这个模板被设置为 ItemsControl 对象的 ItemTemplate,并且数据绑定到它的 ItemsSource 属性的集合包含一千个元素。

因此,应用程序将为在数据模板中本地声明的每个画笔创建一千个具有相同属性的画笔对象。 现在将此与另一种情况进行比较,我们在资源部分中仅声明每个必需的画笔一次并从模板中引用它。 可以清楚地看到这种方法的好处以及可以从计算机资源中节省的大量资金。

此外,这个想法也会影响我们视图的资源部分,尤其是当我们同时显示多个视图时。 如果我们声明一个视图来定义集合中的每个对象应该如何呈现,那么视图中声明的所有资源将为集合中的每个元素初始化一次。 在这种情况下,最好在应用程序级别声明它们。

冻结对象

在 WPF 中,可以将某些资源对象(例如动画、几何图形、画笔和钢笔)设为可冻结。 这提供了有助于提高 WPF 应用程序性能的特殊功能。 可冻结对象可以被冻结或解冻。 在解冻状态下,它们的行为与任何其他对象一样; 然而,当冻结时,它们变得不可变并且不能再被修改。

冻结对象的主要好处是它可以提高应用程序的性能,因为冻结对象在监视和发出更改通知时不再需要消耗资源。 另一个好处是,与未冻结的对象不同,冻结的对象也可以安全地跨线程共享。

许多与 UI 相关的对象扩展了 Freezable 类以提供此功能,并且大多数 Freezable 对象与图形子系统相关,因为呈现视觉效果是最需要改进性能的领域之一。

Brush、Geometry 和 Transform 等类包含非托管资源,系统必须监视它们的变化。 通过冻结这些对象并使它们不可变,系统能够释放其监控资源并在其他地方更好地利用它们。 此外,即使是冻结对象的内存占用也远小于未冻结对象。

因此,为了最大程度地提高性能,我们应该习惯于冻结所有 Resource 部分中的所有资源,只要我们不打算修改它们。 由于大多数资源通常保持不变,我们通常能够冻结其中的绝大多数,并通过这样做获得显着且显着的性能改进。

在第 8 章,创建具有视觉吸引力的用户界面中,我们学习了如何通过调用 Freeze 方法在代码中冻结 Freezable 对象。 现在让我们看看如何在 XAML 中冻结我们的资源。 首先,我们需要向表示选项命名空间添加一个 XAML 命名空间前缀,以访问其 Freeze 属性:

xmlns:PresentationOptions=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation/options
"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="PresentationOptions"

请注意,我们还包括另一个 XAML 命名空间前缀,以便能够访问 Ignorable 属性,并且我们将 PresentationOptions 前缀设置为其值。 这是因为 Freeze 属性主要只被 WPF XAML 处理器识别,并且为了保持与其他 XAML 读取器的兼容性,我们需要指定可以忽略该属性。

我们将在即将发布的“绘图结论”部分中找到一个完整示例,但现在,使用前面示例中的资源,让我们检查如何在 XAML 中冻结 Freezable 对象:

<DropShadowEffect x:Key="Shadow" BlurRadius="10" Direction="270"
ShadowDepth="7" Opacity="0.5" PresentationOptions:Freeze="True" />

一些 Freezable 对象,例如动画和几何对象,可以包含其他 Freezable 对象。 当 Freezable 对象被冻结时,它的子对象也被冻结。 但是,在某些情况下无法冻结 Freezable 对象。

如果由于动画、数据绑定或 DynamicResource 引用而具有任何可能更改值的属性,则会发生一种情况。 另一种情况发生在 Freezable 对象有任何无法冻结的子对象时。

例如,如果我们在自定义控件的代码中冻结资源类型对象,那么我们可以调用 Freezable 类的 CanFreeze 属性来检查每个 Freezable 对象是否可以在尝试冻结它们之前被冻结:

EllipseGeometry ellipseGeometry =
    new EllipseGeometry(new Rect(0, 0, 500, 250));
if (ellipseGeometry.CanFreeze) ellipseGeometry.Freeze();
Path.Data = ellipseGeometry;

一旦 Freezable 对象被冻结,就无法对其进行修改,并且尝试这样做会导致抛出 InvalidOperationException。 请注意,Freezable 对象无法解冻; 因此,为避免这种情况,我们可以在尝试修改对象之前检查 IsFrozen 属性的值。 如果它被冻结,我们可以使用它的 Clone 方法复制它并修改它:

if (ellipseGeometry.IsFrozen)
{
   
    EllipseGeometry ellipseGeometryClone = ellipseGeometry.Clone();
    ellipseGeometryClone.RadiusX = 400;
    ellipseGeometryClone.Freeze();
    Path.Data = ellipseGeometryClone;
}
else ellipseGeometry.RadiusX = 400;

如果一个 Freezable 对象被克隆,它可能具有的任何 Freezable 子对象也将被复制以启用修改。 当一个冻结的对象被动画化时,动画系统将以这种方式复制它,以便它可以修改它们。 但是,由于这会增加性能开销,因此如果您希望设置动画,建议不要冻结 Freezable 对象。

使用正确的性能控制

正如我们之前提到的,在使用 WPF 时,通常有几种不同的方法可以实现相同的功能或 UI 显示。 有些方法将提供比其他方法更好的性能。 例如,我们了解了一些面板如何进行更密集的布局工作,并且, 因此,比其他消耗更多的 CPU 周期和/或 RAM。

因此,这是我们可以调查以提高性能的一个领域。 如果我们不需要 Grid 面板的复杂布局和调整大小的能力,那么我们可以通过使用更高效的 StackPanel 或 Canvas 面板来获得性能改进。

另一个例子是,如果我们不需要在集合控件中进行选择,那么我们应该使用 ItemsControl 元素而不是 ListBox。 虽然交换一个控件本身并不会显着提高性能,但在将显示数千次的项目的 DataTemplate 中进行相同的交换将产生明显的差异。

正如我们在第 5 章,为作业使用正确的控件中所发现的,每次渲染 UI 元素时,布局系统必须完成两次传递,即测量传递和排列传递,统称为布局传递。 如果元素有子和/或孙子,他们也都需要完成布局过程。 这个过程是密集的,可以进行的通道越少,我们的视图渲染速度就越快。

如前所述,我们需要小心确保不会不必要地触发布局系统的额外通道,因为这会导致性能下降。 当在面板中添加或删除项目、对元素应用变换或调用 UIElement.UpdateLayout 方法(强制执行新的布局传递)时,可能会发生这种情况。

由于对 UI 元素的更改会使其子元素无效并强制执行新的布局传递,因此在代码中构建分层数据时需要特别小心。 如果我们先创建子元素,然后是它们的父对象,然后是这些对象的父对象,以此类推,由于现有的子项被迫执行多个布局传递,我们将招致巨大的性能损失。

为了解决这个问题,我们需要始终确保我们从自顶向下构建树,而不是刚刚描述的自顶向上方法。 如果我们先添加父元素,然后添加它们的子元素和它们的子元素(如果有的话),我们可以避免额外的布局通道。 使用自上而下方法的性能改进大约快了五倍,因此并不是微不足道的。 让我们看一下我们接下来可以使用的一些与控制相关的性能优势。

得出结论

当我们需要在 UI 中绘制形状时,例如在第 8 章中的标注窗口示例中,创建视觉上吸引人的用户界面,我们倾向于使用抽象的 Shape 类,或者更准确地说,是它的一个或多个派生类。

Shape 类扩展了 FrameworkElement 类,因此它可以利用布局系统、设置样式、访问一系列笔触和填充属性,并且它的属性可以是数据绑定和动画的。 这使它易于使用,并且通常是在 WPF 应用程序中绘制的首选方法。

但是,WPF 还提供了较低级别的类,它们可以实现相同的最终结果,但效率更高。 扩展抽象绘图类的五个类具有小得多的继承层次结构,因此,与基于 Shape 对象的对应物相比,它们的内存占用量要小得多。

最常用的两个类包括用于绘制几何形状的 GeometryDrawing 类和用于将多个绘图对象组合成单个复合绘图的 DrawingGroup 类。

此外,Drawing 类也由 GlyphRunDrawing 类扩展,该类呈现文本; ImageDrawing 类,用于显示图像; 和 VideoDrawing 类,它使我们能够播放视频文件。 由于 Drawing 类扩展了 Freezable 类,因此可以通过冻结其实例来进一步节省效率,也就是说,如果以后不需要修改它们。

在 WPF 中绘制形状还有另一种可能更有效的方法。 DrawingVisual 类不提供事件处理或布局功能,因此与其他绘图方法相比,它的性能有所提高。 但是,这是一个纯代码解决方案,没有基于 XAML 的 DrawingVisual 选项。

此外,它缺乏布局能力意味着,为了显示它,我们需要创建一个类来扩展一个在 UI 中提供布局支持的类,例如 FrameworkElement 类。 不过,为了更高效,我们可以扩展 Visual 类,因为它是可以在 UI 中呈现的最轻量级的类,具有最少的属性且无需处理任何事件。

此类将负责维护要呈现的 Visual 元素的集合,创建一个或多个 DrawingVisual 对象以添加到集合中,并覆盖属性和方法,以便参与呈现过程。 如果需要用户交互,它还可以选择性地提供事件处理和命中测试功能。

这真的取决于我们想要画什么。 通常,绘图效率越高,其灵活性就越低。 例如,如果我们只是绘制一些静态剪贴画、背景图像,或者可能是徽标,我们可以利用更有效的绘制方法。但是,如果我们需要随着应用程序窗口大小的变化而放大和缩小绘图, 那么我们将需要使用提供更多灵活性的效率较低的方法,或者使用另一个提供该功能的类。

让我们探索一个使用三种不同绘图方法中的每一种创建相同图形图像的示例。 我们将定义一些笑脸表情,从左侧的基于 Shape 的方法开始,中间的基于绘图对象的方法,以及右侧的基于 DrawingVisual 的方法。 我们先来看看视觉输出:

在这里插入图片描述

现在,让我们检查 XAML:

<UserControl x:Class="CompanyName.ApplicationName.Views.DrawingView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:Controls=
             "clr-namespace:CompanyName.ApplicationName.Views.Controls"
             xmlns:PresentationOptions=
             "http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
             Width="450" Height="150">
    <Grid>
        <Grid.Resources>
            <RadialGradientBrush x:Key="RadialBrush" RadiusX="0.8" RadiusY="0.8"
                                 PresentationOptions:Freeze="True">
                <GradientStop Color="Orange" Offset="1.0" />
                <GradientStop Color="Yellow" />
            </RadialGradientBrush>
        </Grid.Resources>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="3*" />
                <RowDefinition Height="2*" />
                <RowDefinition Height="2*" />
                <RowDefinition Height="2*" />
                <RowDefinition Height="3*" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition />
                <ColumnDefinition />
                <ColumnDefinition />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <Ellipse Grid.RowSpan="5" Grid.ColumnSpan="5"
                     Fill="{StaticResource RadialBrush}" Stroke="Black"
                     StrokeThickness="5" />
            <Ellipse Grid.Row="1" Grid.Column="1" Fill="Black" Width="20"
                     HorizontalAlignment="Center" />
            <Ellipse Grid.Row="1" Grid.Column="3" Fill="Black" Width="20"
                     HorizontalAlignment="Center" />
            <Path Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="3" Stroke="Black"
                  StrokeThickness="10" StrokeStartLineCap="Round"
                  StrokeEndLineCap="Round" Data="M0,10 A10,25 0 0 0 12.5,10"
                  Stretch="Fill" HorizontalAlignment="Stretch" />
        </Grid>
        <Canvas Grid.Column="1">
            <Canvas.Background>
                <DrawingBrush PresentationOptions:Freeze="True">
                    <DrawingBrush.Drawing>
                        <DrawingGroup>
                            <GeometryDrawing Brush="{StaticResource RadialBrush}">
                                <GeometryDrawing.Geometry>
                                    <EllipseGeometry Center="50,50" RadiusX="50"
                                                     RadiusY="50" />
                                </GeometryDrawing.Geometry>
                                <GeometryDrawing.Pen>
                                    <Pen Thickness="3.5" Brush="Black" />
                                </GeometryDrawing.Pen>
                            </GeometryDrawing>
                            <GeometryDrawing Brush="Black">
                                <GeometryDrawing.Geometry>
                                    <EllipseGeometry Center="29.5,33" RadiusX="6.75"
                                                     RadiusY="8.5" />
                                </GeometryDrawing.Geometry>
                            </GeometryDrawing>
                            <GeometryDrawing Brush="Black">
                                <GeometryDrawing.Geometry>
                                    <EllipseGeometry Center="70.5,33" RadiusX="6.75"
                                                     RadiusY="8.5" />
                                </GeometryDrawing.Geometry>
                            </GeometryDrawing>
                            <GeometryDrawing>
                                <GeometryDrawing.Geometry>
                                    <PathGeometry>
                                        <PathGeometry.Figures>
                                            <PathFigure StartPoint="23,62.5">
                                                <ArcSegment Point="77,62.5" Size="41 41" />
                                            </PathFigure>
                                        </PathGeometry.Figures>
                                    </PathGeometry>
                                </GeometryDrawing.Geometry>
                                <GeometryDrawing.Pen>
                                    <Pen Thickness="7" Brush="Black" StartLineCap="Round"
                                         EndLineCap="Round" />
                                </GeometryDrawing.Pen>
                            </GeometryDrawing>
                        </DrawingGroup>
                    </DrawingBrush.Drawing>
                </DrawingBrush>
            </Canvas.Background>
        </Canvas>
        <Canvas Grid.Column="2">
            <Canvas.Background>
                <VisualBrush>
                    <VisualBrush.Visual>
                        <Controls:SmileyFace />
                    </VisualBrush.Visual>
                </VisualBrush>
            </Canvas.Background>
        </Canvas>
    </Grid>
</UserControl>

从这个示例中我们可以直接看到的第一件事是,Shape 基于对象的绘图方法要简单得多,它可以在少得多的 XAML 行中实现与更详细的基于对象的绘图方法相同的输出。 现在让我们研究一下代码。

定义 PresentationOptions XAML 命名空间后,我们声明一个 RadialGradientBrush 资源并优化其效率,方法是使用本章前面讨论的 Freeze 属性冻结它。 请注意,如果我们计划同时多次使用此控件,那么通过在应用程序资源中声明所有 Brush 和 Pen 对象并使用 StaticResource 引用来引用它们,我们可能会更加高效。

然后我们声明一个具有两列的外部 Grid 面板。 在左列中,我们声明了另一个 Grid 面板,它有五行五列。 该内部面板用于定位构成第一个笑脸的各种 Shape 元素。 请注意,我们在此面板的行定义中使用星号大小,以便稍微增加顶部和底部行的大小,以更好地定位脸部的眼睛和嘴巴。

在面板内部,我们定义了一个 Ellipse 对象来创建脸部的整体形状,使用资源中的画笔填充它,并使用黑色画笔添加轮廓。 然后我们使用另外两个用黑色笔刷填充的 Ellipse 元素来绘制眼睛和一个 Path 元素 画出笑容。 请注意,我们没有填充 Path 元素,因为这看起来更像是张开嘴而不是微笑。

另外两个需要注意的重点是,我们必须将 Stretch 属性设置为 Fill 以使 Path 元素填充我们提供给它的可用空间,并且我们必须将 StrokeStartLineCap 和 StrokeEndLineCap 属性设置为 Round 以产生漂亮的, 微笑的圆润两端。

我们指定 Path 元素应该使用它的 Data 属性和我们之前使用的内联迷你语言的形状。 现在让我们将这个值分解为各种迷你语言命令:

M0,10 A10,25 0 0 0 12.5,10

与前面的示例一样,我们从移动命令开始,由 M 和以下坐标对指定,该坐标对指示线的起点。 其余部分由椭圆弧命令占用,该命令由 A 和以下五个数字指定。

按照顺序,Elliptical Arc 命令的五个数字与圆弧的大小有关,或者说它的 x 和 y 半径,它的旋转角度,一个位域来指定圆弧的角度是否应该大于 180 度, 另一个位域来指定弧线是顺时针还是逆时针方向绘制,最后是弧线的终点。

可以在 Microsoft 网站上找到此路径迷你语言语法的完整详细信息。 请注意,我们可以将绘制方向的位字段更改为 1 以画皱眉头:

M0,10 A10,25 0 0 1 12.5,10

现在,让我们转到外部 Grid 面板的第二列。 在本专栏中,我们重新创建了相同的笑脸,但使用了更高效的基于对象的绘图对象。 由于它们不能像 Shape 类那样渲染自己,我们需要利用其他元素来为我们完成这项

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

0neKing2017

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值