65.9K
CodeProject 正在变化。 阅读更多。
Home

应用程序加载更快的方法 – Windows Phone 配方

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2011 年 3 月 25 日

Ms-PL

17分钟阅读

viewsIcon

17426

应用程序加载方法

注意:以下文章最初作为“如何更快地加载应用程序”的 Windows Phone 制作指南的一部分发布,可在此处找到,该指南由我和 Yochay Kiriaty 为 Microsoft 撰写。

本文档的目的

本文档介绍了 Windows Phone Silverlight 应用程序的加载过程。它解释了应用程序开发人员在应用程序启动时常犯的错误,以及这些错误如何增加应用程序的总加载时间并损害用户体验。接下来,本文档将介绍一些克服长加载时间的可能解决方案并解释其实现方法。

应用程序启动也在 Windows Phone 应用程序中的性能注意事项中讨论过。该论文提供了出色的背景信息,并有助于让您的 Windows Phone 应用程序脱颖而出。

Windows Phone 应用程序流程:启动、运行和关闭

如果不解释 Windows Phone 应用程序生命周期事件,我们就无法谈论正确的加载技术和优化应用程序启动时间。以下部分提供简要概述;有关详细信息,请参阅Windows Phone 执行模型

Windows Phone 为开发人员提供了一系列事件和 API 来处理墓碑化和其他应用程序状态更改,包括启动和关闭。PhoneApplicationService 类位于 Microsoft.Phone.Shell 命名空间中,该类提供对应用程序生命周期的各个方面的访问,包括在应用程序变为活动或非活动状态时的应用程序状态管理。

PhoneApplicationService 类公开了四个主要的生命周期事件。对于任何自动生成的 Windows Phone 应用程序(由Visual Studio 2010 Express for Windows PhoneVisual Studio 2010 生成),App.xaml.cs 文件包含四个直接与执行模型相关的​​方法。这些方法是您应用程序对 PhoneApplicationService 类公开的生命周期事件的处理程序。

  • Application_Launching
  • Application_Activated
  • Application_Deactivated
  • Application_Closing

这些方法的名称不言自明。但是,有一些微妙之处需要我们覆盖。

  • 应用程序启动 – 当用户通过除按下“后退”按钮返回到先前应用程序以外的方式启动 Windows Phone 应用程序时,该应用程序被视为已启动。当用户点击手机应用程序列表中的应用程序条目或开始屏幕上的应用程序磁贴时,应用程序将被启动。当用户点击弹出通知时,应用程序也可以被启动。
  • 应用程序停用发生在不同的应用程序获得前台控制时;例如,当用户启动选择器或按下“开始”按钮时。在这两种情况下,您的应用程序都会被停用;会引发 Deactivated 事件,而不是在应用程序退出时引发 Closing 事件。但是,与关闭的应用程序不同,停用的应用程序很可能会被墓碑化。不要混淆:墓碑化应用程序的底层进程仍然会被终止,所以您的应用程序确实“关闭”了。但是,与已退出的已关闭应用程序不同,Windows Phone 操作系统会从内存中删除该应用程序的任何痕迹,而在停用的情况下,Windows Phone 操作系统会存储一个记录(一个墓碑)应用程序状态。基本上,Windows Phone 操作系统会保留应用程序的墓碑,该墓碑成为手机应用程序回溯堆栈的一部分,这是一个期刊,它允许使用“后退”按钮来增强导航功能。
  • 应用程序激活 – 在应用程序停用后,它可能永远不会被重新激活。但是,用户也可能通过反复按下“后退”按钮返回到应用程序。当用户返回到已停用的应用程序时,它会被重新激活并引发 Activated 事件。然后,Windows Phone 会导航到用户上次查看的页面,以使用户感觉应用程序只是在继续。请注意,与 Launching 不同,Activated 应用程序中的某些数据对象可能仍已初始化。
  • 应用程序关闭仅仅是用户反复按下“后退”按钮导航到应用程序中的页面,然后超越应用程序的第一页的结果。目前,这是用户退出应用程序的唯一方法。一旦您的应用程序关闭,它的进程就会终止,操作系统会从其内存中删除该应用程序的任何痕迹,并且除了启动新实例外,没有办法返回到该应用程序。

请注意,LaunchingActivated 事件是互斥的,DeactivatedClosing 事件也是如此。

这些事件在应用程序的加载和关闭时间中起着至关重要的作用。加载时间也适用于应用程序的墓碑化,更重要的是,适用于应用程序从墓碑恢复的情况。

在继续之前,还有最后一点说明。在启动和墓碑化期间会调用一系列方法。此方法序列对于优化加载时间操作至关重要。

在大多数情况下,当您的应用程序启动或从墓碑恢复时,将按以下顺序执行以下方法:

  1. 应用程序构造函数 – App.xaml.cs 中的 App()
  2. 应用程序启动 – App.xaml.cs 中的 Application_Launching
  3. 您的应用程序的“第一个页面”构造函数
  4. 您的应用程序的“第一个页面”OnNavigatedTo 方法

注意:从墓碑恢复时,“第一个页面”是应用程序正在显示的最后一个页面,而不是应用程序的实际第一个页面。

注意:当您的应用程序从墓碑状态恢复时,将执行整个序列。但是,在墓碑化被放宽的情况下,您的应用程序被停用但未被墓碑化,则不会执行应用程序构造函数和页面构造函数。有关应用程序生命周期事件和墓碑化的更多信息,请参阅执行模型 for Windows Phone

应用程序启动时间定义为从 Windows Phone 将应用程序加载到内存并开始执行应用程序构造函数(序列中的第一个方法)到第一个页面的 OnNavigatedTo 方法执行结束的时间。

启动和关闭时间

为了保持手机上内置应用程序和第三方应用程序之间一致且响应迅速的用户体验,Windows Phone 对应用程序的启动和关闭强制执行一系列时间限制。如果您的应用程序超过了允许的启动或关闭时间,它将被强制终止并从回溯堆栈中删除。这意味着您的应用程序不会被墓碑化,它只是被终止,并且用户无法使用后退按钮重新激活它。下表概述了启动关闭、停用和重新激活时间。

表 1-1. 应用程序启动/关闭时间

应用程序事件 限制

启动应用程序(应用程序启动事件)

10 秒

从应用程序向前导航(应用程序停用事件)

10 秒

使用“后退”键返回应用程序(应用程序激活事件)

10 秒

退出应用程序(应用程序关闭事件)

10 秒

您可以看到,您有长达 10 秒的实际时间(这并不意味着您有 10 秒的 100% CPU 时间)来处理每个应用程序生命周期事件。如果由于某种原因您的代码执行时间超过 10 秒,Windows Phone 系统将终止您的应用程序。

注意除了操作系统安全措施外,Marketplace 接收过程的一部分是检查启动和从墓碑恢复所需的时间。用户体验指南甚至更严格,要求在 10 秒内正确启动您的应用程序。根据Windows Phone 7 应用程序认证要求,应用程序必须在启动后五秒内渲染第一个屏幕,即使有启动画面。因此,优化和理解加载序列对您应用程序的成功至关重要。

  • 启动时间 – 包括启动或返回到已墓碑化的应用程序。这是从您的应用程序进程开始执行到您的应用程序变得活动或可见的时间。您的应用程序只有在应用程序导航到的页面的 OnNavigatedTo 方法执行结束后才会变得可见,无论是启动还是从墓碑恢复。在此之前,将显示应用程序的启动画面,并且应用程序不被视为活动状态。
  • 关闭时间 – 包括停用和退出应用程序,主要由从调用前向导航到 DeactivatedClosing 方法执行结束的时间组成。

演示问题

如上一节所述,应用程序必须在 10 秒内完成加载并显示其第一个页面(或在从墓碑恢复时显示任何页面),否则操作系统将终止该进程。此外,即使您的应用程序在五秒或八秒内启动,您仍然需要通过尽快加载应用程序并显示某些内容来优化用户体验;否则,不耐烦的用户可能会点击“后退”或“Windows”按钮导航离开您的应用程序。

那么,让我们看看创建行为良好的应用程序有多容易。在以下演示中,我们将创建一个 Windows Phone Silverlight 应用程序,该应用程序在加载时需要执行一些耗时操作。

注意我们的示例应用程序描述了一个非常简单的 Silverlight 应用程序场景,该应用程序在第一个页面上显示一些信息。这些信息应从某个 Web 服务或本地存储中获取。为了说明问题,我们创建了一个虚假的加载项,该加载项故意花费大约 10 秒才能完成。

为了模拟耗时操作,我们将简单地调用一个休眠一秒钟的函数,并重复此操作 10 次。(请参阅下面的 MyService.Init 方法。)

要亲身体验,只需按照以下步骤操作:

步骤 1:创建一个 Windows Phone 应用程序

步骤 2:创建耗时加载项

public class MyService
{
    private const int NumberOfIterations = 10;

    public static void Init()
    {
        for (int i = 0; i < NumberOfIterations; ++i)
        {
            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
    }

    public static void Init(Dispatcher dispatcher)
    {
        for (int i = 0; i < NumberOfIterations; ++i)
        {
            if (dispatcher != null)
            {
                dispatcher.BeginInvoke(() =>
                {
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                });
            }
        }
    }
}

本质上,这两种方法执行相同的操作:它们休眠约 10 秒钟以模拟耗时加载项。第二个 Init 方法接受一个 Dispatcher 作为输入参数,我们将其用于每次休眠期间。这会强制与主用户界面 (UI) 页面同步(因为我们传入的是 Dispatcher),因为编写一个在 10 秒内完全阻止 UI 线程的代码是不合理的。最终,调用 Dispatcher 可以更快地终止 Windows Phone,如您稍后将看到的。

步骤 3:从应用程序的各种位置调用耗时加载项

从五个方法中的任何一个或所有五个方法调用函数 MyService.Init

public partial class App : Application
{
    public App()
    {
        ...

        MyService.Init();
    }

    ...
}

当用户通过点击应用程序图标启动应用程序时,会引发此事件。

public partial class App : Application
{
    ...

    private void Application_Launching(object sender, LaunchingEventArgs e)
    {
        MyService.Init();
    }
}

当用户点击“后退”键返回应用程序,或在从选择器或启动器返回后,会引发此事件。

public partial class App : Application
{
    ...

    private void Application_Activated(object sender, ActivatedEventArgs e)
    {
        MyService.Init();
    }
}

当创建 Page 类时,会调用此函数。

public partial class MainPage : PhoneApplicationPage
{
    public MainPage()
    {
        InitializeComponent();
            
        MyService.Init(this.Dispatcher); //Or you can run MyService.Init();

    }

    ...
}

当导航到页面时,无论是从另一个页面导航还是在应用程序启动/激活时,都会调用此函数。

注意此演示中显示的问题也可能发生在您的应用程序从墓碑状态恢复时,因此您需要确保您的应用程序中的任何页面的构造函数和 OnNavigatedTo 函数中都没有大量工作。

public partial class MainPage : PhoneApplicationPage
{
    ...

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        MyService.Init(this.Dispatcher); //Or you can run MyService.Init();

    }
}
  1. App 构造函数
  2. 应用程序 Launching 事件
  3. 应用程序 Activated 事件
  4. 页面构造函数
  5. 页面 OnNavigatedTo 方法

步骤 4:运行您的应用程序

只需在手机上运行您的应用程序,并观察操作系统终止您的应用程序。当它终止时,您的应用程序取决于特定的实现标准,如下所述。

注意:要测试 Windows Phone 是否在您的应用程序加载时间超过 10 秒限制时终止您的应用程序,您必须在不附加调试器的情况下在真实设备上运行您的应用程序。您需要将应用程序部署到手机,然后运行它。不要从 Visual Studio 运行应用程序,因为当附加调试器时(无论是连接到您的 Windows Phone 设备还是连接到 Windows Phone 模拟器),相关的看门狗计时器都会被禁用,应用程序将在所有虚假加载项方法完成其任务后简单地加载。

如果您使用不带 Dispatcher 的 Init,那么(在主 UI 线程上)的休眠会阻止任何操作在主线程(即主应用程序线程)上执行。这相当极端,并且不代表真实场景。因此,我们在每次休眠期间添加了一个对 Dispatcher 的调用,以同步回 UI 线程,并让 Silverlight 运行时有机会执行其应有的操作(终止您的应用程序)。因此,您会注意到,当调用不带参数的 MyService.Init 方法时,应用程序最终会被终止,但仅在所有休眠期结束后(取决于您调用 MyService.Init 方法的次数)。当您将 Dispatcher 作为输入参数传递时(这仅在 MainPage 中可能),您会注意到您的应用程序终止得更快。

尝试从 Application 构造函数和 Launching 方法中删除 MyService.Init 方法,但将其保留在 MainPage 构造函数和 OnNavigatedTo 方法中。您会发现操作系统仍然需要大约 11 或 12 秒才能终止您的应用程序。这再次证明,只需要一个行为不当的方法就可以严重影响您应用程序的加载时间。

解决方案 – 优化应用程序加载时间

现在您已经了解了 Windows Phone Silverlight 应用程序的启动顺序以及长启动时间的问题,是时候提出一些优化应用程序加载时间的方法了。

该解决方案包含四项行动

  • 使用启动图像
  • 延迟工作
  • 在后台线程上执行工作
  • 使用加载页面

行动项目 1:使用启动图像

您可以做的第一件事是帮助解决用户体验问题。如果用户看到应用程序的图像并显示“正在加载…”,他会在因沮丧而按下“后退”键之前再等几秒钟。因此,要聪明地使用启动画面。

事实上,所有 Windows Phone 项目模板都包含一个启动画面图像。默认情况下,此图像名为 SplashScreenImage.jpg,在应用程序启动时自动显示。默认图像如下所示:

image

确保将默认图像更改为适合您应用程序主题的内容。这样,当用户看到启动画面(瞬间发生)时,他们就会将其识别为应用程序的一部分。更改默认启动屏幕也是 Marketplace 的要求。

注意:图像必须具有

  • 尺寸为 **480 x 800** 像素
  • 文件名设置为 SplashScreenImage.jpg
  • Build Action 设置为 Content

行动项目 2:延迟工作

这与您父母教您的一切都背道而驰,但当涉及到在 Windows Phone 上加载应用程序时,**永远不要现在就做你可以稍后做的事情**。

将非关键工作推迟到应用程序加载之后,这始终是一种好技术。那时,您不受时间限制的约束,并且可能有时间在用户操作之间运行加载项。

例如,假设您为 Windows Phone 编写了一个数独游戏。您可能会尝试在应用程序加载时生成数独游戏地图。但是,最好将此操作延迟到应用程序加载后用户选择“新游戏”。此时,您可以在游戏设置过程中显示一些漂亮的动画。

您应该延迟任何不属于应用程序启动关键且在应用程序加载时不会严重影响用户体验的加载项。始终优先进行惰性初始化对象,这有助于延迟非必要的加载项。

不幸的是,您的应用程序启动序列中的并非所有加载项都可以推迟。在某些情况下,您无法将加载项推迟到应用程序稍后时间或直到用户执行某些操作。某些场景要求您在尽可能接近应用程序启动时间的时间运行加载项。例如,考虑一个显示当前 Twitter 上热门趋势的应用程序。没有热门趋势信息,应用程序主页上没有什么实际内容可显示。但是,我们不能因为应用程序正在通过网络获取最新趋势而允许其被终止。

为了解决应用程序需要在启动时间接近时完成某些工作的场景,您需要一些或全部加载项在后台运行。这正是我们在下一节中解释的内容。

行动项目 3:在后台线程上执行工作

这可能是优化应用程序加载时间最重要的步骤。为了防止您的应用程序挂起或终止,启动方法序列必须尽快完成。如果您无法推迟某些加载项,则必须在后台执行该工作,也就是说,在后台线程上执行。

您可以使用以下选项:

using System.Threading;

Thread thread = new Thread(() => { MyService.Init(); });
thread.Start();

有关手动创建线程的更多信息,请参阅 Thread Class 文档。

using System.Threading;

ThreadPool.QueueUserWorkItem((o) => { MyService.Init(); });

有关使用线程池的更多信息,请参阅 ThreadPool Class 文档。

通过使用 BackgroundWorker 类,您可以分配一个执行加载项的委托,以及一个在委托完成其操作后将被执行的回调方法。

要使用 BackgroundWorker 类,您需要为 System.ComponentModel 添加一个 using 语句。

using System.ComponentModel;

BackgroundWorker backgroundWorker = new BackgroundWorker(); 

backgroundWorker.DoWork += 
    (s,e) => 
    {
        // runs on background thread
        MyService.Init(); 
    };

backgroundWorker.RunWorkerCompleted +=
    (s, e) =>
    {
        // runs on UI thread        
    };

backgroundWorker.RunWorkerAsync();
  1. 手动创建线程
  2. 将工作项排入线程池
  3. 使用 BackgroundWorker

有关使用 BackgroundWorker 类的更多信息,请参阅 BackGroundWorker Class 文档。

在后台线程上工作时,您无法直接更新任何 UI 控件,除非从后台线程将 UI 线程进行封送。Silverlight 提供了一种简单的机制,可以通过使用 Dispatcher.BegineInvoke 方法来实现。

Deployment.Current.Dispatcher.BeginInvoke(() =>
{
    //This happens on UI thread, if you want to marshal info back to UI thread 
    //from any other background thread.
});

注意在后台线程上工作时,您应始终记住 Silverlight 中的 UI 组件具有线程亲和性;也就是说,您只能从创建该控件的线程访问该控件。因此,如果您需要从后台线程更新 UI 以显示进度/完成报告,则应将您的代码调度到正确的线程上运行。这可以通过使用方法 myControl.Dispatcher.BeginInvoke() 来完成,其中 myControl 是您想要更新的控件。

行动项目 4:使用加载页面

现在您已将部分加载项移至后台,您的应用程序将加载得更快,并且您应该会很快看到应用程序的第一个页面。但是,很可能您没有什么有意义的内容(例如您正在获取的新数据)可以显示,因为后台线程仍在执行。因此,如果您在后台线程上开始了一些工作,您应该考虑在等待期间显示一个忙/加载指示器,例如进度条。

您应该避免为加载指示器 UI 设置单独的页面,因为它将成为应用程序页面回溯堆栈的一部分。这会造成一种奇怪的情况,即从主页面按下“后退”按钮会使用户返回到加载页面而不是退出应用程序。

相反,在您的主页面中,添加一个网格,其中包含两个控件:“加载控件”,它将显示加载信息(指示器),以及实际的主页面内容。根据当前的加载状态切换这两个控件的 Visibility 属性。另外,如果将 ProgressBarIsIndeterminate 属性设置为 true,则在不再需要加载屏幕时将其关闭,否则会产生性能问题。您可以在 A ProgressBar With Text For Windows Phone 7 中阅读更多相关信息。

例如,以下 XAML 显示了主页面可以如何构建:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <Grid x:Name="loadingContent">
        <TextBlock
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Margin="0,-30,0,0"
            Text="Loading..."
            />
        <ProgressBar
            x:Name="progressBar"
            Minimum="0"
            Maximum="100"
            />
    </Grid>
    <Grid x:Name="mainPageContent" Visibility="Collapsed">
        <TextBlock
            VerticalAlignment="Center"
            HorizontalAlignment="Center"
            FontSize="56"
            Text="Main page data"
            />
    </Grid>
</Grid>

以下代码演示了如何使用 BackgroundWorker 类在后台执行工作,更新进度条,并在工作完成后切换 Visibility

MyService.BackgroundWorker = new BackgroundWorker()
{
    WorkerReportsProgress = true
};

MyService.BackgroundWorker.DoWork +=
    (s, e) =>
    {
        MyService.Init();
    };

MyService.BackgroundWorker.ProgressChanged +=
    (s, e) =>
    {
        progressBar.Value = e.ProgressPercentage;
    };

MyService.BackgroundWorker.RunWorkerCompleted +=
    (s, e) =>
    {
        loadingContent.Visibility = Visibility.Collapsed;
        mainPageContent.Visibility = Visibility.Visible;
    };

MyService.BackgroundWorker.RunWorkerAsync();

为了接收来自后台线程的进度更新,我们将 BackgroundWorker 传递给了 MyService 类,并使用 ReportProgress 方法。

public class MyService
{
    private const int NumberOfIterations = 10;

    public static BackgroundWorker BackgroundWorker { get; set; }

    public static void Init()
    {
        for (int i = 0; i < NumberOfIterations; ++i)
        {
            Thread.Sleep(TimeSpan.FromSeconds(1));

            if (BackgroundWorker != null)
            {
                BackgroundWorker.ReportProgress((i+1) * 100 / NumberOfIterations);
            }
        }
    }

    ...
}

切换控件的另一种方法是将 mainContentloadingContentVisibility 属性绑定到一个布尔标志,该标志表示加载状态(例如,bool isLoading)。要做到这一点,您需要一个实现 IValueConverter 的类,用于将 bool 转换为 Visibility

摘要

Windows Phone 应用程序必须加载速度快并提供良好的用户体验。这一点由应用程序认证流程和操作系统强制执行。虽然本文中提供的示例相当简单,但这些概念对于所有 Windows Phone 应用程序都适用,并且这里解释的技术将适用于任何 Windows Phone 应用程序。

另一个重要的考虑因素是实际用户体验与感知用户体验。除了进度条之外,还有不止一种方法可以显示加载进度,但重要的是要显示有意义的内容,并告诉用户,“嘿,您很快就会看到您最喜欢的应用程序。”

暂时就到这里,
Arik Poznanski。

© . All rights reserved.