Windows Mobile 进阶系列.多窗体应用的性能与编程调试

 相关文章
第零回.序和属性
第一回.真的了解.NET CF吗?
第二回.初窥CF类型加载器
第三回.让.NET CF CLR有条不紊


第四回
. 多窗体应用的性能与编程调试

摘要:在资源有限的Windows Mobile移动设备上面,具有多窗体的应用程序的性能问题是值得我们去关注的。本文阐述了如何优化多窗体应用程序的性能,提高加载速度的方案以及在性能调试过程中常用的编程调试的技巧。
Keywords
Windows Mobile,多窗体,性能,调试,C#

有朋友说这个系列的前面几篇文章太理论了,不好懂。我很理解,不过我的确得把一些理论性的文章安排在前面,也请筒子们耐心,从现在开始后面的文章会比较轻松一点 J

1.      窗体加载性能

窗体的加载主要是在Formload事件处理函数里面完成的。所以,我们首先需要尽量使Form_Load里面的代码最小化。把那些负荷比较重的任务放到后台线程上完成,尽量不去阻塞和“耽误”加载窗体的线程。

熟悉.Net Framework的朋友应该知道,.NET Framework2.0以上的版本中引入了一个叫做BackgroundWorker的组件用来实现上述功能。把一些高负荷的工作放到BackgroundWorker组件中去执行,不对主线程造成阻塞。遗憾的是目前在.NET CF中并不支持这个组件,不过你仍然可以使用线程池(Threadpool)来实现类似的功能。
下面的示例,对使用和不使用线程池做同样的任务所用的时间做了比较。

我们先把一个比较重的任务放到主线程上去做:

        private void Form2_Load(object sender, EventArgs e)
       
{
//计时起点
            int start = Environment.TickCount;
            
int end = 0;
            
for (int num = 0; num < 1000; num++)
            
{
                
this.Text = num.ToString();
            }

//计时终点
            end = Environment.TickCount;
            Debug.WriteLine(
"Single thread list-populating time (ms) " + (end - start));
        }

所做的工作就是更新form的标题1000次。结果我们可以在output栏看到如下的输出:

可以看到,在Form2的加载过程中,在这样一个任务上(我们仅仅用来模拟某些必不可少的工作)花费了接近1秒钟(916ms)时间。而在这一秒钟时间内,用户是处于等待的状态。显然,这并不是一个好的方案,毕竟在加载Form的时间就让用户等那么久是不厚道的。
下面是使用Threadpool的一个解决方案:

     private delegate void AddDelegate(int num);

        
private void Form2_Load(object sender, EventArgs e)
        
{
            ThreadPool.QueueUserWorkItem(
new WaitCallback(HeavyWork));
        }


        
private void HeavyWork (object o)
        
{
//计时起点
            int start = Environment.TickCount;

            
int end = 0;

            
int count = 1000;

            
try
            
{
                
for (int i = 0; i < count; i++)
                
{
                    
if (this.InvokeRequired)
                    
{
                        
object[] args = new object[1];
                       args[
0= i;
                        
this.BeginInvoke(new AddDelegate(n => this.Text = n.ToString(); }),args);
                    }

                    
else
                    
{
                        
this.Text = i.ToString();
                   }

                }

            }

            
catch(Exception ex)
            
{
                Debug.Write(ex.ToString());
            }

//计时终点
            end = Environment.TickCount;
            Debug.WriteLine(
"Work-thread asynchronize list-populating time (ms) "+ (end - start));
        }



 线程池的某一条线程会去执行挂载在WaitCallback 委托上的“等待回调”的HeavyWork方法,并通过Control.BeginInvoke()方法以异步的方式更新Form的标题。我们在output栏中可以看到如下的输出


我们看到,这种利用多线程的异步方式仅花费了169ms,与前面的结果相比,节约了800ms时间。
注意:不同的设备,不同的调试环境所用的时间可能略有不同

当然,并不是非要启用线程的线程池的线程。你完全可以自己创建一条线程做一些额外的工作,或者直接使用AsyncCallBack委托,MSDNAsyncCallBack的例子很经典,值得大家看一下。不管使用什么方法,我们的目的就在于使用户能最快的看到他想看到的东西,不要让用户对着旋转的光标心叹。

2.      窗体控件与高效布局

在谈论这个话题之前,强烈建议大家先查看一下VS2005以上的版本中为WinForm生成的Form.Designer.cs文件,先找到#region Windows Form Designer generated code这里有一个InitializeComponent()方法,如下:

    private void InitializeComponent()
        
{
            
this.mainMenu1 = new System.Windows.Forms.MainMenu();
            
this.menuItem1 = new System.Windows.Forms.MenuItem();
            
this.menuItem2 = new System.Windows.Forms.MenuItem();
            
this.treeView1 = new System.Windows.Forms.TreeView();

            
this.SuspendLayout();

            
// 

            
// mainMenu1

            
// 

            
this.mainMenu1.MenuItems.Add(this.menuItem1);

            
this.mainMenu1.MenuItems.Add(this.menuItem2);

            
// 

            
// menuItem1

            
// 

            
this.menuItem1.Text = "ShowForm2";

            
this.menuItem1.Click += new System.EventHandler(this.menuItem1_Click);

            
// 

           
// menuItem2

            
// 

            
this.menuItem2.Text = "Exit";

            
// 

            
// treeView1

            
// 

            
this.treeView1.Location = new System.Drawing.Point(33);

            
this.treeView1.Name = "treeView1";

            
this.treeView1.Size = new System.Drawing.Size(234262);

            
this.treeView1.TabIndex = 1;

            
// 

            
// Form1

            
// 

            
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);

            
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;

            
this.AutoScroll = true;

            
this.ClientSize = new System.Drawing.Size(240268);

            
this.Controls.Add(this.treeView1);

            
this.Menu = this.mainMenu1;

            
this.Name = "Form1";

            
this.Text = "Form1";

            
this.ResumeLayout(false);

        }



创建控件和布局都好说,但是有两个特别的方法应当注意一个是在创建控件完毕之后的SuspendLayout()方法,另一个是在末尾的ResumeLayout()方法。这两个方法在.NET CF 2.0以后的版本中是被支持的。它们能够防止控件在添加子控件时创建多个布局事件。

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

当我们的Form对象被创建时,构造器调用InitializeComponent()方法。在这个方法中,当控件以创建完毕等待布局的时候,this.SuspendLayout()方法被调用,用于暂时挂起布局行为。然后当所有定位的工作完成之后,ResumeLayout()用于恢复布局行为。这样我们的布局引擎就不必每次单独的去处理每个控件的布局事件了,而是集中进行一次统一布局,这样可以减少方法调用的次数。末尾的ResumeLayout()带的布尔参数表示是否立即强制进行布局,ResumeLayout无参或设为true时表示立即执行布局函数,设为false时表示没有必要执行挂起的布局请求,因为,在这之前控件的布局已经完成。

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

在减少方法调用方面,还可以把这里的Control.Location Control.Size用到的两个方法(PointSize的构造函数)可以合并为一次这样的构造:

Control.Bounds = new System.Drawing.Rectangle(int x,int y,int width,int height)

上述技巧对控件较多的form(尽管不建议使用很多控件,毕竟是手持设备)尤为有效。

关于如何提高窗体加载的速度,这次Webcast里面也有所涉及,值得一看~

3.      使用异步的Update方式

你可以使用TreeView ,ListView和ComboBoxd等空间的BeginUpdate()EndUpdate()方法来获得更好的性能。BeginUpdate()方法用于停止控件的重绘(开始更新操作)EndUpdate()方法用于恢复重绘。以TreeView为例,你可能想要更新TreeView的某个节点下的内容(比如某个目录),你可以先调用TreeView的BeginUpdate()方法,然后进行更新操作。更新操作完毕之后,调用TreeView的EndUpdate()方法来刷新该控件。这样可以防止你的空间在执行更新造作的时候可能出现的白屏现象。这在节点比较多,TreeView不能立即迅速重绘的时候尤为有效。下面这个例子演示了如何利用这两个方法异步的读取目录树绑定到TreeView上并实现更新操作。这个例子写在Smartphone上面,因为我很久以前写过一个类似的小玩意儿,你可以在这篇blog里面找到,不过现在我们关注的不是它的功能本身了:

 void TestTreeViewLoad(bool async) 
        

            
// async表示是否是否使用BeginUpdate/EndUpdate
            if (async) 
          treeView1.BeginUpdate(); 

            treeView1.Nodes.Clear(); 

            
// 根节点
            TreeNode tn = new TreeNode(@""Windows"); 
            treeView1.Nodes.Add(tn); 
            DirectoryInfo rootDir 
= new DirectoryInfo(@""Windows"); 

            
//进行目录树的更新操作 
            UpdateTreeView(tn, rootDir); 
            treeView1.Nodes[
0].Expand(); 



            
if (async) 
               treeView1.EndUpdate(); 
        }
 


        
// 迭代更新Treeview 
        private void UpdateTreeView(TreeNode tn, DirectoryInfo dir) 
        

        
//显示文件夹名
            foreach (DirectoryInfo di in dir.GetDirectories()) 
            

                TreeNode tn2 
= new TreeNode(di.Name); 
                tn.Nodes.Add(tn2); 
                UpdateTreeView(tn2, di); 
            }
 


            
// 显示文件名 
            foreach (FileInfo fi in dir.GetFiles()) 
            

                TreeNode tn3 
= new TreeNode(fi.Name); 
                tn.Nodes.Add(tn3); 
            }
 
        }
 
        
private void menuItem1_Click(object sender, EventArgs e) 
        

            TestTreeViewLoad(
true); 
        }
 

在调用这个测试方法之前, 假设你的Treeview正在用来显示一个长长的目录树,现在由于某些需求你需要刷新或者重新绑定这个目录树。于是迭代调用了UpdateTreeView()来枚举指定根节点("Windows)下的所有文件和文件夹。这时,采用异步的刷新模式来避免响应多余的重绘事件。事实上,TreeView空间本身就用到了这个特性,当你调用TreeView.Nodes[i].AddRange()方法的时候,AddRange()方法会在内部调用BeginUpdate和EndUpdate()方法。不过.NET CF对异步操作的支持还很不完善(如对IAsynCallback的支持),期待将来的版本会有所改进。

4.      编程调试方法小结

本文和之前的3篇文章都是跟.NET CF程序性能相关的文章,作为本系列性能篇的最后一篇文章,有必要在这里介绍几个常用的编程调试程序性能的方法。前面提到的几点也许你都已经明白,但是有时候你可能还是会遇到莫名奇妙的性能问题,可能是你的算法出了毛病,也可能是你调用的方法背后发生了额外的JIT行为……诸如此类,这时候我们可能就要借助一些小的诊断技巧来帮忙了。尽管微软提供了Power Toys for .NET Compact Framework 3.5
其中有CF CLR Profiler等小工具,可以帮助我们详细的诊断程序的性能(可以参考这一次Webcast),但是更多情况下,我们需要的是一个简单快速的判断。

1).TickCount

就像前面的程序用到的,为了记录某个方法或者某段程序执行的时间,Environment.TickCount是一个传统的办法。一个TickCount区间的差值(精确到毫秒)可以用来指示程序执行的时间,例如:

     private void SomeMethodB()

     
{

       
int start = Environment.TickCount;

       
// 模拟一些费时的操作

       Thread.Sleep(
2000);

       
int end = Environment.TickCount;

       
int millis = end - start;

       MessageBox.Show(millis.ToString());

     }



示例程序如下:

效果如图所示:


2). Stopwatch

完整版的.NET Framework 2.0引入了一个叫Stopwatch的类(在System.Diagnostics命名空间下),用于获取高精度的本地时间。而在最新的.NET CF3.5中,Stopwatch终于得以以官方的形式在移动平台上实现。用法与上面的类似:

     private void SomeMethodA()

     
{

       Stopwatch sw 
= new Stopwatch();

       sw.Start();

       
// some long-running task

       Thread.Sleep(
2000);

       sw.Stop();

       
long millis = sw.ElapsedMilliseconds;

       MessageBox.Show(millis.ToString());

     }



不过需要注意的是并不是所有OEM都对这个高精度的计时器做了完全的实现,实际上Stopwatch是对QueryPerformanceCounter和 QueryPerformanceFrequency这两个API的封装,如果你的硬件不支持高频计数器QueryPerformanceFrequency会返回1000,即毫秒级的精度,这就跟使用Environment.TickCount是一样的了。

3). 内存开销

如何编程获得更确切一点数据比如内存开销呢? 下面这行代码演示了如何获取到目前为止,系统为你的应用程序分配内存的字节数。

     long bytesInUseByManagedObjects = GC.GetTotalMemory(false);
如图:

要想获得进一步的信息,比如系统物理内存和虚拟内存各自使用了多少,你可以使用P/invoke的方法调用GlobalMemoryStatus 结构,如下:

     private void ShowMemory()

     
{

       MemoryStatus ms 
= new MemoryStatus();

       GlobalMemoryStatus(ms);

//单位换做kB显示

       
string result =

         
"Memory Load % = " + ms.MemoryLoad +

         
""r"nTotal Physical (KB) = " + ms.TotalPhysical / 1024 +

         
""r"nAvailable Physical (KB) = " + ms.AvailPhysical / 1024 +

         
""r"nTotal Virtual = (KB) " + ms.TotalVirtual / 1024+

         
""r"nAvailable Virtual = (KB) " + ms.AvailVirtual / 1024;

       MessageBox.Show(result);

     }


     [DllImport(
"coredll.dll")]

     
public static extern void GlobalMemoryStatus(MemoryStatus lpBuffer);

     
public class MemoryStatus

     
{

       
public int Length;

       
public int MemoryLoad;

       
public int TotalPhysical;

       
public int AvailPhysical;

       
public int TotalPageFile;

       
public int AvailPageFile;

       
public int TotalVirtual;

       
public int AvailVirtual;

       
public MemoryStatus()

       
{

         Length 
= Marshal.SizeOf(this);

       }


     }


效果:


总结

基于.NET CFWindows forms应用程序其设计目标之一就是减少窗体加载的时间。在Form的初始化函数(通常是InitializeComponent(),你可以在FormDesigner.cs中找到它)中,要尽量减少调用方法的数量。另外,尽量不要在FormLoad()或者Show()的方法中进行一些读取数据的操作,那些必要的数据可以在最开始,或者集中的某个操作中完成,或者采用异步的方式在后台完成。总之,窗体和控件的加载性能直接影响到用户的体验,我们的目标就是尽量不要浪费用户的时间。
对于程序性能的调试,首先要确保每一步都尽量按原则来,比如使用尽量少的控件,尽量使用真机调试。上述提到的一些小技巧都应该了解,对于复杂难解的程序还是推荐使用Power Toys这样的小工具进行详细的分析。
文中示例程序在此处下载

Regards
©Freesc Huang
  黄季冬<fox23>@HUST
  2008.03.24

posted on 2008-03-26 09:40  J.D Huang  阅读(5273)  评论(8编辑  收藏  举报