[转]如何智能客户端应用程序性能

 

智能客户端应用程序性能

 

发布日期: 08/20/2004 | 更新日期: 08/20/2004
pponline
 

 

 

智能客户端体系结构与设计指南

David Hill、Brenton Webster、Edward A. Jezierski、Srinath Vasireddy、Mohammad Al-Sabt,Microsoft Corporation,Blaine Wastell Ascentium Corporation,Jonathan Rasmusson 和 Paul Gale ThoughtWorks 和 Paul Slater Wadeware LLC

相关链接

Microsoft® patterns & practiceshttp://www.microsoft.com/resources/practices/default.mspx

.NET 的应用程序体系结构:设计应用程序和服务http://msdn.microsoft.com/library/en-us/dnbda/html/distapp.asp

摘要:本章讨论如何优化您的智能客户端应用程序。本章分析您可以在设计时采取的步骤,并介绍如何调整智能客户端应用程序以及诊断任何性能问题。

*
 
本页内容
针对性能进行设计针对性能进行设计
性能调整和诊断性能调整和诊断
小结小结
参考资料参考资料

智能客户端应用程序可以提供比 Web 应用程序更丰富和响应速度更快的用户界面,并且可以利用本地系统资源。如果应用程序的大部分驻留在用户的计算机上,则应用程序不需要到 Web 服务器的持续的往返行程。这有利于提高性能和响应性。然而,要实现智能客户端应用程序的全部潜能,您应该在应用程序的设计阶段仔细考虑性能问题。通过在规划和设计您的应用程序时解决性能问题,可以帮助您及早控制成本,并减小以后陷入性能问题的可能性。

改善智能客户端应用程序的性能并不仅限于应用程序设计问题。您可以在整个应用程序生存期中采取许多个步骤来使 .NET 代码具有更高的性能。虽然 .NET 公共语言运行库 (CLR) 在执行代码方面非常有效,但您可以使用多种技术来提高代码的性能,并防止在代码级引入性能问题。有关这些问题的详细信息,请参阅http://msdn.microsoft.com/perf

在应用程序的设计中定义现实的性能要求并识别潜在的问题显然是重要的,但是性能问题通常只在编写代码之后对其进行测试时出现。在这种情况下,您可以使用一些工具和技术来跟踪性能问题。

本章分析如何设计和调整您的智能客户端应用程序以获得最佳性能。它讨论了许多设计和体系结构问题(包括线程处理和缓存注意事项),并且分析了如何增强应用程序的 Windows 窗体部分的性能。本章还介绍了您可以用来跟踪和诊断智能客户端应用程序性能问题的一些技术和工具。

针对性能进行设计

您可以在应用程序设计或体系结构级完成许多工作,以确保智能客户端应用程序具有良好的性能。您应该确保在设计阶段尽可能早地制定现实的且可度量的性能目标,以便评估设计折衷,并且提供最划算的方法来解决性能问题。只要可能,性能目标就应该基于实际的用户和业务要求,因为这些要求受到应用程序所处的操作环境的强烈影响。性能建模是一种结构化的且可重复的过程,您可以使用该过程来管理您的应用程序并确保其实现性能目标。有关详细信息,请参阅 Improving .NET Application Performance and Scalability 中的第 2 章“Performance Modeling”,网址为:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag/html/scalenetchapt02.asp

智能客户端通常是较大的分布式应用程序的组成部分。很重要的一点是在完整应用程序的上下文中考虑智能客户端应用程序的性能,包括该客户端应用程序使用的所有位于网络中的资源。微调并优化应用程序中的每一个组件通常是不必要或不可能的。相反,性能调整应该基于优先级、时间、预算约束和风险。一味地追求高性能通常并不是一种划算的策略。

智能客户端还将需要与用户计算机上的其他应用程序共存。当您设计智能客户端应用程序时,您应该考虑到您的应用程序将需要与客户端计算机上的其他应用程序共享系统资源,例如,内存、CPU 时间和网络利用率。

有关设计可伸缩的高性能远程服务的信息,请参阅Improving .NET Performance and Scalability,网址为:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag/html/scalenet.asp。本指南包含有关如何优化 .NET 代码以获得最佳性能的详细信息。

要设计高性能的智能客户端,请考虑下列事项:

在适当的位置缓存数据。数据缓存可以显著改善智能客户端应用程序的性能,使您可以在本地使用数据,而不必经常从网络检索数据。但是,敏感数据或频繁更改的数据通常不适合进行缓存。
优化网络通讯。如果通过“健谈的”接口与远程层服务进行通讯,并且借助于多个请求/响应往返行程来执行单个逻辑操作,则可能消耗系统和网络资源,从而导致低劣的应用程序性能。
有效地使用线程。如果您使用用户界面 (UI) 线程执行阻塞 I/O 绑定调用,则 UI 似乎不对用户作出响应。因为创建和关闭线程需要系统开销,所以创建大量不必要的线程可能导致低劣的性能。
有效地使用事务。如果客户端具有本地数据,则使用原子事务可帮助您确保该数据是一致的。因为数据是本地的,所以事务也是本地的而不是分布式的。对于脱机工作的智能客户端而言,对本地数据进行的任何更改都是暂时的。客户端在重新联机时需要同步更改。对于非本地数据而言,在某些情况下可以使用分布式事务(例如,当服务位于具有良好连接性的同一物理位置并且服务支持它时)。诸如 Web 服务和消息队列之类的服务不支持分布式事务。
优化应用程序启动时间。较短的应用程序启动时间使用户可以更为迅速地开始与应用程序交互,从而使用户立刻对应用程序的性能和可用性产生好感。应该对您的应用程序进行适当的设计,以便在应用程序启动时仅加载那些必需的程序集。因为加载每个程序集都会引起性能开销,所以请避免使用大量程序集。
有效地管理可用资源。低劣的设计决策(例如,实现不必要的完成器,未能在 Dispose 方法中取消终止,或者未能释放非托管资源)可能导致在回收资源时发生不必要的延迟,并且可能造成使应用程序性能降低的资源泄漏。如果应用程序未能正确地释放资源,或者应用程序显式强制进行垃圾回收,则可能会妨碍 CLR 有效地管理内存。
优化 Windows 窗体性能。智能客户端应用程序依靠 Windows 窗体来提供内容丰富且响应迅速的用户界面.您可以使用多种技术来确保 Windows 窗体提供最佳性能。这些技术包括降低用户界面的复杂性,以及避免同时加载大量数据。

在许多情况下,从用户角度感受到的应用程序性能起码与应用程序的实际性能同样重要。您可以通过对设计进行某些特定的更改来创建在用户看来性能高得多的应用程序,例如:使用后台异步处理(以使 UI 能作出响应);显示进度栏以指示任务的进度;提供相应的选项以便用户取消长期运行的任务。

本节将专门详细讨论这些问题。

数据缓存原则

缓存是一种能够改善应用程序性能并提供响应迅速的用户界面的重要技术。您应该考虑下列选项:

缓存频繁检索的数据以减少往返行程。如果您的应用程序必须频繁地与网络服务交互以检索数据,则应该考虑在客户端缓存数据,从而减少通过网络重复获取数据的需要。这可以极大地提高性能,提供对数据的近乎即时的访问,并且消除了可能对智能客户端应用程序性能造成不利影响的网络延迟和中断风险。
缓存只读引用数据。只读引用数据通常是理想的缓存对象。此类数据用于提供进行验证和用户界面显示所需的数据,例如,产品说明、ID 等等。因为客户端无法更改此类数据,所以通常可以在客户端缓存它而无须进行任何进一步的特殊处理。
缓存要发送给位于网络上的服务的数据。您应该考虑缓存要发送给位于网络上的服务的数据。例如,如果您的应用程序允许用户输入由在多个窗体中收集的一些离散数据项组成的定单信息,则请考虑允许用户输入全部数据,然后在输入过程的结尾在一个网络调用中发送定单信息。
尽量少地缓存高度不稳定的数据。在缓存任何不稳定的数据之前,您需要考虑在其变得陈旧或者由于其他原因变得不可用之前,能够将其缓存多长时间。如果数据高度不稳定并且您的应用程序依赖于最新信息,则或许只能将数据缓存很短一段时间(如果可以缓存)。
尽量少地缓存敏感数据。您应该避免在客户端上缓存敏感数据,因为在大多数情况下,您无法保证客户端的物理安全。但是,如果您必须在客户端上缓存敏感数据,则您通常将需要加密数据,该操作本身也会影响性能。

有关数据缓存的其他问题的详细信息,请参阅本指南的第 2 章。另请参阅 Improving .NET Application Performance and Scalability 的第 3 章“Design Guidelines for Application Performance”(http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag/html/scalenetchapt03.asp) 的“Caching”一节以及 Improving .NET Application Performance and Scalability 的第 4 章“Architecture and Design Review of .NET Application for Performance and Scalability”(http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag/html/scalenetchapt04.asp)。

网络通讯原则

您将面临的另一个决策是如何设计和使用网络服务,例如,Web 服务。特别地,您应该考虑与网络服务交互的粒度、同步性和频率。要获得最佳的性能和可伸缩性,您应该在单个调用中发送更多的数据,而不是在多个调用中发送较少量的数据。例如,如果您的应用程序允许用户在定单中输入多个项,则较好的做法是为所有项收集数据,然后将完成的采购定单一次性发送给服务,而不是在多个调用中发送单个项的详细信息。除了降低与进行大量网络调用相关联的系统开销以外,这还可以减少服务和/或客户端内的复杂状态管理的需要。

应该将您的智能客户端应用程序设计为尽可能地使用异步通讯,因为这将有助于使用户界面快速响应以及并行执行任务。有关如何使用 BeginInvokeEndInvoke 方法异步启动调用和检索数据的详细信息,请参阅“Asynchronous Programming Overview”(http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpovrasynchronousprogrammingoverview.asp)。

有关设计和构建偶尔连接到网络的智能客户端应用程序的详细信息,请参阅第 3 章“建立连接”第 4 章“偶尔连接的智能客户端”

线程处理原则

在应用程序内使用多个线程可能是一种提高其响应性和性能的好方法。特别地,您应该考虑使用线程来执行可以在后台安全地完成且不需要用户交互的处理。通过在后台执行此类工作,可以使用户能够继续使用应用程序,并且使应用程序的主用户界面线程能够维持应用程序的响应性。

适合于在单独的线程上完成的处理包括:

应用程序初始化。请在后台线程上执行漫长的初始化,以便用户能够尽快地与您的应用程序交互,尤其是在应用程序功能的重要或主要部分并不依赖于该初始化完成时。
远程服务调用。请在单独的后台线程上通过网络进行所有远程调用。很难(如果不是无法)保证位于网络上的服务的响应时间。在单独的线程上执行这些调用可以减少发生网络中断或延迟的风险,从而避免对应用程序性能造成不利影响。
IO 绑定处理。应该在单独的线程上完成诸如在磁盘上搜索和排序数据之类的处理。通常,这种工作要受到磁盘 I/O 子系统而不是处理器可用性的限制,因此当该工作在后台执行时,您的应用程序可以有效地维持其响应性。

尽管使用多个线程的性能好处可能很显著,但需要注意,线程使用它们自己的资源,并且使用太多的线程可能给处理器(它需要管理线程之间的上下文切换)造成负担。要避免这一点,请考虑使用线程池,而不是创建和管理您自己的线程。线程池将为您有效地管理线程,重新使用现有的线程对象,并且尽可能地减小与线程创建和处置相关联的系统开销。

如果用户体验受到后台线程所执行的工作的影响,则您应该总是让用户了解工作的进度。以这种方式提供反馈可以增强用户对您的应用程序的性能的感觉,并且防止他或她假设没有任何事情发生。请努力确保用户可以随时取消漫长的操作。

您还应该考虑使用 Application 对象的 Idle 事件来执行简单的操作。Idle 事件提供了使用单独的线程来进行后台处理的简单替代方案。当应用程序不再有其他用户界面消息需要处理并且将要进入空闲状态时,该事件将激发。您可以通过该事件执行简单的操作,并且利用用户不活动的情况。例如:

[C#]

public Form1()
{
InitializeComponent();
Application.Idle += new EventHandler( OnApplicationIdle );
}
private void OnApplicationIdle( object sender, EventArgs e )
{
}

[Visual Basic .NET]

Public Class Form1
Inherits System.Windows.Forms.Form
Public Sub New()
MyBase.New()
InitializeComponent()
AddHandler Application.Idle, AddressOf OnApplicationIdle
End Sub
Private Sub OnApplicationIdle(ByVal sender As System.Object, ByVal e As System.EventArgs)
End Sub
End Class

有关在智能客户端中使用多个线程的详细信息,请参阅第 6 章“使用多个线程”

事务原则

事务可以提供重要的支持,以确保不会违反业务规则并维护数据一致性。事务可以确保一组相关任务作为一个单元成功或失败。您可以使用事务来维护本地数据库和其他资源(包括消息队列的队列)之间的一致性。

对于需要在网络连接不可用时使用脱机缓存数据的智能客户端应用程序,您应该将事务性数据排队,并且在网络连接可用时将其与服务器进行同步。

您应该避免使用涉及到位于网络上的资源的分布式事务,因为这些情况可能导致与不断变化的网络和资源响应时间有关的性能问题。如果您的应用程序需要在事务中涉及到位于网络上的资源,则应该考虑使用补偿事务,以便使您的应用程序能够在本地事务失败时取消以前的请求。尽管补偿事务在某些情况下可能不适用,但它们使您的应用程序能够按照松耦合方式在事务的上下文内与网络资源交互,从而减少了不在本地计算机控制之下的资源对应用程序的性能造成不利影响的可能性。

有关在智能客户端中使用事务的详细信息,请参阅第 3 章“建立连接”

优化应用程序启动时间

快速的应用程序启动时间几乎可以使用户立即开始与应用程序交互,从而使用户立刻对应用程序的性能和可用性产生好感。

当应用程序启动时,首先加载 CLR,再加载应用程序的主程序集,随后加载为解析从应用程序的主窗体中引用的对象的类型所需要的所有程序集。CLR 在该阶段不会 加载所有相关程序集;它仅加载包含主窗体类上的成员变量的类型定义的程序集。在加载了这些程序集之后,实时 (JIT) 编译器将在方法运行时编译方法的代码(从 Main 方法开始)。同样,JIT 编译器不会 编译您的程序集中的所有代码。相反,将根据需要逐个方法地编译代码。

要尽可能减少应用程序的启动时间,您应该遵循下列原则:

尽可能减少应用程序主窗体类中的成员变量。这将在 CLR 加载主窗体类时尽可能减少必须解析的类型数量。
尽量不要立即使用大型基类程序集(XML 库或 ADO.NET 库)中的类型。这些程序集的加载很费时间。使用应用程序配置类和跟踪开关功能时将引入 XML 库。如果要优先考虑应用程序启动时间,请避免这一点。
尽可能使用惰性加载。仅在需要时获取数据,而不是提前加载和冻结 UI。
将应用程序设计为使用较少的程序集。带有大量程序集的应用程序会招致性能开销增加。这些开销来自加载元数据、访问 CLR 中的预编译映像中的各种内存页以加载程序集(如果它是用本机映像生成器工具 Ngen.exe 预编译的)、JIT 编译时间、安全检查等等。您应该考虑基于程序集的使用模式来合并程序集,以便降低相关联的性能开销。
避免设计将多个组件的功能组合到一个组件中的单一类。将设计分解到多个只须在实际调用时进行编译的较小类。
将应用程序设计为在初始化期间对网络服务进行并行调用。通过在初始化期间调用可以并行运行的网络服务,可以利用服务代理提供的异步功能。这有助于释放当前执行的线程并且并发地调用服务以完成任务。
使用 NGEN.exe 编译和试验 NGen 和非 NGen 程序集,并且确定哪个程序集保存了最大数量的工作集页面。NGEN.exe(它随附在 .NET Framework 中)用于预编译程序集以创建本机映像,该映像随后被存储在全局程序集缓存的特殊部分,以便应用程序下次需要它时使用。通过创建程序集的本机映像,可以使程序集更快地加载和执行,因为 CLR 不需要动态生成程序集中包含的代码和数据结构。有关详细信息,请参阅 Improving .NET Application Performance and Scalability 的第 5 章“Improving Managed Code Performance”中的“Working Set Considerations”和“NGen.exe Explained”部分,网址为:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag/html/scalenetchapt05.asp

如果您使用 NGEN 预编译程序集,则会立即加载它的所有依赖程序集。

管理可用资源

公共语言运行库 (CLR) 使用垃圾回收器来管理对象生存期和内存使用。这意味着无法再访问的对象将被垃圾回收器自动回收,并且自动回收内存。由于多种原因无法再访问对象。例如,可能没有对该对象的任何引用,或者对该对象的所有引用可能来自其他可作为当前回收周期的一部分进行回收的对象。尽管自动垃圾回收使您的代码不必负责管理对象删除,但这意味着您的代码不再对对象的确切删除时间具有显式控制。

请考虑下列原则,以确保您能够有效地管理可用资源:

确保在被调用方对象提供 Dispose 方法时该方法得到调用。如果您的代码调用了支持 Dispose 方法的对象,则您应该确保在使用完该对象之后立即调用此方法。调用 Dispose 方法可以确保抢先释放非托管资源,而不是等到发生垃圾回收。除了提供 Dispose 方法以外,某些对象还提供其他管理资源的方法,例如,Close 方法。在这些情况下,您应该参考文档资料以了解如何使用其他方法。例如,对于 SqlConnection 对象而言,调用 CloseDispose 都足可以抢先将数据库连接释放回连接池中。一种可以确保您在对象使用完毕之后立即调用 Dispose 的方法是使用 Visual C# .NET 中的 using 语句或 Visual Basic .NET 中的 Try/Finally 块。

下面的代码片段演示了 Dispose 的用法。

C# 中的 using 语句示例:

using( StreamReader myFile = new StreamReader("C:\\ReadMe.Txt")){
            string contents = myFile.ReadToEnd();
            //... use the contents of the file
            } // dispose is called and the StreamReader's resources released
            

Visual Basic .NET 中的 Try/Finally 块示例:

Dim myFile As StreamReader
            myFile = New StreamReader("C:\\ReadMe.Txt")
            Try
            String contents = myFile.ReadToEnd()
            '... use the contents of the file
            Finally
            myFile.Close()
            End Try
            

在 C# 和 C++ 中,Finalize 方法是作为析构函数实现的。在 Visual Basic .NET 中,Finalize 方法是作为 Object 基类上的 Finalize 子例程的重写实现的。

如果您在客户端调用过程中占据非托管资源,则请提供 Finalize 和 Dispose 方法。如果您在公共或受保护的方法调用中创建访问非托管资源的对象,则应用程序需要控制非托管资源的生存期。在图 8.1 中,第一种情况是对非托管资源的调用,在此将打开、获取和关闭资源。在此情况下,您的对象无须提供 FinalizeDispose 方法。在第二种情况下,在方法调用过程中占据非托管资源;因此,您的对象应该提供 FinalizeDispose 方法,以便客户端在使用完该对象后可以立即显式释放资源。
ppt-pic_f01
 

图 8.1:Dispose 和 Finalize 方法调用的用法

 

垃圾回收通常有利于提高总体性能,因为它将速度的重要性置于内存利用率之上。只有当内存资源不足时,才需要删除对象;否则,将使用所有可用的应用程序资源以使您的应用程序受益。但是,如果您的对象保持对非托管资源(例如,窗口句柄、文件、GDI 对象和网络连接)的引用,则程序员通过在这些资源不再使用时显式释放它们可以获得更好的性能。如果您要在客户端方法调用过程中占据非托管资源,则对象应该允许调用方使用 IDisposable 接口(它提供 Dispose 方法)显式管理资源。通过实现 IDisposable,对象将通知它可被要求明确进行清理,而不是等待垃圾回收。实现 IDisposable 的对象的调用方在使用完该对象后将简单地调用 Dispose 方法,以便它可以根据需要释放资源。

有关如何在某个对象上实现 IDisposable 的详细信息,请参阅 Improving .NET Application Performance and Scalability 中的第 5 章“Improving Managed Code Performance”,网址为:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag/html/scalenetchapt05.asp

如果您的可处置对象派生自另一个也实现了 IDisposable 接口的对象,则您应该调用基类的 Dispose 方法以使其可以清理它的资源。您还应该调用实现了 IDisposable 接口的对象所拥有的所有对象上的 Dispose

Finalize 方法也使您的对象可以在删除时显式释放其引用的任何资源。由于垃圾回收器所具有的非确定性,在某些情况下,Finalize 方法可能长时间不会被调用。实际上,如果您的应用程序在垃圾回收器删除对象之前终止,则该方法可能永远不会被调用。然而,需要使用 Finalize 方法作为一种后备策略,以防调用方没有显式调用 Dispose 方法(DisposeFinalize 方法共享相同的资源清理代码)。通过这种方式,可能在某个时刻释放资源,即使这发生在最佳时刻之后。

要确保 Dispose 和 Finalize 中的清理代码不会被调用两次,您应该调用 GC.SuppressFinalize 以通知垃圾回收器不要调用 Finalize 方法。

垃圾回收器实现了 Collect 方法,该方法强制垃圾回收器删除所有对象挂起删除。不应该从应用程序内调用该方法,因为回收周期在高优先级线程上运行。回收周期可能冻结所有 UI 线程,从而使得用户界面停止响应。

有关详细信息,请参阅 Improving .NET Application Performance and Scalability 中的“Garbage Collection Guidelines”、“Finalize and Dispose Guidelines”、“Dispose Pattern”和“Finalize and Dispose Guidelines”,网址为:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag/html/scalenetchapt05.asp

优化 Windows 窗体性能

Windows 窗体为智能客户端应用程序提供了内容丰富的用户界面,并且您可以使用许多种技术来帮助确保 Windows 窗体提供最佳性能。在讨论特定技术之前,对一些可以显著提高 Windows 窗体性能的高级原则进行回顾是有用的。

小心创建句柄。Windows 窗体将句柄创建虚拟化(即,它动态创建和重新创建窗口句柄对象)。创建句柄对象的系统开销可能非常大;因此,请避免进行不必要的边框样式更改或者更改 MDI 父对象。
避免创建带有太多子控件的应用程序。Microsoft? Windows? 操作系统限制每个进程最多有 10,000 个控件,但您应该避免在窗体上使用成百上千个控件,因为每个控件都要消耗内存资源。

本节的其余部分讨论您可以用来优化应用程序用户界面性能的更为具体的技术。

使用 BeginUpdate 和 EndUpdate

许多 Windows 窗体控件(例如,ListViewTreeView 控件)实现了 BeginUpdateEndUpdate 方法,它们在操纵基础数据或控件属性时取消了控件的重新绘制。通过使用 BeginUpdateEndUpdate 方法,您可以对控件进行重大更改,并且避免在应用这些更改时让控件经常重新绘制自身。此类重新绘制会导致性能显著降低,并且用户界面闪烁且不反应。

例如,如果您的应用程序具有一个要求添加大量节点项的树控件,则您应该调用 BeginUpdate,添加所有必需的节点项,然后调用 EndUpdate。下面的代码示例显示了一个树控件,该控件用于显示许多个客户的层次结构表示形式及其定单信息。

[C#]

// Suppress repainting the TreeView until all the objects have been created.
TreeView1.BeginUpdate();
// Clear the TreeView.
TreeView1.Nodes.Clear();
// Add a root TreeNode for each Customer object in the ArrayList.
foreach( Customer customer2 in customerArray )
{
TreeView1.Nodes.Add( new TreeNode( customer2.CustomerName ) );
// Add a child TreeNode for each Order object in the current Customer.
foreach( Order order1 in customer2.CustomerOrders )
{
TreeView1.Nodes[ customerArray.IndexOf(customer2) ].Nodes.Add(
new TreeNode( customer2.CustomerName + "." + order1.OrderID ) );
}
}
// Begin repainting the TreeView.
TreeView1.EndUpdate();

[Visual Basic .NET]

       ' Suppress repainting the TreeView until all the objects have been created.
TreeView1.BeginUpdate()
' Clear the TreeView
TreeView1.Nodes.Clear()
' Add a root TreeNode for each Customer object in the ArrayList
For Each customer2 As Customer In customerArray
TreeView1.Nodes.Add(New TreeNode(customer2.CustomerName))
' Add a child TreeNode for each Order object in the current Customer.
For Each order1 As Order In customer2.CustomerOrders
TreeView1.Nodes(Array.IndexOf(customerArray, customer2)).Nodes.Add( _
New TreeNode(customer2.CustomerName & "." & order1.OrderID))
Next
Next
' Begin repainting the TreeView.
TreeView1.EndUpdate()

即使在您不希望向控件添加许多对象时,您也应该使用 BeginUpdateEndUpdate 方法。在大多数情况下,您在运行之前将不知道要添加的项的确切个数。因此,为了妥善处理大量数据以及应付将来的要求,您应该总是调用 BeginUpdateEndUpdate 方法。

调用 Windows 窗体控件使用的许多 Collection 类的 AddRange 方法时,将自动为您调用 BeginUpdateEndUpdate 方法。

使用 SuspendLayout 和 ResumeLayout

许多 Windows 窗体控件(例如,ListViewTreeView 控件)都实现了 SuspendLayoutResumeLayout 方法,它们能够防止控件在添加子控件时创建多个布局事件。

如果您的控件以编程方式添加和删除子控件或者执行动态布局,则您应该调用 SuspendLayoutResumeLayout 方法。通过 SuspendLayout 方法,可以在控件上执行多个操作,而不必为每个更改执行布局。例如,如果您调整控件的大小并移动控件,则每个操作都将引发单独的布局事件。

这些方法按照与 BeginUpdateEndUpdate 方法类似的方式操作,并且在性能和用户界面稳定性方面提供相同的好处。

下面的示例以编程方式向父窗体中添加按钮:

[C#]

private void AddButtons()
{
// Suspend the form layout and add two buttons.
this.SuspendLayout();
Button buttonOK = new Button();
buttonOK.Location = new Point(10, 10);
buttonOK.Size = new Size(75, 25);
buttonOK.Text = "OK";
Button buttonCancel = new Button();
buttonCancel.Location = new Point(90, 10);
buttonCancel.Size = new Size(75, 25);
buttonCancel.Text = "Cancel";
this.Controls.AddRange(new Control[]{buttonOK, buttonCancel});
this.ResumeLayout();
}

[Visual Basic .NET]

Private Sub AddButtons()
' Suspend the form layout and add two buttons
Me.SuspendLayout()
Dim buttonOK As New Button
buttonOK.Location = New Point(10, 10)
buttonOK.Size = New Size(75, 25)
buttonOK.Text = "OK"
Dim buttonCancel As New Button
buttonCancel.Location = New Point(90, 10)
buttonCancel.Size = New Size(75, 25)
buttonCancel.Text = "Cancel"
Me.Controls.AddRange(New Control() { buttonOK, buttonCancel } )
Me.ResumeLayout()
End Sub

每当您添加或删除控件、执行子控件的自动布局或者设置任何影响控件布局的属性(例如,大小、位置、定位点或停靠属性)时,您都应该使用 SuspendLayoutResumeLayout 方法。

处理图像

如果您的应用程序显示大量图像文件(例如,.jpg 和 .gif 文件),则您可以通过以位图格式预先呈现图像来显著改善显示性能。

要使用该技术,请首先从文件中加载图像,然后使用 PARGB 格式将其呈现为位图。下面的代码示例从磁盘中加载文件,然后使用该类将图像呈现为预乘的、Alpha 混合 RGB 格式。例如:

[C#]

if ( image != null && image is Bitmap )
{
Bitmap bm = (Bitmap)image;
Bitmap newImage = new Bitmap( bm.Width, bm.Height,
System.Drawing.Imaging.PixelFormat.Format32bppPArgb );
using ( Graphics g = Graphics.FromImage( newImage ) )
{
g.DrawImage( bm, new Rectangle( 0,0, bm.Width, bm.Height ) );
}
image = newImage;
}

[Visual Basic .NET]

        If Not(image Is Nothing)  AndAlso (TypeOf image Is Bitmap) Then
Dim bm As Bitmap = CType(image, Bitmap)
Dim newImage As New Bitmap(bm.Width, bm.Height, _
System.Drawing.Imaging.PixelFormat.Format32bppPArgb)
Using g As Graphics = Graphics.FromImage(newImage)
g.DrawImage(bm, New Rectangle(0, 0, bm.Width, bm.Height))
End Using
image = newImage
End If
使用分页和惰性加载

在大多数情况下,您应该仅在需要时检索或显示数据。如果您的应用程序需要检索和显示大量信息,则您应该考虑将数据分解到多个页面中,并且一次显示一页数据。这可以使用户界面具有更高的性能,因为它无须显示大量数据。此外,这可以提高应用程序的可用性,因为用户不会同时面对大量数据,并且可以更加容易地导航以查找他或她需要的确切数据。

例如,如果您的应用程序显示来自大型产品目录的产品数据,则您可以按照字母顺序显示这些项,并且将所有以“A”开头的产品显示在一个页面上,将所有以“B”开头的产品显示在下一个页面上。然后,您可以让用户直接导航到适当的页面,以便他或她无须浏览所有页面就可以获得他或她需要的数据。

以这种方式将数据分页还使您可以根据需要获取后台的数据。例如,您可能只需要获取第一页信息以便显示并且让用户与其进行交互。然后,您可以获取后台中的、已经准备好供用户使用的下一页数据。该技术在与数据缓存技术结合使用时可能特别有效。

您还可以通过使用惰性加载技术来提高智能客户端应用程序的性能。您无须立即加载可能在将来某个时刻需要的数据或资源,而是可以根据需要加载它们。您可以在构建大型列表或树结构时使用惰性加载来提高用户界面的性能。在此情况下,您可以在用户需要看到数据时(例如,在用户展开树节点时)加载它。

优化显示速度

根据您用于显示用户界面控件和应用程序窗体的技术,您可以用多种不同的方式来优化应用程序的显示速度。

当您的应用程序启动时,您应该考虑尽可能地显示简单的用户界面。这将减少启动时间,并且向用户呈现整洁且易于使用的用户界面。而且,您应该努力避免引用类以及在启动时加载任何不会立刻需要的数据。这将减少应用程序和 .NET Framework 初始化时间,并且提高应用程序的显示速度。

当您需要显示对话框或窗体时,您应该在它们做好显示准备之前使其保持隐藏状态,以便减少需要的绘制工作量。这将有助于确保窗体仅在初始化之后显示。

如果您的应用程序具有的控件含有覆盖整个客户端表面区域的子控件,则您应该考虑将控件背景样式设置为不透明。这可以避免在发生每个绘制事件时重绘控件的背景。您可以通过使用 SetStyle 方法来设置控件的样式。使用 ControlsStyles.Opaque 枚举可以指定不透明控件样式。

您应该避免任何不必要的控件重新绘制操作。一种方法是在设置控件的属性时隐藏控件。在 OnPaint 事件中具有复杂绘图代码的应用程序能够只重绘窗体的无效区域,而不是绘制整个窗体。OnPaint 事件的 PaintEventArgs 参数包含一个 ClipRect 结构,它指示窗口的哪个部分无效。这可以减少用户等待查看完整显示的时间。

使用标准的绘图优化,例如,剪辑、双缓冲和 ClipRectangle。这还将通过防止对不可见或要求重绘的显示部分执行不必要的绘制操作,从而有助于改善智能客户端应用程序的显示性能。有关增强绘图性能的详细信息,请参阅 Painting techniques using Windows Forms for the Microsoft .NET Framework,网址为:http://windowsforms.net/articles/windowsformspainting.aspx

如果您的显示包含动画或者经常更改某个显示元素,则您应该使用双缓冲或多缓冲,在绘制当前图像的过程中准备下一个图像。System.Windows.Forms 命名空间中的 ControlStyles 枚举适用于许多控件,并且 DoubleBuffer 成员可以帮助防止闪烁。启用 DoubleBuffer 样式将使您的控件绘制在离屏缓冲中完成,然后同时绘制到屏幕上。尽管这有助于防止闪烁,但它的确为分配的缓冲区使用了更多内存。

性能调整和诊断

在设计和实现阶段处理性能问题是实现应用程序性能目标的最划算的方法。但是,您只有在开发阶段经常且尽早测试应用程序的性能,才能真正有效地优化应用程序的性能。

尽管针对性能进行设计和测试都很重要,但在这些早期阶段优化每个组件和所有代码不是有效的资源用法,因此应该予以避免。所以,应用程序可能存在您在设计阶段未预料到的性能问题。例如,您可能遇到由于两个系统或组件之间的无法预料的交互而产生的性能问题,或者您可能使用原来存在的、未按希望的方式执行的代码。在此情况下,您需要追究性能问题的根源,以便您可以适当地解决该问题。

本节讨论一些将帮助您诊断性能问题以及调整应用程序以获得最佳性能的工具和技术。

制定性能目标

当您设计和规划智能客户端应用程序时,您应该仔细考虑性能方面的要求,并且定义合适的性能目标。在定义这些目标时,请考虑您将如何度量应用程序的实际性能。您的性能度量标准应该明确体现应用程序的重要性能特征。请努力避免无法准确度量的模糊或不完整的目标,例如,“应用程序必须快速运行”或“应用程序必须快速加载”。您需要了解应用程序的性能和可伸缩性目标,以便您可以设法满足这些目标并且围绕它们来规划您的测试。请确保您的目标是可度量的和可验证的。

定义良好的性能度量标准使您可以准确跟踪应用程序的性能,以便您可以确定应用程序是否能够满足它的性能目标。这些度量标准应该包括在应用程序测试计划中,以便可以在应用程序的测试阶段度量它们。

本节重点讨论与智能客户端应用程序相关的特定性能目标的定义。如果您还要设计和生成客户端应用程序将消耗的网络服务,则您还需要为这些服务定义适当的性能目标。在此情况下,您应该确保考虑整个系统的性能要求,以及应用程序各个部分的性能与其他部分以及整个系统之间存在怎样的关系。

考虑用户的观点

当您为智能客户端应用程序确定合适的性能目标时,您应该仔细考虑用户的观点。对于智能客户端应用程序而言,性能与可用性和用户感受有关。例如,只要用户能够继续工作并且获得有关操作进度的足够反馈,用户就可以接受漫长的操作。

在确定要求时,将应用程序的功能分解为多个使用情景或使用案例通常是有用的。您应该识别对于实现特定性能目标而言关键且必需的使用案例和情景。应该将许多使用案例所共有且经常执行的任务设计得具有较高性能。同样,如果任务要求用户全神贯注并且不允许用户从其切换以执行其他任务,则需要提供优化的且有效的用户体验。如果任务不太经常使用且不会阻止用户执行其他任务,则可能无须进行大量调整。

对于您识别的每个性能敏感型任务,您都应该精确地定义用户的操作以及应用程序的响应方式。您还应该确定每个任务使用的网络和客户端资源或组件。该信息将影响性能目标,并且将驱动对性能进行度量的测试。

可用性研究提供了非常有价值的信息源,并且可能大大影响性能目标的定义。正式的可用性研究在确定用户如何执行他们的工作、哪些使用情景是共有的以及哪些不是共有的、用户经常执行哪些任务以及从性能观点看来应用程序的哪些特征是重要的等方面可能非常有用。如果您要生成新的应用程序,您应该考虑提供应用程序的原型或模型,以便可以执行基本的可用性测试。

考虑应用程序操作环境

对应用程序的操作环境进行评估是很重要的,因为这可能对应用程序施加必须在您制定的性能目标中予以反映的约束。

位于网络上的服务可能对您的应用程序施加性能约束。例如,您可能需要与您无法控制的 Web 服务进行交互。在这种情况下,需要确定该服务的性能,并且确定这是否将对客户端应用程序的性能产生影响。

您还应该确定任何相关服务和组件的性能如何随着时间的变化而变化。某些系统会经受相当稳定的使用,而其他系统则会在一天或一周的特定时间经受变动极大的使用。这些区别可能在关键时间对应用程序的性能造成不利影响。例如,提供应用程序部署和更新服务的服务可能会在星期一早上 9 点缓慢响应,因为所有用户都在此时升级到应用程序的最新版本。

另外,还需要准确地对所有相关系统和组件的性能进行建模,以便可以在严格模拟应用程序的实际部署环境的环境中测试您的应用程序。对于每个系统,您都应该确定性能概况以及最低、平均和最高性能特征。然后,您可以在定义应用程序的性能要求时根据需要使用该数据。

您还应该仔细考虑用于运行应用程序的硬件。您将需要确定在处理器、内存、图形功能等方面的目标硬件配置,或者至少确定一个如果得不到满足则无法保证性能的最低配置。

通常,应用程序的业务操作环境将规定一些更为苛刻的性能要求。例如,执行实时股票交易的应用程序将需要执行这些交易并及时显示所有相关数据。

性能调整过程

对应用程序进行性能调整是一个迭代过程。该过程由一些重复执行直至应用程序满足其性能目标的阶段组成。(请参见图 8.2。)

chapter8_f02
 

图 8.2:性能调整过程

 

正如图 8.2 所阐明的,性能调整要求您完成下列过程:

建立基准。在您开始针对性能调整应用程序时,您必须具有与性能目标、目标和度量标准有关的定义良好的基准。这可能包括应用程序工作集大小、加载数据(例如,目录)的时间、事务持续时间等等。
收集数据。您将需要通过针对您已经定义的性能目标度量应用程序的性能,来对应用程序性能进行评价。性能目标应该体现特定的且可度量的度量标准,以使您可以在任何时刻量化应用程序的性能。要使您可以收集性能数据,您可能必须对应用程序进行规范,以便可以发布和收集必需的性能数据。下一节将详细讨论您可以用来完成这一工作的一些选项。
分析结果。在收集应用程序的性能数据之后,您将能够通过确定哪些应用程序功能要求最多的关注,来区分性能调整工作的轻重缓急。此外,您可以使用该数据来确定任何性能瓶颈的位置。通常,您将只能够通过收集更详细的性能数据来确定瓶颈的确切位置:例如,通过使用应用程序规范。性能分析工具可能帮助您识别瓶颈。
调整应用程序。在已经识别瓶颈之后,您可能需要修改应用程序或其配置,以便尝试解决问题。您应该致力于将更改降低至最低限度,以便可以确定更改对应用程序性能的影响。如果您同时进行多项更改,可能难以确定每项更改对应用程序的总体性能的影响。
测试和度量。在更改应用程序或其配置之后,您应该再次测试它以确定更改具有的效果,并且使新的性能数据得以收集。性能工作通常要求进行体系结构或其他具有较高影响的更改,因此彻底的测试是很关键的。您的应用程序测试计划应该针对预料到的所有情况,在配置了适当硬件和软件的客户计算机上演习应用程序所实现的完整范围的功能。如果您的应用程序使用网络资源,则应该加载这些资源,以便您可以获得有关应用程序在此类环境中所具有的性能的准确度量。

上述过程将使您可以通过针对特定目标度量应用程序的总体性能,来重点解决特定的性能问题。

性能工具

您可以使用许多工具来帮助您收集和分析应用程序的性能数据。本节中介绍的每种工具都具有不同的功能,您可以使用这些功能来度量、分析和查找应用程序中的性能瓶颈。

除了这里介绍的工具以外,您还可以使用其他一些选项和第三方工具。有关其他日志记录和异常管理选项的说明,请参阅Exception Management Architecture Guide,网址为:

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/exceptdotnet.asp

在决定哪些工具最适合您的需要之前,您应该仔细考虑您的确切要求。

使用性能日志和警报

性能日志和警报是作为 Windows 操作系统的一部分发行的一种管理性能监控工具。它依靠由各种 Windows 组件、子系统和应用程序发布的性能计数器,使您可以跟踪资源使用情况以及针对时间以图形方式绘制它们。

您可以使用 Performance Logs and Alerts 来监控标准的性能计数器(例如,内存使用情况或处理器使用情况),或者您可以定义您自己的自定义计数器来监控应用程序特定的活动。

.NET CLR 提供了许多有用的性能计数器,它们使您可以洞察应用程序性能的好坏。关系比较大的一些性能对象是:

.NET CLR 内存。提供有关托管 .NET 应用程序内存使用情况的数据,包括应用程序正在使用的内存数量以及对未使用的对象进行垃圾回收所花费的时间。
.NET CLR 加载。提供有关应用程序正在使用的类和应用程序域的数量的数据,并且提供有关它们的加载和卸载速率的数据。
.NET CLR 锁和线程。提供与应用程序内使用的线程有关的性能数据,包括线程个数以及试图同时对受保护的资源进行访问的线程之间的争用率。
.NET CLR 网络。提供与通过网络发送和接收数据有关的性能计数器,包括每秒发送和接收的字节数以及活动连接的个数。
.NET CLR 异常。提供有关应用程序所引发和捕获的异常个数的报告。

有关上述计数器、它们的阈值、要度量的内容以及如何度量它们的详细信息,请参阅 Improving .NET Application Performance and Scalability 的第 15 章“Measuring .NET Application Performance”中的“CLR and Managed Code”部分,网址为:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag/html/scalenetchapt15.asp

您的应用程序还可以提供您可以通过使用性能日志和警报轻松监控的、应用程序特定的性能计数器。您可以像以下示例所显示的那样,定义自定义性能计数器:

[C#]

PerformanceCounter counter = new PerformanceCounter( "Category",
"CounterName", false );

[Visual Basic .NET]

Dim counter As New PerformanceCounter("Category", "CounterName", False)

在创建性能计数器对象之后,您可以为您的自定义性能计数器指定类别,并将所有相关计数器保存在一起。PerformanceCounter 类在 System.Diagnostics 命名空间中定义,该命名空间中还定义了其他一些可用于读取和定义性能计数器和类别的类。有关创建自定义性能计数器的详细信息,请参阅知识库中编号为 317679 的文章“How to create and make changes to a custom counter for the Windows Performance Monitor by using Visual Basic .NET”,网址为:http://support.microsoft.com/default.aspx?scid=kb;en-us;317679

要注册性能计数器,您必须首先注册该类别。您必须具有足够的权限才能注册性能计数器类别(它可能影响您部署应用程序的方式)。

规范

您可以使用许多工具和技术来帮助您对应用程序进行规范,并且生成度量应用程序性能所需的信息。这些工具和技术包括:

Event Tracing for Windows (ETW)。该 ETW 子系统提供了一种系统开销较低(与性能日志和警报相比)的手段,用以监控具有负载的系统的性能。这主要用于必须频繁记录事件、错误、警告或审核的服务器应用程序。有关详细信息,请参阅 Microsoft Platform SDK 中的“Event Tracing”,网址为:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/perfmon/base/event_tracing.asp
Enterprise Instrumentation Framework (EIF)。EIF 是一种可扩展且可配置的框架,您可以使用它来对智能客户端应用程序进行规划。它提供了一种可扩展的事件架构和统一的 API — 它使用 Windows 中内置的现有事件、日志记录和跟踪机制,包括 Windows Management Instrumentation (WMI)、Windows Event Log 和 Windows Event Tracing。它大大简化了发布应用程序事件所需的编码。如果您计划使用 EIF,则需要通过使用 EIF .msi 在客户计算机上安装 EIF。如果您要在智能客户端应用程序中使用 EIF,则需要在决定应用程序的部署方式时考虑这一要求。有关详细信息,请参阅“How To:Use EIF”,网址为:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag/html/scalenethowto14.asp
Logging Application Block。Logging Application Block 提供了可扩展且可重用的代码组件,以帮助您生成规范化的应用程序。它建立在 EIF 的功能基础之上,以提供某些功能,例如,针对事件架构的增强功能、多个日志级别、附加的事件接收等等。有关详细信息,请参阅“Logging Application Block”,网址为“http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag/html/Logging.asp
Windows Management Instrumentation (WMI)。WMI 组件是 Windows 操作系统的一部分,并且提供了用于访问企业中的管理信息和控件的编程接口。系统管理员常用它来自动完成管理任务(通过使用调用 WMI 组件的脚本)。有关详细信息,请参阅 Windows Management Information,网址为:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/wmisdk/wmi/wmi_start_page.asp
调试和跟踪类。.NET Framework 在 System.Diagnosis 下提供了 DebugTrace 类来对代码进行规范。Debug 类主要用于打印调试信息以及检查是否有断言。Trace 类使您可以对发布版本进行规范,以便在运行时监控应用程序的完好状况。在 Visual Studio .NET 中,默认情况下启用跟踪。在使用命令行版本时,您必须为编译器添加 /d:Trace 标志,或者在 Visual C# .NET 源代码中添加 #define TRACE,以便启用跟踪。对于 Visual Basic .NET 源代码,您必须为命令行编译器添加 /d:TRACE=True。有关详细信息,请参阅知识库中编号为 815788 的文章“HOW TO:Trace and Debug in Visual C# .NET”,网址为:http://support.microsoft.com/default.aspx?scid=kb;en-us;815788

CLR Profiler

CLR Profiler 是 Microsoft 提供的一种内存分析工具,并且可以从 MSDN 下载。它使您能够查看应用程序进程的托管堆以及调查垃圾回收器的行为。使用该工具,您可以获取有关应用程序的执行、内存分配和内存消耗的有用信息。这些信息可以帮助您了解应用程序的内存使用方式以及如何优化应用程序的内存使用情况。

CLR Profiler 可从 http://msdn.microsoft.com/netframework/downloads/tools/default.aspx 获得。有关如何使用 CLR Profiler 工具的详细信息,另请参阅“How to use CLR Profiler”,网址为:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag/html/scalenethowto13.asp?frame=true

CLR Profiler 在日志文件中记录内存消耗和垃圾回收器行为信息。然后,您可以使用一些不同的图形视图,通过 CLR Profiler 来分析该数据。一些比较重要的视图是:

Allocation Graph。显示有关对象分配方式的调用堆栈。您可以使用该视图来查看方法进行的每个分配的系统开销,隔离您不希望发生的分配,以及查看方法可能进行的过度分配。
Assembly, Module, Function, and Class Graph。显示哪些方法造成了哪些程序集、函数、模块或类的加载。
Call Graph。使您可以查看哪些方法调用了其他哪些方法以及相应的调用频率。您可以使用该图表来确定库调用的系统开销,以及调用了哪些方法或对特定方法进行了多少个调用。
Time Line。提供了有关应用程序执行的基于文本的、按时间顺序的层次结构视图。使用该视图可以查看分配了哪些类型以及这些类型的大小。您还可以使用该视图查看方法调用使得哪些程序集被加载,并且分析您不希望发生的分配。您可以分析完成器的使用情况,并且识别尚未实现或调用 CloseDispose 从而导致瓶颈的方法。

您可以使用 CLR Profiler.exe 来识别和隔离与垃圾回收有关的问题。这包括内存消耗问题(例如,过度或未知的分配、内存泄漏、生存期很长的对象)以及在执行垃圾回收时花费的时间的百分比。

有关如何使用 CLR Profiler 工具的详细信息,请参阅“Improving .NET Application Performance and Scalability”,网址为:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag/html/scalenethowto13.asp?frame=true

小结

要完全实现智能客户端应用程序的潜能,您需要在应用程序的设计阶段仔细考虑性能问题。通过在早期阶段解决这些性能问题,您可以在应用程序设计过程中控制成本,并减小在开发周期的后期陷入性能问题的可能性。

本章分析了许多不同的技术,您可以在规划和设计智能客户端应用程序时使用这些技术,以确保优化它们的性能。本章还考察了您可以用来确定智能客户端应用程序内的性能问题的一些工具和技术。

参考资料

有关详细信息,请参阅以下内容:

http://msdn.microsoft.com/perf
http://www.windowsforms.net/Default.aspx
http://msdn.microsoft.com/vstudio/using/understand/perf/
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetcomp/html/netcfimproveformloadperf.asp
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dndotnet/html/highperfmanagedapps.asp
http://msdn.microsoft.com/msdnmag/issues/02/08/AdvancedBasics/default.aspx
http://msdn.microsoft.com/library/default.asp?url=/msdnmag/issues/04/01/NET/toc.asp?frame=true
http://msdn.microsoft.com/library/default.asp?url=/msdnmag/issues/03/02/Multithreading/toc.asp?frame=true

 

转载于:https://www.cnblogs.com/hanchan/archive/2007/12/10/989643.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值