應用程式生命週期 API 的應用程式執行個體
應用程式的執行個體模型會判斷應用程式的多個執行個體是否可以同時執行。 Windows 應用程式 SDK 中的應用程式生命週期 API 可讓您控制應用程式可以同時執行多少個實例,並在必要時將啟用重新導向至其他實例。
本文說明如何使用應用程式生命週期 API 來控制 WinUI 應用程式中的應用程式實例。
必要條件
若要在 WinUI 3 應用程式中使用應用程式生命週期 API:
- 下載並安裝最新版的 Windows 應用程式 SDK。 如需詳細資訊,請參閱 開始使用 WinUI。
- 請遵循指示來建立您的第一個 WinUI 3 專案,或使用現有專案中的 Windows 應用程式 SDK。
單一執行個體應用程式
如果一次只能有一個主要程序執行,應用程式就會是單一執行個體。 嘗試啟動單一執行個體應用程式的第二個執行個體,通常會導致第一個執行個體的主視窗改為啟動。 請注意,這適用於主要程序。 單一執行個體應用程式可以建立多個背景程序,但仍視為單一執行個體。
WinUI 應用程式預設為多重實例,但能夠在啟動時決定建立新的實例或改為啟動現有的實例,以成為單一實例。
Microsoft 相片 應用程式是單一實例 WinUI 應用程式的良好範例。 當您第一次啟動相片時,將會建立新的視窗。 如果您再次嘗試啟動相片,則會改為啟動現有的視窗。
如需如何使用 C# 在 WinUI 3 應用程式中實作單一實例的範例,請參閱 建立單一實例的 WinUI 應用程式。
多執行個體應用程式
如果主要程序可以同時執行多次,應用程式就會多執行個體化。 嘗試啟動多執行個體應用程式的第二個執行個體會建立新的程序和主視窗。
傳統上,未封裝的應用程式預設為多重實例,但可以在一定的情況下實作單一實例。 這通常是使用單一具名 Mutex 來完成,以指出應用程式是否已執行。
記事本是多執行個體應用程式的一個很好的例子。 每次嘗試啟動記事本時,無論有多少記事本執行個體已在執行,都會建立一個新的記事本執行個體。
Windows 應用程式 SDK 執行個體與 UWP 執行個體有何不同
Windows 應用程式 SDK 中的執行個體行為是以UWP的模型、類別為基礎,但有一些主要差異:
AppInstance 類別
- UWP:Windows.ApplicationModel.AppInstance 類別純粹著重於執行個體重新導向案例。
- Windows 應用程式 SDK:Microsoft.Windows.AppLifeycle.AppInstance 類別支持執行個體重新導向案例,並包含其他功能以支援更新版本中的新功能。
執行個體的清單
- UWP:GetInstances 只會傳回應用程式明確註冊以進行潛在重新導向的執行個體。
- Windows 應用程式 SDK:GetInstances 會傳回使用AppInstance API 之應用程式的所有執行中執行個體,無論它們是否已註冊密鑰。 這可以包含目前的執行個體。 如果您要將目前的實體包含在清單中,請呼叫
AppInstance.GetCurrent
。 為同一應用程式的不同版本以及不同使用者啟動的應用程式執行個體維護單獨的清單。
註冊金鑰
多重執行個體應用程式的每個執行個體都可以透過FindOrRegisterForKey
方法註冊任意密鑰。 密鑰沒有固有的意義;應用程式可以按照他們希望的任何形式或方式使用密鑰。
應用程式的執行個體可以隨時設定其密鑰,但每個執行個體只允許一個密鑰;設定新值會覆寫先前的值。
應用程式的執行個體無法將其索引鍵設定為另一個執行個體已註冊的相同值。 嘗試註冊現有的金鑰會導致FindOrRegisterForKey
傳回已註冊該金鑰的應用程式執行個體。
- UWP:執行個體必須註冊密鑰,才能包含在從 GetInstances 傳回的清單中。
- Windows 應用程式 SDK:註冊金鑰會與執行個體清單分離。 執行個體不需要註冊金鑰,才能包含在清單中。
取消註冊金鑰
應用程式的執行個體可以取消註冊其密鑰。
- UWP:當執行個體取消註冊其密鑰時,它就不再可供啟用重新導向使用,而且不會包含在從 GetInstances 傳回的執行個體清單中。
- Windows 應用程式 SDK:尚未註冊其密鑰的執行個體仍可供啟用重新導向使用,而且仍包含在從 GetInstances 傳回的執行個體清單中。
執行個體重新導向目標
應用程式的多個執行個體可以互相啟動,稱為「啟用重新導向」的程式。 例如,應用程式可以透過僅在啟動時找不到應用程式的其他執行個體時初始化自身來實現單一執行個體,如果存在另一個執行個體則重新導向並退出。 根據該應用程式的商業規則,多重執行個體應用程式可以在適當時重新導向啟用。 當啟用重新導向至另一個執行個體時,它會使用該執行個體的Activated
回呼,這是所有其他啟用案例中使用的相同回呼。
- UWP:只有已註冊密鑰的執行個體才能成為重新導向的目標。
- Windows 應用程式 SDK:任何執行個體都可以是重新導向目標,無論其是否有已註冊的密鑰。
重新導向後行為
UWP:重新導向是終端機作業;即使重新導向失敗,應用程式會在重新導向之後終止。
Windows 應用程式 SDK:在 Windows 應用程式 SDK 中,重新導向不是終端機作業。 這在一定程度上反映了任意終止 Win32 應用程式的潛在問題,該應用程式可能已經分配了一些記憶體,但也允許支援更複雜的重新導向方案。 請考慮執行大量 CPU 密集型工作時,執行個體會收到啟用要求的多執行個體應用程式。 該應用程式可以將啟用要求重新導向至另一個執行個體,並繼續處理。 如果應用程式在重新導向之後終止,就無法達到該案例。
啟用要求可以重新導向多次。 執行個體 A 可以重新導向至執行個體 B,而執行個體 B 可能會重新導向至執行個體 C。Windows 應用程式 SDK 利用這項功能的應用程式必須防範迴圈重新導向 - 如果上述範例中的 C 重新導向至 A,則可能會有無限啟用迴圈。 應用程式取決於應用程式支援哪些工作流程,判斷如何處理迴圈重新導向。
啟用事件
為了處理重新啟用,應用程式可以註冊啟用事件。
- UWP:事件會將 IActivatedEventArgs 傳遞至應用程式。
- Windows 應用程式 SDK:事件會將 Microsoft.Windows.AppLifecycle.AppActivationArguments 執行個體傳遞至包含其中
-ActivatedEventArgs
一個執行個體的應用程式。
範例
處理啟用
此範例示範應用程式如何註冊及處理Activated
事件。 當它收到Activated
事件時,此應用程式會使用事件自變數來判斷造成啟用的動作類型,並適當地回應。
int APIENTRY wWinMain(
_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine, _In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// Initialize the Windows App SDK framework package for unpackaged apps.
HRESULT hr{ MddBootstrapInitialize(majorMinorVersion, versionTag, minVersion) };
if (FAILED(hr))
{
OutputFormattedDebugString(
L"Error 0x%X in MddBootstrapInitialize(0x%08X, %s, %hu.%hu.%hu.%hu)\n",
hr, majorMinorVersion, versionTag,
minVersion.Major, minVersion.Minor, minVersion.Build, minVersion.Revision);
return hr;
}
if (DecideRedirection())
{
return 1;
}
// Connect the Activated event, to allow for this instance of the app
// getting reactivated as a result of multi-instance redirection.
AppInstance thisInstance = AppInstance::GetCurrent();
auto activationToken = thisInstance.Activated(
auto_revoke, [&thisInstance](
const auto& sender, const AppActivationArguments& args)
{ OnActivated(sender, args); }
);
// Carry on with regular Windows initialization.
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_CLASSNAME, szWindowClass, MAX_LOADSTRING);
RegisterWindowClass(hInstance);
if (!InitInstance(hInstance, nCmdShow))
{
return FALSE;
}
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
MddBootstrapShutdown();
return (int)msg.wParam;
}
void OnActivated(const IInspectable&, const AppActivationArguments& args)
{
int const arraysize = 4096;
WCHAR szTmp[arraysize];
size_t cbTmp = arraysize * sizeof(WCHAR);
StringCbPrintf(szTmp, cbTmp, L"OnActivated (%d)", activationCount++);
ExtendedActivationKind kind = args.Kind();
if (kind == ExtendedActivationKind::Launch)
{
ReportLaunchArgs(szTmp, args);
}
else if (kind == ExtendedActivationKind::File)
{
ReportFileArgs(szTmp, args);
}
}
以啟用類型為基礎的重新導向邏輯
在此範例中,應用程式會註冊啟用事件的處理程式,也會檢查啟用事件自變數,以決定是否要將啟用重新導向至另一個執行個體。
針對大部分的啟用類型,應用程式會繼續執行其一般初始化程式。 不過,如果啟用是由開啟關聯的檔類型所造成,而且如果此應用程式的另一個執行個體已經開啟檔案,則目前的執行個體會將啟用重新導向至現有的執行個體並結束。
此應用程式會使用金鑰註冊來判斷哪些檔案會在哪個執行個體中開啟。 當執行個體開啟檔案時,它會註冊包含該檔名的密鑰。 然後,其他執行個體可以檢查已註冊的密鑰,並尋找特定的檔名,並在沒有其他執行個體時自行註冊為該檔案的執行個體。
請注意,雖然金鑰註冊本身是 Windows 應用程式 SDK 應用程式生命週期 API 的一部分,但密鑰的內容只會在應用程式本身內指定。 應用程式不需要註冊檔名或任何其他有意義的資料。 不過,此應用程式已決定根據其特定需求和支援的工作流程,透過金鑰追蹤開啟的檔案。
bool DecideRedirection()
{
// Get the current executable filesystem path, so we can
// use it later in registering for activation kinds.
GetModuleFileName(NULL, szExePath, MAX_PATH);
wcscpy_s(szExePathAndIconIndex, szExePath);
wcscat_s(szExePathAndIconIndex, L",1");
// Find out what kind of activation this is.
AppActivationArguments args = AppInstance::GetCurrent().GetActivatedEventArgs();
ExtendedActivationKind kind = args.Kind();
if (kind == ExtendedActivationKind::Launch)
{
ReportLaunchArgs(L"WinMain", args);
}
else if (kind == ExtendedActivationKind::File)
{
ReportFileArgs(L"WinMain", args);
try
{
// This is a file activation: here we'll get the file information,
// and register the file name as our instance key.
IFileActivatedEventArgs fileArgs = args.Data().as<IFileActivatedEventArgs>();
if (fileArgs != NULL)
{
IStorageItem file = fileArgs.Files().GetAt(0);
AppInstance keyInstance = AppInstance::FindOrRegisterForKey(file.Name());
OutputFormattedMessage(
L"Registered key = %ls", keyInstance.Key().c_str());
// If we successfully registered the file name, we must be the
// only instance running that was activated for this file.
if (keyInstance.IsCurrent())
{
// Report successful file name key registration.
OutputFormattedMessage(
L"IsCurrent=true; registered this instance for %ls",
file.Name().c_str());
}
else
{
keyInstance.RedirectActivationToAsync(args).get();
return true;
}
}
}
catch (...)
{
OutputErrorString(L"Error getting instance information");
}
}
return false;
}
任意重新導向
此範例會藉由新增更複雜的重新導向規則,來擴充上一個範例。 應用程式仍會執行上一個範例的開啟檔案檢查。 不過,如果先前的範例未根據開啟的檔案檢查重新導向,則一律會建立新的執行個體,本範例會新增「可重複使用」執行個體的概念。 如果找到可重複使用的執行個體,則目前的執行個體會重新導向至可重複使用的執行個體並結束。 否則,它會將其本身註冊為可重複使用,並繼續其一般初始化。
同樣地,請注意,應用程式生命週期 API 中不存在「可重複使用」執行個體的概念;它只會在應用程式本身內建立及使用。
int APIENTRY wWinMain(
_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine, _In_ int nCmdShow)
{
// Initialize COM.
winrt::init_apartment();
AppActivationArguments activationArgs =
AppInstance::GetCurrent().GetActivatedEventArgs();
// Check for any specific activation kind we care about.
ExtendedActivationKind kind = activationArgs.Kind;
if (kind == ExtendedActivationKind::File)
{
// etc... as in previous scenario.
}
else
{
// For other activation kinds, we'll trawl all instances to see if
// any are suitable for redirecting this request. First, get a list
// of all running instances of this app.
auto instances = AppInstance::GetInstances();
// In the simple case, we'll redirect to any other instance.
AppInstance instance = instances.GetAt(0);
// If the app re-registers re-usable instances, we can filter for these instead.
// In this example, the app uses the string "REUSABLE" to indicate to itself
// that it can redirect to a particular instance.
bool isFound = false;
for (AppInstance instance : instances)
{
if (instance.Key == L"REUSABLE")
{
isFound = true;
instance.RedirectActivationToAsync(activationArgs).get();
break;
}
}
if (!isFound)
{
// We'll register this as a reusable instance, and then
// go ahead and do normal initialization.
winrt::hstring szKey = L"REUSABLE";
AppInstance::FindOrRegisterForKey(szKey);
RegisterClassAndStartMessagePump(hInstance, nCmdShow);
}
}
return 1;
}
重新導向協調流程
此範例會再次新增更複雜的重新導向行為。 在這裡,應用程式執行個體可以自行註冊為處理特定類型所有啟用的執行個體。 當應用程式的執行個體收到Protocol
啟用時,會先檢查已註冊以處理 Protocol
啟用的執行個體。 如果找到該執行個體,則會將啟用重新導向至該執行個體。 如果沒有,則目前的執行個體會自行註冊啟用Protocol
,然後套用其他邏輯 (未顯示),這可能會因為其他原因而重新導向啟用。
void OnActivated(const IInspectable&, const AppActivationArguments& args)
{
const ExtendedActivationKind kind = args.Kind;
// For example, we might want to redirect protocol activations.
if (kind == ExtendedActivationKind::Protocol)
{
auto protocolArgs = args.Data().as<ProtocolActivatedEventArgs>();
Uri uri = protocolArgs.Uri();
// We'll try to find the instance that handles protocol activations.
// If there isn't one, then this instance will take over that duty.
auto instance = AppInstance::FindOrRegisterForKey(uri.AbsoluteUri());
if (!instance.IsCurrent)
{
instance.RedirectActivationToAsync(args).get();
}
else
{
DoSomethingWithProtocolArgs(uri);
}
}
else
{
// In this example, this instance of the app handles all other
// activation kinds.
DoSomethingWithNewActivationArgs(args);
}
}
不同於的 UWP 版本,RedirectActivationTo
Windows 應用程式 SDK 的 RedirectActivationToAsync 實作需要在重新導向啟用時明確傳遞事件自變數。 這是必要的,因為 UWP 會嚴格控制啟用,而且可以確保正確的啟用自變數會傳遞至正確的執行個體,Windows 應用程式 SDK 的版本支援許多平臺,而且無法依賴 UWP 特定功能。 此模型的優點之一是使用 Windows 應用程式 SDK 的應用程式有機會修改或取代將傳遞至目標執行個體的自變數。
重新導向而不封鎖
在進行不必要的初始化工作之前,大部分的應用程式都希望儘早重新導向。 針對某些應用程式類型,初始化邏輯會在 STA 執行緒上執行,而該執行緒不得遭到封鎖。 AppInstance.RedirectActivationToAsync 方法為非同步,而且呼叫的應用程式必須等候方法完成,否則重新導向將會失敗。 不過,等候非同步呼叫將會封鎖 STA。 在這些情況下,在另一個執行緒中呼叫 RedirectActivationToAsync,並在呼叫完成時設定事件。 然後使用 CoWaitForMultipleObjects 等非封鎖 API 等候該事件。 以下是 WPF 應用程式的 C# 範例。
private static bool DecideRedirection()
{
bool isRedirect = false;
// Find out what kind of activation this is.
AppActivationArguments args = AppInstance.GetCurrent().GetActivatedEventArgs();
ExtendedActivationKind kind = args.Kind;
if (kind == ExtendedActivationKind.File)
{
try
{
// This is a file activation: here we'll get the file information,
// and register the file name as our instance key.
if (args.Data is IFileActivatedEventArgs fileArgs)
{
IStorageItem file = fileArgs.Files[0];
AppInstance keyInstance = AppInstance.FindOrRegisterForKey(file.Name);
// If we successfully registered the file name, we must be the
// only instance running that was activated for this file.
if (keyInstance.IsCurrent)
{
// Hook up the Activated event, to allow for this instance of the app
// getting reactivated as a result of multi-instance redirection.
keyInstance.Activated += OnActivated;
}
else
{
isRedirect = true;
// Ensure we don't block the STA, by doing the redirect operation
// in another thread, and using an event to signal when it has completed.
redirectEventHandle = CreateEvent(IntPtr.Zero, true, false, null);
if (redirectEventHandle != IntPtr.Zero)
{
Task.Run(() =>
{
keyInstance.RedirectActivationToAsync(args).AsTask().Wait();
SetEvent(redirectEventHandle);
});
uint CWMO_DEFAULT = 0;
uint INFINITE = 0xFFFFFFFF;
_ = CoWaitForMultipleObjects(
CWMO_DEFAULT, INFINITE, 1,
new IntPtr[] { redirectEventHandle }, out uint handleIndex);
}
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error getting instance information: {ex.Message}");
}
}
return isRedirect;
}
取消註冊重新導向
註冊金鑰的應用程式可以隨時取消註冊該金鑰。 此範例假設目前執行個體先前已註冊索引鍵,指出它已開啟特定檔案,這表示後續嘗試開啟該檔案將會重新導向至該檔案。 關閉該檔案時,必須刪除包含檔名的索引鍵。
void CALLBACK OnFileClosed(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
AppInstance::GetCurrent().UnregisterKey();
}
警告
雖然索引鍵會在程序終止時自動取消註冊,但在取消註冊終止的執行個體之前,可能會有另一個執行個體起始重新導向至終止的執行個體。 為了減輕這種可能性,應用程式可以使用 UnregisterKey 在終止密鑰之前手動取消註冊金鑰,讓應用程式有機會將啟用重新導向至不在結束過程中的另一個應用程式。
執行個體資訊
Microsoft.Windows.AppLifeycle.AppInstance 類別代表應用程式的單一執行個體。 在目前的預覽中,AppInstance
只包含支援啟用重新導向所需的方法和屬性。 在更新版本中,AppInstance
將會展開以包含與應用程式執行個體相關的其他方法和屬性。
void DumpExistingInstances()
{
for (AppInstance const& instance : AppInstance::GetInstances())
{
std::wostringstream sStream;
sStream << L"Instance: ProcessId = " << instance.ProcessId
<< L", Key = " << instance.Key().c_str() << std::endl;
::OutputDebugString(sStream.str().c_str());
}
}