提升應用程式啟動效能的最佳策略 - UWP applications
2024-07-06藉由改善您處理啟動和啟用的方式,建立具有最佳啟動時間的通用 Windows 平臺 (UWP) 應用程式。
提升應用程式啟動效能的最佳策略
部分地,用戶會根據啟動需要多久時間來察覺您的應用程式是否快速或緩慢。 針對本主題的目的,應用程式的啟動時間會在用戶啟動應用程式時開始,並在使用者以某種有意義的方式與應用程式互動時結束。 本節提供有關如何在應用程式啟動時取得更佳效能的建議。
測量應用程式的啟動時間
請務必先啟動應用程式幾次,再實際測量其啟動時間。 這可提供測量的基準,並確保您儘可能縮短啟動時間。
在您 UWP 應用程式抵達客戶的電腦上時,您的應用程式已使用 .NET Native 工具鏈進行編譯。 .NET Native 是預先編譯技術,可將 MSIL 轉換成原生執行的機器程序代碼。 .NET 原生應用程式啟動速度較快、使用較少的記憶體,以及使用比 MSIL 對應專案更少的電池。 以 .NET Native 靜態方式建置的應用程式會在自定義運行時間和可在所有裝置上執行的新聚合式 .NET Core 連結,因此它們不會相依於內建的 .NET 實作。 在您的開發計算機上,如果您的應用程式是以「發行」模式建置它,則預設會使用 .NET Native,而如果您要以「偵錯」模式建置它,則會使用 CoreCLR。 您可以在 Visual Studio 中,從 [屬性] (C#) 的 [建置] 頁面或 [我的專案] (VB) 中的 [編譯->進階] 頁面進行設定。 尋找顯示「使用 .NET 原生工具鏈編譯」的複選框。
當然,您應該測量能代表最終使用者體驗的指標。 因此,如果您不確定要將應用程式編譯為開發電腦上的機器碼,您可以執行原生映像產生器 (Ngen.exe) 工具來預先編譯您的應用程式,再測量其啟動時間。
下列程式說明如何執行 Ngen.exe 來預先編譯您的應用程式。
執行 Ngen.exe
至少執行您的應用程式一次,以確保 Ngen.exe 偵測到它。
執行下列其中一項動作來開啟 工作排程器 :
從開始畫面搜尋「工作排程器」。
執行 「taskschd.msc」。
在 工作排程器的左側窗格中,展開 工作排程器程式庫。
展開 Microsoft。
展開「Windows」。
選取 [.NET Framework]。
從工作清單中選取 [.NET Framework NGEN 4.x]。
如果您使用 64 位元電腦,也有 .NET Framework NGEN v4.x 64。 如果您要建置 64 位應用程式, 請選擇 。NET Framework NGEN v4.x 64。
從 [動作 ] 選單中,點擊 [執行 ]。
Ngen.exe 先行編譯計算機上已使用且沒有原生映像的所有應用程式。 如果有許多應用程式需要先行編譯,這可能需要很長的時間,但後續的執行速度要快得多。
當您重新編譯應用程式時,不再使用原生映像。 相反地,應用程式會進行 Just-In-Time 編譯,這表示它會在應用程式執行時進行編譯。 您必須重新執行 Ngen.exe,以獲取新的原生映像。
盡可能延遲工作
若要改善應用程式的啟動時間,請只執行絕對需要完成的工作,讓用戶開始與應用程式互動。 如果您能夠延遲載入額外的元件,這將特別有利。 公共語言執行平台會在第一次使用時載入組件。 如果您可以將載入的元件數目降到最低,您可以改善應用程式的啟動時間和記憶體耗用量。
獨立進行長時間的工作
即使應用程式的某些部分無法完全正常運作,您的應用程式還是可以互動。 例如,如果您的應用程式顯示需要一段時間才能擷取的數據,您可以藉由以異步方式擷取數據,讓該程式代碼獨立於應用程式的啟動程式代碼執行。 當資料可供使用時,將資料填入至應用程式的使用者介面。
許多擷取數據的通用 Windows 平臺 (UWP) API 都是異步的,因此您可能還是會以異步方式擷取數據。 如需異步 API 的詳細資訊,請參閱 在 C# 或 Visual Basic 中呼叫異步 API。 如果您執行的工作未使用異步 API,您可以使用 Task 類別來執行長時間執行的工作,以免阻止使用者與應用程式互動。 這會讓應用程式在數據載入時回應使用者。
如果您的 app 需要很長的時間才能載入部分 UI,請考慮在該區域中新增字串,其內容如下:「取得最新數據」,讓使用者知道應用程式仍在處理中。
最小化啟動時間
除了最簡單的應用程式外,所有應用程式都需要一段時間才能載入資源、剖析 XAML、設定數據結構,以及在啟用時執行邏輯。 在這裡,我們會將啟動過程分成三個階段來進行分析。 我們也提供減少每個階段所花費時間的秘訣,以及讓應用程式啟動的每個階段對使用者更方便使用的技巧。
啟用期間是用戶啟動應用程式與應用程式運作時間之間的時間。 這是一個關鍵的時間,因為它是使用者對應用程式的第一印象。 他們預期系統與應用程式會有立即和持續的意見反應。 當應用程式無法快速啟動時,系統與應用程式會被視為中斷或設計不佳。 更糟的是,如果應用程式需要太長的時間才能啟動,進程存留期管理員 (PLM) 可能會終止它,或使用者可能會將其卸載。
啟動階段簡介
啟動涉及許多動態因素,且所有部分都必須正確協調,以獲得最佳使用者體驗。 以下步驟發生在用戶點擊您的應用程式圖示與應用程式內容顯示之間。
Windows 殼層會啟動進程,並呼叫 Main。
已經建立 Application 物件。
(項目範本) 建構函式會呼叫 InitializeComponent,這將解析 App.xaml 並創建物件。
當 Application.OnLaunched 事件被觸發。
(ProjectTemplate)應用程式程式碼會建立框架並導向至 MainPage。
(ProjectTemplate)MainPage 建構函式會呼叫 InitializeComponent,從而解析 MainPage.xaml 並建立物件。
ProjectTemplate) Window.Current.Activate() 被呼叫。
XAML 平臺會執行版面配置階段,包括“測量(Measure)”和“排列(Arrange)”。
ApplyTemplate 會導致為每個控制項建立控制項範本內容,這通常是啟動時版面配置時間的主要部分。
呼叫 Render 來建立所有視窗內容的視覺效果。
畫面會顯示給桌面 Windows 管理員 (DWM)。
在創業過程中減少投入
保持您的啟動程式代碼路徑不包含不需要於第一個畫面的任何元素。
如果您有使用者 dll 包含第一個畫面格期間不需要的控件,請考慮延遲載入它們。
如果您的部分使用者介面依賴來自雲端的資料,請將該 UI 分開。 首先,啟動不依賴於雲端數據的使用者介面,並以異步方式啟動依賴雲端的使用者介面。 您也應該考慮在本機快取資料,這樣應用程式就能離線運作,也不會受到網路連線緩慢的影響。
如果您的 UI 正在等待數據,請顯示進度界面。
請謹慎處理涉及許多組態檔剖析的應用程式設計,或由程式碼動態產生的使用者介面。
減少元素數量
XAML 應用程式中的啟動效能會與您在啟動期間建立的項目數目直接相關。 您建立的元素越少,應用程式啟動所需的時間就越少。 作為粗略的基準檢驗,請考慮每個元素需要 1 毫秒才能建立。
專案控制元件中使用的範本影響可能最大,因為它們會重複多次。 請參閱 ListView 和 GridView UI 優化。
UserControls 和控件範本將會展開,因此也應該將這些範本納入考慮。
如果您建立任何未出現在畫面上的 XAML,則應該證明是否應在啟動期間建立這些 XAML 片段。
Visual Studio Live Visual Tree 視窗會顯示樹狀結構中每個節點的子元素數量。
使用延遲。 折疊元素,或將其不透明度設定為0,都不會阻止元素的創建。 使用 x:Load 或 x:DeferLoadStrategy,您可以延遲載入一段 UI,並在需要時載入它。 這是一種延遲處理在啟動畫面期間不可見的使用者介面的方法,因此您可以在需要時載入它,或將其作為一組延遲邏輯的一部分。 若要觸發載入,您只需呼叫該元素的 FindName。 如需範例和詳細資訊,請參閱 x:Load 屬性 和 x:DeferLoadStrategy 屬性。
虛擬化。 如果您的UI中有清單或重複程式內容,強烈建議您使用UI虛擬化。 如果未虛擬化清單 UI,則您需支付在前面建立所有元素的費用,這樣可能會減緩您的啟動速度。 請參閱 ListView 和 GridView UI 優化。
應用程式效能不僅與原始效能有關,也與感知有關。 變更作業順序,讓視覺層面先發生,通常會讓用戶覺得應用程式更快速。 當內容顯示在螢幕上時,用戶會認為應用程式已載入。 通常,應用程式在啟動時需要執行多個動作,其中有些不是立即顯示 UI 介面所必需的,因此應該延遲或將優先順序排在 UI 之後。
本主題討論來自動畫/電視的「第一個畫面」,並衡量使用者看到內容的時間長度。
改善新創企業形象
讓我們使用簡單的在線遊戲範例來識別每個啟動階段和不同的技術,以在整個過程中提供使用者意見反應。 在此範例中,啟用的第一個階段是從用戶點選遊戲圖標到遊戲開始執行程式碼的時間。 在此期間,系統沒有任何內容可以顯示給用戶,甚至無法表明正確的遊戲已開始。 但提供啟動顯示畫面會將該內容提供給系統。 然後,遊戲會通知用戶啟動的第一階段已完成,當程序代碼開始執行時,它會用自己的UI取代靜態啟動畫面。
啟用的第二個階段包括建立和初始化對遊戲至關重要的結構。 如果應用程式可以在啟用第一個階段之後使用可用的數據快速建立其初始 UI,則第二個階段是微不足道的,您可以立即顯示 UI。 否則,我們建議應用程式在初始化時顯示載入頁面。
載入頁面的外觀是由您決定的,而且可以像顯示進度列或進度環一樣簡單。 關鍵是應用程式在變得有反應之前,指出它正在執行工作。 在遊戲的情況下,它想要顯示其初始畫面,但該 UI 需要從磁碟載入記憶體中的一些影像和音效。 這些工作需要幾秒鐘的時間,因此應用程式會以載入頁面取代啟動顯示畫面,讓使用者保持通知,這會顯示與遊戲主題相關的簡單動畫。
當遊戲取得最小化的資訊集,可以建立互動式使用者介面時,第三階段便會開始,這個介面會取代載入頁面。 此時,在線遊戲唯一可用的資訊就是應用程式從磁碟載入的內容。 遊戲可以隨附足夠的內容來建立互動式UI;但是,因為它是一個在線遊戲,它不會運作,直到它連接到因特網並下載一些額外的資訊。 在擁有運作所需的所有資訊之前,使用者可以與UI互動,但需要從網路獲取額外數據的功能應該提供內容仍在載入的回饋。 應用程式可能需要一些時間才能完全運作,因此請務必儘快提供功能。
既然我們已在在線遊戲中識別出啟用的三個階段,讓我們將其系結至實際程序代碼。
階段 1
在應用程式開始之前,必須告訴系統想要顯示作為啟動畫面的內容。 其方式是將影像和背景色彩提供給應用程式指令清單中的 SplashScreen 元素,如範例所示。 Windows 會在應用程式開始啟用後顯示此訊息。
...
...
...
如需詳細資訊,請參閱 新增啟動顯示畫面。
僅使用應用程式的建構函式來初始化對應用程式至關重要的數據結構。 建構函式只會在第一次執行應用程式時呼叫,而且不一定每次啟動應用程式。 例如,不會針對已執行、置於背景的應用程式呼叫建構函式,然後透過搜尋合約啟動。
階段 2
啟動應用程式的原因有很多,您可能想要以不同的方式處理。 您可以覆寫 OnActivated、OnCachedFileUpdaterActivated、OnFileActivated、OnFileOpenPickerActivated、OnFileSavePickerActivated、OnLaunched、OnSearchActivated和 OnShareTargetActivated 方法來處理啟用的每個原因。 應用程式在這些方法中必須執行的其中一件事是建立UI、將它指派給 Window.Content,然後呼叫 Window.Activate。 此時,啟動顯示畫面會由應用程式建立的使用者介面取代。 如果啟用時有足夠的資訊可供建立,此視覺效果可能會載入畫面或應用程式的實際 UI。
public partial class App : Application
{
// A handler for regular activation.
async protected override void OnLaunched(LaunchActivatedEventArgs args)
{
base.OnLaunched(args);
// Asynchronously restore state based on generic launch.
// Create the ExtendedSplash screen which serves as a loading page while the
// reader downloads the section information.
ExtendedSplash eSplash = new ExtendedSplash();
// Set the content of the window to the extended splash screen.
Window.Current.Content = eSplash;
// Notify the Window that the process of activation is completed
Window.Current.Activate();
}
// a different handler for activation via the search contract
async protected override void OnSearchActivated(SearchActivatedEventArgs args)
{
base.OnSearchActivated(args);
// Do an asynchronous restore based on Search activation
// the rest of the code is the same as the OnLaunched method
}
}
partial class ExtendedSplash : Page
{
// This is the UIELement that's the game's home page.
private GameHomePage homePage;
public ExtendedSplash()
{
InitializeComponent();
homePage = new GameHomePage();
}
// Shown for demonstration purposes only.
// This is typically autogenerated by Visual Studio.
private void InitializeComponent()
{
}
}
Partial Public Class App
Inherits Application
' A handler for regular activation.
Protected Overrides Async Sub OnLaunched(ByVal args As LaunchActivatedEventArgs)
MyBase.OnLaunched(args)
' Asynchronously restore state based on generic launch.
' Create the ExtendedSplash screen which serves as a loading page while the
' reader downloads the section information.
Dim eSplash As New ExtendedSplash()
' Set the content of the window to the extended splash screen.
Window.Current.Content = eSplash
' Notify the Window that the process of activation is completed
Window.Current.Activate()
End Sub
' a different handler for activation via the search contract
Protected Overrides Async Sub OnSearchActivated(ByVal args As SearchActivatedEventArgs)
MyBase.OnSearchActivated(args)
' Do an asynchronous restore based on Search activation
' the rest of the code is the same as the OnLaunched method
End Sub
End Class
Partial Friend Class ExtendedSplash
Inherits Page
Public Sub New()
InitializeComponent()
' Downloading the data necessary for
' initial UI on a background thread.
Task.Run(Sub() DownloadData())
End Sub
Private Sub DownloadData()
' Download data to populate the initial UI.
' Create the first page.
Dim firstPage As New MainPage()
' Add the data just downloaded to the first page
' Replace the loading page, which is currently
' set as the window's content, with the initial UI for the app
Window.Current.Content = firstPage
End Sub
' Shown for demonstration purposes only.
' This is typically autogenerated by Visual Studio.
Private Sub InitializeComponent()
End Sub
End Class
在啟用處理程式中顯示載入畫面的應用程式會開始在背景中建立用戶介面。 在建立該元素後,其 FrameworkElement.Loaded 事件將會觸發。 在事件處理程式中,您會以新建立的首頁取代目前為載入畫面的窗口內容。
對於具有較長初始化時間的應用程式,顯示載入頁面是至關重要的。 除了提供使用者關於啟用程式的意見反應之外,如果在啟動程序開始後的 15 秒內未呼叫 Window.Activate ,程式將會終止。
partial class GameHomePage : Page
{
public GameHomePage()
{
InitializeComponent();
// add a handler to be called when the home page has been loaded
this.Loaded += ReaderHomePageLoaded;
// load the minimal amount of image and sound data from disk necessary to create the home page.
}
void ReaderHomePageLoaded(object sender, RoutedEventArgs e)
{
// set the content of the window to the home page now that it's ready to be displayed.
Window.Current.Content = this;
}
// Shown for demonstration purposes only.
// This is typically autogenerated by Visual Studio.
private void InitializeComponent()
{
}
}
Partial Friend Class GameHomePage
Inherits Page
Public Sub New()
InitializeComponent()
' add a handler to be called when the home page has been loaded
AddHandler Me.Loaded, AddressOf ReaderHomePageLoaded
' load the minimal amount of image and sound data from disk necessary to create the home page.
End Sub
Private Sub ReaderHomePageLoaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
' set the content of the window to the home page now that it's ready to be displayed.
Window.Current.Content = Me
End Sub
' Shown for demonstration purposes only.
' This is typically autogenerated by Visual Studio.
Private Sub InitializeComponent()
End Sub
End Class
如需使用延伸啟動顯示畫面的範例,請參閱 啟動顯示畫面範例。
階段 3
只是因為應用程式顯示UI並不表示它已完全可供使用。 在我們的遊戲中,UI 會以需要從互聯網獲取數據的功能佔位符顯示。 此時,遊戲會下載讓應用程式完整運作所需的額外數據,並隨著數據取得而逐漸啟用功能。
有時候,啟用所需的大部分內容都可以與應用程式一起封裝。 這是一個簡單遊戲的例子。 這讓啟用程序相當簡單。 但許多節目(如新聞讀者和照片觀眾)必須從網路提取資訊才能發揮作用。 此數據可能很大,而且需要相當多的時間才能下載。 在啟用程式期間,應用程式如何取得此數據,可能會對應用程式的感知效能產生巨大影響。
如果應用程式在啟用的第一或第二階段嘗試下載其功能所需的整個資料集,您可能會看到載入頁面,甚至更糟的是,啟動畫面可能會顯示數分鐘。 這會使應用程式看起來像當機,或被系統終止。 我們建議應用程式下載最少的數據量,以顯示階段 2 中具有佔位元元素的互動式 UI,然後逐漸載入數據,以取代階段 3 中的佔位元元素。 如需處理數據的詳細資訊,請參閱 優化 ListView 和 GridView。
應用程式對每個啟動階段的反應完全由您決定,但提供使用者盡可能多的回饋(啟動畫面、載入畫面、在載入數據時顯示的 UI)可以讓用戶覺得應用程式和整個系統運行得很快。
將啟動路徑中的Managed元件最小化
可重複使用的程式碼通常以專案中包含的模組 (DLL) 形式提供。 載入這些模組需要存取磁碟,如您所想像,這樣做的成本可能會增加。 這對冷啟動的影響最大,但它也會對暖啟動產生影響。 在 C# 和 Visual Basic 的情況下,CLR 會視需求載入程式集,以嘗試盡可能延後該成本。 也就是說,在執行的方法參考模組之前,CLR 不會載入模組。 因此,僅參考啟動程式碼中啟動應用程式所需的元件,讓CLR不會載入不必要的模組。 如果您的啟動路徑中有未使用的程式代碼路徑且包含不必要的參考,您可以將這些程式代碼路徑移至其他方法,以避免不必要的載入。
減少模組載入的另一種方式是結合您的應用程式模組。 載入一個大型元件通常需要比載入兩個小組件少一些時間。 這不一定可行,只有在模組不會對開發人員生產力或程式代碼重複使用性產生重大影響時,您才應該合併模組。 您可以使用 PerfView 或 Windows Performance Analyzer (WPA) 等工具來了解啟動時載入哪些模組。
提出智慧型 Web 要求
將應用程式的內容(包括 XAML、影像及其他重要檔案)封裝在本機,可以顯著提升應用程式的載入速度。 磁碟作業比網路作業更快。 如果應用程式在初始化時需要特定檔案,您可以從磁碟載入它,而不是從遠端伺服器擷取它,以減少整體啟動時間。
有效率地記錄和快取頁面
「Frame」控制項提供導覽功能。 它提供導覽至頁面(Navigate 方法)、導覽記錄(BackStack/ForwardStack 屬性、GoForward/GoBack 方法)、頁面快取(Page.NavigationCacheMode)和序列化支援(GetNavigationState 方法)。
在使用 Frame 時,效能方面需要注意的主要涉及日誌和頁面快取。
框架日誌。 當您流覽至具有 Frame.Navigate() 的頁面時,目前頁面的 PageStackEntry 會新增至 Frame.BackStack 集合。 PageStackEntry 相對較小,但沒有BackStack集合大小的內建限制。 使用者有可能在迴圈中巡覽,並無限期地成長此集合。
PageStackEntry 也包含傳遞至 Frame.Navigate() 方法的參數。 建議參數為基本可串行化類型(例如 int 或 string),以允許 Frame.GetNavigationState() 方法運作。 但是,該參數可能會參考一個物件,而這個物件可能會消耗更多的工作集或其他資源,使BackStack中的每個條目的開銷更高。 例如,您可能會使用 StorageFile 做為參數,結果 BackStack 會保持無限數量的檔案開啟著。
因此,建議讓瀏覽參數保持較小,並限制BackStack的大小。 BackStack 是標準向量(C# 中的 IList、C++/CX 中的 Platform::Vector),因此只要移除專案即可修剪。
頁面快取。 根據預設,當您使用 Frame.Navigate 方法巡覽至頁面時,頁面的新實例會具現化。 同樣地,如果您接著使用 Frame.GoBack 瀏覽回上一頁,則會分配一個上一頁的新實例。
不過,Frame 提供了一種可選擇的頁面快取,可以避免這些初始化過程。 若要將頁面放入快取中,請使用 Page.NavigationCacheMode 屬性。 將模式設定為 [必要] 會強制快取頁面,將它設定為 [已啟用] 將允許快取。 根據預設,快取大小為 10 頁,但可以使用 Frame.CacheSize 屬性覆寫。 所有必需頁面都會被快取,如果必需頁面的數量少於 CacheSize,也可以快取已啟用的頁面。
頁面快取可藉由避免具現化來協助效能,進而改善流覽效能。 頁面快取可能會因過度快取而影響效能,進而影響工作集。
因此,建議您根據應用程式的需求適當地使用頁面快取。 例如,假設您有一個應用程式會顯示 Frame 中的專案清單,而且當您點選專案時,它會將框架巡覽至該專案的詳細數據頁面。 清單頁面可能應該設定為快取。 如果所有項目的詳細頁面都相同,可能也應該快取。 但是,如果詳細頁面更異質,最好不要啟用快取。