业务需求:
一个测试工具系统,在原有对静态HTML测试的基础上增加对动态HTML进行验证,验证的是HTML标签的完整性。
测试对象:
一套内部办公系统,大量使用了AngularJs,除去登录地址外,页面中几乎所有的功能按钮,菜单,链接均是由AngularJs完成。
使用技术:
WPF+WebBrowser组件+多线程
经验总结:
- 开发中遇到问题绝大部分都能在google中搜索到解决方案,访问google方法一是翻墙,二是通过http://www.baigoogledu.com/
- http://stackoverflow.com/是个好地方,Google上搜索到的资料都是出自此处。
- 在开发中主要是围绕WebBrowser组件进行,多线程最后使用了System.Window.Forms.Timer实现,而其他方式遇到了跨线程访问WebBrowser组件的问题,尤其是工具自动点击A Tag(调用AngularJs代码)时,会停止响应(卡死,或者说一直block)
- 大部分跨线程访问UI组件的问题,都可以使用Control.Invoke(new Action(()=>{ 业务代码}));这样方式解决。
- WebBrowser组件只有在调用Navigate(url)之后,才会触发DocumentCompleted事件,多线程的业务调用中,也只有在DocumentCompleted事件中进行业务处理,才能得到WebBrowser组件属性值和Document文档结构,从而得到HTML代码。
- 静态页面可以从MainURL开始不断的抽取里面的各种链接(URL,Js事件)然后或者Navigate或者InvokeScript调用JS,获取HTML代码进行完整性验证,最后进行子节点的抽取。
- 动态页面也要冲MainURL开始,不过抽取到的可能是各种含有js的A Tag,而这些js(例如AngularJs)及其有可能在InvokeScript调用后无反应,这是需要在Document中遍历所需要执行的A Tag,转换为HtmlElement对象,在调用Click方法,这样就实现了程序自动点击链接,动态生成HTML,抽取,验证。
- WebBrowser Session是共享的,在同一个进程中无论是Control.WebBrowser 还是Forms.WebBrowser,无论是一个还是多个,他们之间默认是共享Session的,这就为登陆一次,多次抽取创建了便利
- AngularJs,jQuery中类似于jQuery(document).ready(function($) {
$("#content").load("AElementList.html");
}); 这样的函数是与WebBrowser.DocumentCompleted同时发生的,所以这就需在WebBrowser.DocumentCompleted事件中加入 延时等待功能,对ReadyState和IsBusy属性的检测,判断HTML页面动态加载完成。
补充
- WebBrowser Control存在内存泄露的Bug查了许多资料,这篇资料较全
http://stackoverflow.com/questions/8302933/how-to-get-around-the-memory-leak-in-the-net-webbrowser-control/
我准备了,三个解决方案,目前使用了第一个,后面两个没有尝试。
Solution1:
class MemoryHelper { [DllImport("KERNEL32.DLL", EntryPoint = "SetProcessWorkingSetSize", SetLastError = true, CallingConvention = CallingConvention.StdCall)] internal static extern bool SetProcessWorkingSetSize( IntPtr pProcess, int dwMinimumWorkingSetSize, int dwMaximumWorkingSetSize ); [DllImport("KERNEL32.DLL", EntryPoint = "GetCurrentProcess", SetLastError = true, CallingConvention = CallingConvention.StdCall)] internal static extern IntPtr GetCurrentProcess( ); public static void ReleaseMemory( ) { SetProcessWorkingSetSize(GetCurrentProcess(), -1, -1); } } //在每个线程中 MemoryHelper.ReleaseMemory(); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
Solution2:
try { if (webBrowser.Url.Equals("about:blank")) //first visit { webBrowser.Navigate(new Uri("http://url")); } else { webBrowser.Refresh(WebBrowserRefreshOption.Completely); } } catch (System.UriFormatException) { return; } System.GC.Collect(); // may be omitted, Windows can do this automatically
Solution3:
I have been looking for a solution for this for ages, and finally found one. I am using Delphi so the syntax for others may be slightly different The code I used is: (web1.Document as IPersistStreamInit).InitNew; where web1 is a tWebbroser component. Hope this helps!
补充2
上述Solutions经过测试,都不能解决问题,Solution1可以起到缓解内存泄露的问题,但不能从根本上解决问题。
不断的Google这个问题,结论是WebBrowser Leak Memory是控件本身的一个bug,而且是一个存在了很多年的一个Bug
现在就剩下曲线救国的解决方案了:
- 使用第三方Web Browser控件替换.Net WebBrowser Control,看这里
- 放弃目前的多线程结构,使用进程间通讯IPC,在进程A中启动进程B,WebBrowser工作进程B主要处理抽取页面代码,进程A主要负责展示,调度进程进程B,尤其是在进程B内存超过1GB后,重启进程B
补充3
再将程序在AnyCPU模式下进行bulid后,放到Win64系统进行运行(内存4G),虽然内存泄露依然但已经不行崩溃,线程的内存使用量达到2GB。估计换成8GB内存的系统中,效果会更好。但是更好的方法还是要用上面多进程的结构。