应用实例化与应用生命周期 API

应用的实例化模型决定了应用进程的多个实例是否可以同时运行。 Windows 应用 SDK中的应用生命周期 API 提供了一种方法,用于控制应用可以同时运行多少个实例,并在必要时将激活重定向到其他实例。

本文介绍如何使用应用生命周期 API 控制 WinUI 应用中的应用实例化。

先决条件

若要在 WinUI 3 应用中使用应用生命周期 API,请执行以下操作:

单实例应用

如果一次只能运行一个主进程,则应用是单实例的。 尝试启动单实例应用的第二个实例通常会导致第一个实例的主窗口被激活。 请注意,这仅适用于主进程。 单实例应用可以创建多个后台进程,但仍被视为单实例。

WinUI 应用默认为多实例,但可以通过在启动时决定创建新实例还是激活现有实例来成为单实例。

Microsoft 照片应用是单个实例 WinUI 应用的一个很好的示例。 首次启动“照片”时,将创建一个新窗口。 如果再次尝试启动“照片”,则会改为激活现有窗口。

有关如何使用 C# 在 WinUI 3 应用中实现单实例的示例,请参阅 创建单实例 WinUI 应用

多实例应用

如果主进程可同时运行多个实例,则应用是多实例应用程序。 如果尝试启动多实例应用程序的第二个实例,则会创建一个新的进程和主窗口。

传统上,默认情况下,未打包的应用是多实例的,但一定可以实现单实例化。 通常这是使用单个命名互斥来实现的,以表明应用是否已在运行。

记事本是多实例应用的一个很好的例子。 每次你尝试启动记事本时,都会创建一个新的记事本实例,无论有多少实例已经在运行。

Windows 应用 SDK 实例化与 UWP 实例化有何不同

Windows 应用 SDK 中的实例化行为是基于 UWP 的模型、类,但有一些关键区别:

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,则存在潜在的无限激活循环. 由应用确定如何处理循环重定向,取决于应用程序支持的工作流有何意义。

激活事件

为了处理重新激活,应用可以注册一个 Activated 事件。

示例

处理激活

此示例演示应用如何注册和处理 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);
    }
}

基于激活类型的重定向逻辑

在此示例中,应用为 Activated 事件注册了一个处理程序,并且还检查了激活事件参数以决定是否将激活重定向到另一个实例。

对于大多数类型的激活,应用继续其常规初始化过程。 但是,如果激活是由打开的关联文件类型引起的,并且如果此应用的另一个实例已经打开了该文件,则当前实例会将激活重定向到现有实例并退出。

此应用使用密钥注册来确定哪些文件在哪些实例中打开。 当实例打开文件时,它将注册包含该文件名的密钥。 然后,其他实例可以检查注册的密钥,查找特定的文件名,并将其自身注册为该文件的实例(如果任何其他实例还没有注册)。

请注意,尽管密钥注册本身是 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,并设置调用完成时的事件。 然后等待这个使用非阻塞 API(如 CoWaitForMultipleObjects)的事件。 下面是 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());
    }
}

创建单实例 WinUI 应用

Microsoft.Windows.AppLifeycle.AppInstance