我正在优化 WinForms 应用程序的启动。我发现的一个问题是启动屏幕表单的加载。大约需要半秒到一秒。
我知道多线程是 UI 部分的禁忌,但是,看到启动屏幕是应用程序的一个相当自主的部分,是否有可能通过将它扔到另一个线程来以某种方式减轻其性能损失(也许在Chrome 的方式),这样应用程序的重要部分才能真正开始运行。
我正在优化 WinForms 应用程序的启动。我发现的一个问题是启动屏幕表单的加载。大约需要半秒到一秒。
我知道多线程是 UI 部分的禁忌,但是,看到启动屏幕是应用程序的一个相当自主的部分,是否有可能通过将它扔到另一个线程来以某种方式减轻其性能损失(也许在Chrome 的方式),这样应用程序的重要部分才能真正开始运行。
.NET 框架已经对 Windows 窗体应用程序中的启动屏幕提供了很好的支持。检查此线程以获取代码示例。它确实针对热启动时间进行了优化,它确保在初始化主应用程序之前启动并运行启动线程和屏幕。
如果您的目标是尽快启动启动画面,那么生成线程不会有任何收获。
有几种方法可以做启动画面,这里提到了一种更复杂的方法,但这是我使用过的一种简单的方法并取得了圆满成功:
只需确保您首先加载并显示初始表单,然后在用户查看漂亮的初始屏幕时继续加载您的应用程序。当主窗体完成加载后,它可以在它出现之前关闭启动画面(一种简单的方法是在其构造函数中将启动画面传递给主窗体):
static void Main()
{
Application.SetCompatibleTextRenderingDefault(false);
SplashForm splash = new SplashForm();
splash.Show();
splash.Refresh(); // make sure the splash draws itself properly
Application.EnableVisualStyles();
Application.Run(new MainForm(splash));
}
public partial class MainForm : Form
{
SplashForm _splash;
public MainForm(SplashForm splash)
{
_splash = splash;
InitializeComponent();
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
// or do all expensive loading here (or in the constructor if you prefer)
_splash.Close();
}
}
替代方案:如果您不想将启动画面传递给 MainForm(可能看起来不优雅),那么订阅 MainForm 的 Load 事件,并在此处关闭启动画面:
static class Program
{
static SplashForm _splash;
[STAThread]
static void Main()
{
Application.SetCompatibleTextRenderingDefault(false);
_splash = new SplashForm();
_splash.Show();
_splash.Refresh();
Application.EnableVisualStyles();
MainForm mainForm = new MainForm();
mainForm.Load += new EventHandler(mainForm_Load);
Application.Run(mainForm);
}
static void mainForm_Load(object sender, EventArgs e)
{
_splash.Dispose();
}
}
如该线程中所述,此解决方案的潜在缺点是用户将无法与启动屏幕进行交互。但是,这通常不是必需的。
是的。
您需要创建一个新的 STA 线程,使用 显示初始屏幕Application.Run
,然后在主窗体准备好后调用Close
using Invoke
(在主线程上)。
编辑:例如:
static SplashForm splash;
Thread splashThread = new Thread(delegate() {
splash = new SplashForm();
Application.Run(splash); //Blocking call on separate thread
});
splashThread.SetApartmentState(ApartmentState.STA)
splashThread.Start();
LoadApp();
//In MainForm_Shown:
splash.BeginInvoke(new Action(splash.Close));
为了获得最佳性能,您应该让您的Main
方法显示启动屏幕,然后调用一个单独的方法来加载应用程序。这样,所有程序集都将在显示初始屏幕后加载。(当你调用一个方法时,JITter 会在方法开始执行之前加载它使用的所有类型)
只要所有 UI 都保持在一个线程上,WinForms 中的多线程就可以了。
这就是通常的启动画面的完成方式。重要的工作在后台线程上完成,而启动屏幕窗口显示在 UI 线程上,让用户知道程序的其余部分将很快出现。
在重要的事情发生后,引发一个事件让 UI 线程知道是时候隐藏启动画面了(只需记住使用 Invoke() 将事件处理程序编组回 UI 线程以关闭启动画面屏幕)。
答案实际上与感知有关。有多种方法,NGEN 一个程序集,将东西放入 GAC,但您应该了解的是真正发生了什么。
C# 需要时间来加载虚拟机、加载引用的程序集,并根据启动屏幕上的内容加载程序集。然后从“冷”启动到启动第一个屏幕仍然需要一段时间。
这是因为当您第一次访问屏幕时正在进行 JIT 编译。
我使用的一种策略是传统的轻量级启动页面,它加载速度很快,因此可见,但在后台我生成了一个线程并加载了一个不可见的表单。此表单包含我打算使用的所有控件,因此 JIT 编译器正在做它的事情并加载程序集。这提供了轻微手响应的错觉。从启动 + 可见启动页面 + 暂停 + 单击第一个选项所需的时间大于线程执行然后清理和卸载表单所需的时间。
否则,应用程序在首次启动时对用户来说显得笨重且缓慢。屏幕的热启动要快得多,因为程序集和 JIT 已经完成。
我们通过提供一个用作启动屏幕的小型原生 C++ 应用程序来做到这一点。
然后它遵循这个过程:
C++ 应用程序也有一个超时时间(以防 C# 应用程序崩溃),如果在 30 秒内没有收到退出通知,它会自动退出。
这种方法的缺点在于您无法将应用程序固定到任务栏。如果您固定 C++ 应用程序(它是最终用户的主应用程序),您会在任务栏上看到另一个任务,因为 C# 应用程序是不同的。我认为,我可以通过在 C++ 和 C# 应用程序的应用程序清单中提供设置来指示它们在任务栏方面是“相同”的应用程序来解决这个问题。