代码改变世界

用 ASP.NET 2.0 改进的 ViewState 加快网站速度

2007-10-23 10:41  Jacky_Xu  阅读(594)  评论(0编辑  收藏  举报

如果您是个经验丰富的 ASP.NET 开发人员,一提起 ViewState ,您可能会不寒而栗,因为您想到的是大量通过“鸡尾酒吸管”吸入的 Base64 编码数据。除非采取步骤进行预防,否则大部分 ASP.NET 页面将有大量辅助数据被存储在一个名为 __VIEWSTATE 的隐藏字段中,多数情况下,甚至不需要这个字段。浏览用 ASP.NET 生成的您喜爱的站点,查看页面源代码,计算隐藏在 __VIEWSTATE 字段中的字符数。我尝试了一下,数量为 800 到 7,800 个字符。

当然, ViewState 在 ASP.NET 中有个重要的角色。如果使用恰当,它能够简化页面开发,改进用户与站点的交互。如果置之不理,它能够显著增加站点响应大小,在连接速度慢的情况下,使您的响应时间更加缓慢。ASP.NET 2.0 的发布带来了 ViewState 机制的一些改进,这使得 ViewState 使用更简单,又不会防碍站点性能。这些改进包括:减少编码数量,采用控件状态从内容中分离出行为状态,以及智能集成数据绑定控件。

ViewState 基本原理


在介绍 ASP.NET 2.0 ViewState 的改进之前,简要总结目前版本中 ViewState 的用途和实现是适宜的。 ViewState 为 ASP.NET 开发人员解决了一个特定问题 — 保留服务器端不形成元素的控件的状态。这很重要,因为 ASP.NET 中的大部分服务器端控件模型是根据这样一个假设生成的,那就是 — 如果用户回发到相同页面,所有控件保持其状态不变。也就是说,如果在处理请求期间修改任何控件的内容,任何后续 POST 请求回到相同页面时,您可以依赖于那些仍然存在的修改。作为一个活动的 ViewState 示例,尝试运行图 1 显示的页面。

每次按下 Submit 按钮时,_sum 范围值递增。因为 ViewState 在请求期间保持以前的值,因此它将从上一次显示的值开始,显示 1、2、3、4 等等。如果想知道 ViewState 不可用时发生的事情,尝试添加 enableviewstate='false' 作为范围元素的一个属性。因为以前的范围值在请求处理时没有传播,所以不论页面发布多少次,都将显示值为 1。

在 ASP.NET 中, ViewState 完成基于控件的编程模型。如果没有 ViewState ,一些控件(如文本框和下拉列表)在 POST 请求期间保持状态,而其他控件不保持,使用这些状态各异的控件记录一些特殊的情况是令人沮丧的体验。使用 ViewState ,开发人员能够专注于编程模型和用户界面,而不用担心状态保持。还能对 ViewState 进行哈希或加密,以防止用户篡改或解码。关于这个主题的更多信息,请参见在线书籍 Securing Your ASP.NET Application and Web Services 的第 19 章。

使用 ViewState 的另一个重要之处是在控件中发布服务器端更改事件。如果用户改变了文本框中的值或切换了下拉列表中的选定元素,您就能够注册一个事件处理程序,引发事件时,执行代码。这些控件比较其当前值与以前值,如果有任何过程预订更改事件,以前值隐式存储在 ViewState 中。如果禁用一个控件的 ViewState ,而您正在处理该控件的更改通知事件,因为总是假定以前值与窗体默认值相同,所以更改通知事件不会正确激发。

ViewState 问题


正如我早前指出的,在 ASP.NET 1.x 中, ViewState 有很多问题。默认情况下,它是启用的,除非您知道在不需要使用时找到并禁用它,否则它能显著增加页面呈现的数据量。当使用数据绑定控件时,所有控件都使用 ViewState 保存回发后的状态,这将变得异常痛苦。作为一个简单而生动的示例,考虑图 2 所示的 ASP.NET 页面。

这个页面有一个 DataGrid 控件,该控件绑定对 pubs 数据库中 authors 表格进行简单查询的结果。如果运行这个页面(对连接字符串做出必要改正),查看 ViewState 字段,您可能吃惊地发现里面有超过 12,000 个字符。当查看页面内容,意识到显示浏览器中整个表格内容所需的实际字符数量大约是 1,600,您会更加吃惊。造成这个结果的原因之一是 ViewState 不仅编码数据,而且编码数据类型(元数据);同样,Base64 编码一般要增加大约 33 % 的空间系统开销。而且,大量的系统开销只是为了保留 POST 请求后的控件状态,这似乎与时间不成比例,必须竭尽全力避免发生。

很明显,如果您关心响应大小,那么决定何时禁用控件的 ViewState 是重要的。刚才的示例就是一个典型的情况,传播了 ViewState ,但是从未使用,这是优化站点的 ViewState 使用时建议采用的主要规则:如果每次请求页面时填充控件内容,禁用该控件的 ViewState 一般是安全(而明智)的。

另一方面,您可能决定利用 ViewState 保留控件状态这一实事,并且只在页面的首次 GET 请求时,填充控件内容(只是贯穿后续 POST 请求)。这省去了使用的任何后端数据源的往返行程。通过修改我的页面来利用这一结论,我更改了 OnLoad 方法,使它在填充 DataGrid 前检查 IsPostBack 标志,如图 3 所示。

当然,有一些其他可能更好的选择,例如缓存服务器的查询结果,每次发出请求时重新绑定控件。由您权衡状态存储的位置和特定体系结构与应用程序的重要性。

您可能注意到我曾小心提过,如果每次请求页面时填充控件内容,那么禁用 ViewState “一般”是安全的。例外情况是,一些控件既使用 ViewState 保留行为,也保留一般状态。如前所述,下拉列表和文本框控件使用 ViewState 存储以前的值来正确发布服务器上的更改通知事件。同样地,DataGrid 类使用 ViewState 发布分页、编辑和排序事件。遗憾的是,如果您想要使用 DataGrid 中诸如排序、分页或编辑的功能,则不能禁用其 ViewState 。对于尝试生成快速有效站点的开发人员来说,服务器端控件 ViewState 的非全有即全无的状况,是 ASP.NET 1.x 的服务器端控件模型另人沮丧的一面。

ASP.NET 2.0 中的 ViewState 改进


既然我概述了这些问题,是时候讨论 ASP.NET 2.0 中的所有改进了。第一个是 ViewState 序列化时的总大小。在 ASP.NET 1.x 中,两个字符串进入 ViewState 缓冲区中的序列化如下所示:

<p<l<string1;>;l<string2;>>;>;

ASP.NET 1.x 中使用的 ViewState 序列化格式是元组格式,由三个一组的层次集合和使用大于号和小于号的序列对组成。大于号之前的字母代表存储对象的类型(t=triplet,p=pair,i=integer,l=ArrayList,等)。大于号和小于号内的每个子元素由分号分隔。这是有趣的序列化格式,有点像一个压缩的 XML。但是,如果您关心空间,那么它不是最有效的序列化格式(只是比 XML 稍好一点)。

ASP.NET 2.0 改变了这种序列化格式。在 ASP.NET 2.0 中,相同的两个字符串进入 ViewState 缓冲区的序列化如以下代码行所示:

[][]string1[]string2

至少,这很像浏览器呈现的格式。方括号实际上是非打印字符,如果我们使用 Unicode 字符引用来重写,则变成了以下显示的编码:

&#15;&#5;string1&#5;string2

ASP.NET 2.0 使用一些非打印字符标识对象的起始并表述对象的类型,而非使用可打印字符集(“<”、“>”、“;”、“l”、“i”、“p”等)来分割 ViewState 流中的对象。

使用非打印字符分割 ViewState 中存储的对象有两个目的。第一个目的是改进解析 ViewState 字符串时进行词法分析的效率,因为此时不再需要匹配字符或解析标记。第二个,也是更重要的目的是它减少了为 ViewState 中的对象编码所使用的字符数量。在为两个字符串编码的简单示例中,第一种编码方法使用了 16 个分割字符,而 2.0 格式使用了 3 个。这种作用很快地复合而产生了重大的影响。

例如,如果我们在窗体上放置一个 DataGrid,将其绑定到 pubs 数据库的 authors 表格,如图 3所示,则 1.x 中的 ViewState 字符串大小是 12,648 个字符。如果在 2.0 中也这样做, ViewState 大小减少到 6,728 个字符,几乎减少了一半。图 4 显示 ASP.NET 1.x、ASP.NET 2.0 中 ViewState 所需的空间对比,以及不同输入大小在页面实际呈现的字符数。在这种情况下,authors 表格在 500 行处增加了额外的行,达到了 2,000 行。


图 4 ViewState 大小对比


随着 ASP.NET 2.0 版本的发布,您能够期望立即减小 ViewState 大小,而无需做任何特别的工作。这不意味着您应该停止考虑控件正常工作是否需要 ViewState ,这是因为它仍然对响应大小有很大影响,在图 4 中表现的很明显。然而, ViewState 的下一个改进能潜在地节省更多。

控件状态


我先前提过,ASP.NET 1.x 中使用服务器端控件最另人沮丧的一面是关于 ViewState 的非全有即全无的状态。控件的行为方面,如 DataGrid 中的分页或文本框中的选定更改通知,需要启用 ViewState 来正常运行。我确定在任何 DataGrid 中启用 ViewState 的前景令所有人十分沮丧。在 ASP.NET 2.0 中,Microsoft 通过将 ViewState 分割成两个独立不同的类别解决了这一特殊问题: ViewState 和控件状态。

控件状态是另一类隐藏的状态,专门为维护控件的核心行为功能而保留,而 ViewState 只包含维护控件内容 (UI) 的状态。从技术上来说,控件状态存储在与 ViewState 相同的隐藏字段中(只是在 ViewState 层次结构末端的另一个叶子节点),但是如果禁用一个特定控件或整个页面的 ViewState ,控件状态仍然传播。实现这一技术好的一面是,现在我们能够改进 ASP.NET 2.0 优化 ViewState 的原始原则,使之更强健:如果在每次请求页面时传播控件内容,您应该禁用该控件的 ViewState 。

只要控件生成器将其状态正确划分成行为状态(为了维护核心功能)和 UI 状态(为了保留内容),就应该坚定地遵守这个原则。我建议您现在使用 ASP.NET 2.0 时开始遵循这个原则,但是 DataGrid 还未重构以保留控件状态中的行为状态。幸运的是,新的 GridView 是 DataGrid 的逻辑继承者,它像 TextBox 和 DropDownList 控件的新版本一样正确使用控件状态。图 5 显示的是目前使用控件状态的控件列表及其存储的属性,供您参考。随着 ASP.NET 2.0 的最终发布,这个列表可能会作出变动。

如果您愿意进一步研究 ASP.NET 2.0 的新控件如何使用控件状态和 ViewState ,可以使用我编写的一个叫做 View State Decoder 的实用工具,它用来显示给定页面上所有的 ViewState 和控件状态。图 6 显示的是活动的实用工具的一个屏幕快照。您能够从本文顶端的链接下载该程序。注意,这个应用程序依赖于目前 ViewState 使用的编码字符,在最终版本发布前可能会改变。

对于您的那些生成控件,控件状态的使用模型不像 ViewState 一样方便。您必须重写 LoadControlState 和 SaveControlState 虚方法,手动管理您那部分映射到控件状态中的对象集合,而非提供一个带有索引的状态包用来插入和移除选项。另一件您必须做的事情是在初始化期间调用 Page.RegisterRequiresControlState 方法,使页面能够在正确的时间询问控件状态。在控件状态中存储选项比在 ViewState 中更难可能是一件好事,因为用户无法禁用它(控件状态有一个选择模型也有性能方面的好处)。开发人员在存储任何状态前应该认真考虑,因为它总是添加到呈现的页面中。图 7 显示在控件状态中存储一个字符串的自定义控件(本例中是它的颜色)。

当您决定控件状态和 ViewState 各存储什么内容时,请记住行为状态与内容或者 UI 状态应该有分别。诸如属性和数据集一样的内容一般应存储在 ViewState 中,而不是迁移到控件状态。触发服务器端事件的状态是存储在控件状态中最典型的一类状态。

声明性数据源和 ViewState


应用我们的 ViewState 优化原则有一个小问题 — 您经常不知道每次请求时是否正在填充控件内容。在 ASP.NET 2.0 中采用声明性数据源意味着将数据绑定到一个控件不再需要将数据源属性显式连接至 DataReader 或 DataSet,并调用 DataBind。而是在页面上声明性地放置一个数据源,并将控件指向该数据源。例如,使用绑定到 pubs 数据库中 authors 表格的新 GridView 类和与之相关的 SqlDataSource(参见 图 8)。如果运行这个页面,您将发现它正好有效。GridView 及其相应的数据源能够了解何时进行交互。在页面呈现之前,GridView 充满数据源的数据,并且很可能在客户端呈现整个 authors 表格。

在这个简单的情况中,我们还未禁用 GridView 的 ViewState ,因此您可能会认为,我们再一次仅仅使用 ViewState 存储了从未使用的数据。幸运的是,ASP.NET 2.0 引擎作了正确的事情,当控件上的 ViewState 可用时,它将不厌其烦地返回到数据库。

同样地,如果禁用 GridView 上的 ViewState ,数据绑定到数据源将发生在每次请求发生的时候,包括 POST back 请求。DataBoundControl 基类中置入了这一功能,其中的 AdRotator、BulletedList、CheckBoxList、DropDownList、ListBox、RadioButtonList、GridView、DetailsView 和 FormView 控件继承了该功能。这些控件展示了 ViewState 在绑定到声明性数据源时表现的智能使用。

小结


ASP.NET 的下一个版本对 Web 开发人员承诺了许多改进,不只是更有可能晚上好好休息,不会做 Base64 编码数据淹没了 Web 窗体的噩梦。使用更紧凑的序列化格式、行为状态和 UI 状态的分离以及数据绑定控件和声明性数据源的智能交互,那些 ViewState 的噩梦仅可能变成美梦。

客观看待ViewState对ASP.NET程序作用和影响

前段时间由于程序出现了比较大的性能问题,视图(View)之间的跳转速度非常慢。通过Fiddler调试和分析,查找到是由于在视图(View)转换(PostBack)过程中,客户端给服务器端的发送字节数非常大,一般在30K以上,就相当于客户端每次都要给服务器上传大十K的数据量,这如果是比较好的网络环境下完全是可以忽略的,但是目前的网络环境确实还达不到这样的要求。详细请看《无刷新视图跳转的局限性》。针对这一情况,我的解决方案就是禁用页面的ViewState,只有这样才是最根本的解决办法。原本还想写一篇blog来好好批一下ViewState,当初想好的标题是“asp.net程序的性能杀手----ViewState”。现在看来,还好没写,要不还不被人批是“没有真正会用 asp.net 的人”?(尽管确实还没有真正全面认识asp.net)。

jillzhang的blog《给页面减减肥!》中给页面减肥的办法是对页面进行压缩。这确实是一种办法,特别是当在硬件环境允许的条件下,可以带来非常大的好处,一般体积都可以减小好几倍。减小页面体积还有一种办法,那就是禁用ViewState,两种方法并不是互斥的,而且我认为只有禁用ViewState后,页面压缩的效果才更明显的。因为ViewState的值本身就是一些相对紧凑的字符,而HTML代码则相对松散,(我也不是特别肯定这对压缩有必然的联系。)。最近一直在从事页面速度的优化方面的工作,所以很多平常不注意的细节,它所造成的性能影响在这时候就体现出来了。一般的页面(服务器控件比较多)如果禁用ViewState后,它的体积至少会减小一半。而且这一半的数据在很多情况都是没用的(特别是在不需要PostBack的情况下,简直就是累赘),如果这时候再加HTML压缩的话,那压缩比就不止3-5倍了。有一个页面正常的大小(禁用ViewState后)是101,730 byte ,压缩后变成了11,182 byte。说实话我也很惊讶这样的压缩比。通过这里可以看这组惊人数据。

那这一切是不是都是Asp.net的错呢?ViewState是不是就是“万恶之源”呢?是,也不是。为什么呢?首先我们要正确认识ViewState存在的意义,更多的情况下MS是为我们这些新手快速入门而考虑的。正因为有了ViewState,让我们开发B/S应用程序能够按照我们的正常的思维逻辑来进行。而屏蔽了在PostBack时,还要去初始化一堆的页面控件,给这个控件还原我们提交的请求值等等,想想这对于我们来说是多少复杂而麻烦的一项工作啊!而不是像我们现在这样,直接在PostBack事件取我们想要的控件的值这么简单。而默认情况下ViewState=true,也是在为初学者着想的,不至于让一个初学asp.net的同学在写postback事件时出现一些奇怪的错误而灰心丧气,提高门坎。一段个人的理解可能还不能让一些朋友看得很明白,关于ViewState的讨论已经很多了,但是最重要一点就是理解页面的执行生命周期。如果把下面这张图啃下去后你也许就会有深刻理解了。

 ASP.NET+Page+Life+Cycle(图大占篇幅。)

谈点有意义的吧?是禁还是用?决定因素有以下几点(个人理解):

1.你的目标应用环境。

这是最根本的,如果你的asp.net应用程序只在局域网(Intertrant)内应用的话,那非常棒,我们完全可以忽略ViewState存在的影响。

2.页面的性质。

如果你的页面是一种信息浏览的性质,而完全没有PostBack事件的话。这里的ViewState就完全是可以被消灭的。反之,如果页面中有PostBack事件,尽管只有一个,那你如果禁用了ViewState,都有可能产生不可预期的错误。

3.你对ViewState和页面事件的理解程度。

如果你很理解页面的生命周期和执行过程,那你完全可以根据需要来设置哪些控件需要打开ViewState,哪些控件可以禁用ViewState,做到按需使用,合理使用ViewState。达到性能的最优化。

4.开发人员的勤劳程度和外在因素。

如果你很勤劳,而且你也了解了ViewState的原理,你可以按需使用。但是如果你很懒,而且很多外在因素(团队其它成员的理解程度)你没无法控制的话,那就直接禁用页面的ViewState好了。当然前提是你必须知道如果去正确处理禁用ViewState后遗留下的问题,这些问题一般都是一些让人难以捉摸的东西。如果你都是一一的解决了这些问题的话,那对ViewState的感情就更深了。呵呵。

可以这么说ViewState是页面控件状态的一个副本,比如一个DropDownList控件,它在asp.net页面上要是以select HTML tag 来展现的,而这时在ViewState中还保存着它所有Item的副本。当我们在postback的时候为什么能够取到值呢?就是因为ViewState,它会在ProcessPostData(before Load)之前将这个副本还原成了DropDownList的Item了。然后在ProcessPostData方法中将表单提交的选择项设置为DropDownList的SelectedValue。以前这一过程我们无需参与。而当我们禁用ViewState后,我们就要手动去维护DropDownList回发情况下的Item初始化,利用Request.Params(或Request.Form)取得SelectedValue值等等,而这些工作有可能就要在Page_Load事件之前做了。

对于ViewState,有一句话说的很好,“鱼与熊掌不可兼得”,欢迎讨论。

补充:可能有以下一些因素让你会选择坚定不移的使用ViewState

1.你认为ViewState所以来的负面影响在我所能接受的范围之内。

2.通过对ViewState的一些处理,如改变ViewState的保存机制,压缩ViewState等,我们能够得到比较好的结果。

3.由于一些客户原因,如第三控件需要,设计需要必须使用ViewState的。

4.禁用ViewState带来更多的工作量,但是改善效果不理想。

等等,但是有一个原则,既要合理使用,也要理性判断,做到效果最优化。