我們經常抱怨首次調用WebService比較慢,通常的做法是在程式啟動的時候,後台逐一調用一遍所有的WebService,還有人利用多線程來解決這個問題。其實,大家只是看到了問題的現象以及“工程”的解決辦法,而沒有接觸到問題的本質。經過本人反編譯.Net類庫,逐步尋找,應該說找到瞭解決這個問題的根本辦法。我們都知道,WebService是通過Soap協議來傳遞訊息的,所有的訊息都是XML,而在用戶端和伺服器端,都是使用的對象,這其中必然有一個XML和對象之間的轉換,這個轉換就是慢的罪魁禍首。下面,我就逐步分析,希望對你能有協助,請耐心看完。
1. 添加Web引用的時候,WebService在用戶端有一個代理,如下:
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "2.0.50727.42")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] [System.Web.Services.WebServiceBindingAttribute(Name="WebService1Soap", Namespace="http://tempuri.org/") public partial class WebService1 : System.Web.Services.Protocols.SoapHttpClientProtocol 用戶端調用WebServivce就是通過這個代理類來調用的。 2. 調用WebService方法,用戶端和伺服器端通訊是Xml,所以代理類跟Xml之間就有序列化和還原序列化的過程3. 用戶端調用WebService的過程如下a) 用戶端調用代理類Hello world方法string str = (new Service2.WebService1()).HelloWorld ();b) 代理類調用基類SoapHttpClientProtocal的Invoke方法 [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://tempuri.org/HelloWorld0766", RequestNamespace="http://tempuri.org/", ResponseNamespace="http://tempuri.org/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] public string HelloWorld() { object[] results = this.Invoke("HelloWorld", new object[0]); return ((string)(results[0])); }c) SoapHttpClientProtocal進行Soap序列化Soap頭和方法,都是這個類自己做的,但是輸入參數和傳回值,是利用的XmlSerializer,輸入參數要序列化,傳回值要還原序列化。 protected object[] Invoke(string methodName, object[] parameters) { … try { message1.SetStream(stream1); this.Serialize(message1);//注1 } … response1 = this.GetWebResponse(request1); Stream stream2 = null; try { stream2 = response1.GetResponseStream(); objArray1 = this.ReadResponse(message1, response1, stream2, false);//注2 } } 注1:this.Serialize中有一句參數序列化的代碼如下 method1.parameterSerializer.Serialize(writer1, message.GetParameterValues(), null, flag1 ? text2 : null); 注2:this.ReadResponse中有一句傳回值的還原序列化的代碼如下 message.SetParameterValues((object[]) method1.returnSerializer.Deserialize(reader1, flag1 ? text1 : null));d) XmlSerializer會緩衝暫存程序集,這個程式集作用是序列化和還原序列化,如果緩衝中沒有會調用TempAssembly產生一個 Static的緩衝(就是我們每次調用慢的罪魁禍首):private static TempAssemblyCache cache;擷取緩衝中的程式集:this.tempAssembly = XmlSerializer.cache[defaultNamespace, type];緩衝中沒有就去載入:Assembly assembly1 = TempAssembly.LoadGeneratedAssembly(type, defaultNamespace, out implementation1);載入沒有就去產生(會產生臨時檔案並編譯,很慢):this.tempAssembly = new TempAssembly(new XmlMapping[] { this.mapping }, assembly1, implementation1); e) TempAssemlby這個類負責載入以及產生暫存程序集在LoadGeneratedAssemlby方法中,有一段邏輯,就是預設去載入序列化類別,這個類的命名是規則如下 internal static string GetTempAssemblyName(AssemblyName parent, string ns) { return (parent.Name + ".XmlSerializers" + (((ns == null) || (ns.Length == 0)) ? "" : ("." + ns.GetHashCode()))); } 同時,如果載入失敗會觸發AppDomain.CurrentDomain.AssemblyResolve事件 4. 結論1) WebService的序列化是調用XmlSerializer 2) WebService慢,是因為產生序列化類別慢,所謂的臨時檔案都是XmlSerializer的中間代碼。可以在config檔案中加入如下的配置,臨時序列化的檔案就不會被刪除了,WinForm程式是*.exe.config,asp.net是web.config。 <configuration>
<system.diagnostics>
<switches>
<add name="XmlSerialization.Compilation" value="4"/>
</switches>
</system.diagnostics>
</configuration> 臨時檔案在C:/Documents and Settings/抹布/Local Settings/Temp下,注意,因為名稱是隨機的,序列化的dll檔案,並不能重用,重開進程會重建。 3) 如果自訂序列化類別,可以跳過產生臨時序列化的步驟,大大提高第一次載入的速度,也就是說,只要有一個程式集名稱+“.XmlSerializers”的序列化類別存在,就不會動態產生序列化程式集了。 4) 在代理類上可以加[System.Xml.Serialization.XmlSerializerAssemblyAttribute(AssemblyName = "TestPerformance.XmlSerializers")]指定Xml序列化的類,這個序列化的類可以通過一個工具產生,但是根據研究TempAssemlby的LoadGeneratedAssemlby代碼發現,這個Attribute可以不加的,只要你有一個GetTempAssemblyName傳回值一樣的名稱的序列化類別即可。 5) 根據載入失敗會觸發AppDomain.CurrentDomain.AssemblyResolve事件,可以在載入失敗後動態產生序列化類別,如下。 http://support.microsoft.com/kb/872800/zh-cn,請參考這個kb private void Form1_Load(object sender, EventArgs e) { AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(MyResolveEventHandler); } static Assembly MyResolveEventHandler(object sender, ResolveEventArgs args) { Assembly a = null; string[] arr = args.Name.Split(new string[] { "." }, StringSplitOptions.None); if (args.Name.IndexOf("XmlSerializers") >= 0) { if (!System.IO.File.Exists(args.Name + ".dll")) PreGenNS.Pregen.Generate(new string[] { arr[0] }); string sSerializersDLL = args.Name + ".dll"; string smartDeploymentHostLocation = ""; a = Assembly.LoadFrom(smartDeploymentHostLocation + sSerializersDLL); } return a; }6)VS2005利用Release編譯,會產生AssemblyName+"XmlSerializer.dll"的序列化檔案,可以隨著用戶端一起部署,跟5這種方式不太一樣,可以根據實際情況來選擇。利用5這種方式,是第一次調用WebService時,動態產生序列化類別;而6是在軟體發布時,產生這個類,並部署到用戶端。