Share via


App instancing with the app lifecycle API (アプリのライフサイクル API によるアプリのインスタンス化)

アプリのインスタンス化モデルでは、アプリのプロセスの複数のインスタンスを同時に実行できるかどうかが決定されます。

前提条件

Windows App SDK でアプリ ライフサイクル API を使用するには、次のようにします。

  1. Windows App SDK の最新リリースをダウンロードしてインストールします。 詳細については、「Windows App SDK 用ツールをインストールする」を参照してください。
  2. 手順に従って、最初の WinUI 3 プロジェクトを作成するか、既存のプロジェクトで Windows App SDK を使用します

単一インスタンス アプリ

Note

C# を使用して WinUI 3 アプリで単一インスタンス化を実装する方法の例については、Windows 開発者ブログの「アプリを単一インスタンス化する」を参照してください。

一度に実行できるメイン プロセスが 1 つだけの場合、アプリは単一インスタンス アプリです。 単一インスタンス化されたアプリの 2 番目のインスタンスを起動しようとすると、通常はその代わりに最初のインスタンスのメイン ウィンドウがアクティブになります。 これはメイン プロセスにのみ該当することに注意してください。 単一インスタンス化されたアプリでは複数のバックグラウンド プロセスを作成できますが、それでもなお単一インスタンスとしてみなされます。

UWP アプリは既定で単一インスタンス化されます。 ただし、その代わりに追加のインスタンスを作成するか、既存のインスタンスをアクティブ化するかを起動時に決定することで、マルチインスタンス化される能力を持っています。

Windows メール アプリは、単一インスタンス化されたアプリのよい例です。 メールを初めて起動すると、新しいウィンドウが作成されます。 メールを再び起動しようとすると、その代わりに既存のメール ウィンドウがアクティブになります。

マルチインスタンス化アプリ

メイン プロセスを同時に複数回実行できる場合、アプリはマルチインスタンス アプリです。 マルチインスタンス化アプリの 2 番目のインスタンスを起動しようとすると、新しいプロセスとメイン ウィンドウが作成されます。

従来、非パッケージ アプリは既定でマルチインスタンス化されますが、必要な場合は単一インスタンス化に実装できます。 これは通常、あるアプリが既に実行されているかどうかを示すために単一の名前付きミューテックスを使用して行います。

メモ帳は、マルチインスタンス化されたアプリの良い例です。 メモ帳を起動するたびに、既に実行されているインスタンスの数に関係なく、メモ帳の新しいインスタンスが作成されます。

Windows App SDK のインスタンス化と UWP のインスタンス化の違い

Windows App SDK でのインスタンス化の動作は、UWP のモデルやクラスに基づいていますが、いくつかの重要な違いがあります。

AppInstance クラス

  • UWP: Windows.ApplicationModel.AppInstance クラスでは、単にインスタンス リダイレクトのシナリオに重点を置いています。
  • Windows App SDK: Microsoft.Windows.AppLifeycle.AppInstance クラスでは、インスタンス リダイレクトのシナリオがサポートされ、後のリリースで新機能をサポートするための追加機能を備えています。

インスタンスのリスト

  • UWP: GetInstances では、潜在的なリダイレクトのためにアプリで明示的に登録されたインスタンスのみが返されます。
  • Windows App SDK: GetInstances では、キーが登録されているかどうかにかかわらず、AppInstance API を使用しているアプリの実行中のすべてのインスタンスが返されます。 これには現在のインスタンスを含めることができます。 現在のインスタンスを一覧に含める場合は、AppInstance.GetCurrent を呼び出します。 同じアプリの異なるバージョンごと、および異なるユーザーによって起動されるアプリのインスタンスごとに別々のリストが保持されます。

キーの登録

マルチインスタンス化されたアプリの各インスタンスでは、FindOrRegisterForKey メソッドを使用して任意のキーを登録できます。 キーには固有の意味がないため、アプリでは任意の形式で、または必要な方法でキーを使用できます。

アプリのインスタンスでは、そのキーをいつでも設定できますが、インスタンスごとに 1 つのキーのみが許可されます。新しい値を設定すると、以前の値が上書きされます。

アプリのインスタンスは、他のインスタンスが既に登録しているのと同じ値にそのキーを設定することはできません。 既存のキーを登録しようとすると、FindOrRegisterForKey が発生し、そのキーを既に登録しているアプリ インスタンスが返されます。

  • UWP: インスタンスでは、GetInstances から返されるリストに含まれるようにするために、キーを登録する必要があります。
  • Windows App SDK: キーの登録は、インスタンスの一覧から分離されています。 インスタンスでは、一覧に含まれるためにキーを登録する必要はありません。

キーの登録解除

アプリのインスタンスは、そのキーを登録解除できます。

  • UWP: インスタンスのキーが登録解除されると、これはアクティブ化のリダイレクトに使用できなくなり、GetInstances から返されるインスタンスの一覧に含まれなくなります。
  • Windows App SDK: キーが登録解除されたインスタンスは、アクティブ化のリダイレクトに引き続き使用でき、GetInstances から返されるインスタンスの一覧に引き続き含まれます。

インスタンス リダイレクトのターゲット

1 つのアプリの複数のインスタンスは、互いにアクティブ化することができ、このプロセスを "アクティブ化のリダイレクト" といいます。 たとえば、あるアプリは起動時にアプリの他のインスタンスが見つからない場合にのみ自分自身を初期化することによって単一インスタンス化を実装しますが、別のインスタンスが存在する場合はリダイレクトして終了する場合があります。 マルチインスタンス アプリでは、そのアプリのビジネス ロジックに従って、必要に応じてアクティブ化をリダイレクトできます。 アクティブ化が別のインスタンスにリダイレクトされると、そのインスタンスの Activated コールバックが使用されますが、これは他のすべてのアクティブ化のシナリオで使用されるものと同じコールバックです。

  • UWP: キーを登録したインスタンスだけが、リダイレクトのターゲットになることができます。
  • Windows App SDK: 登録されたキーがあるかどうかにかかわらず、任意のインスタンスをリダイレクトのターゲットにすることができます。

リダイレクト後の動作

  • UWP: リダイレクトは終了の操作です。リダイレクトに失敗した場合でも、アプリはアクティブ化のリダイレクト後に終了します。

  • Windows App SDK: Windows App SDK では、リダイレクトは終了の操作ではありません。 これは一つには、既にメモリを割り当てている可能性のある Win32 アプリを任意に終了する際に発生する可能性のある問題が反映されたものですが、他方では、より高度なリダイレクト シナリオのサポートを可能にするものでもあります。 マルチインスタンス化されたアプリにおいて、あるインスタンスが CPU を集中的に使用する大量の作業を実行しているときに、アクティブ化要求を受け取る場合について考えてみましょう。 このアプリは、アクティブ化要求を別のインスタンスにリダイレクトし、自身の処理を続行できます。 このシナリオは、リダイレクト後にアプリが終了したとすると、実現不可能です。

アクティブ化要求は、複数回リダイレクトできます。 インスタンス A からインスタンス B にリダイレクトした後、今度はインスタンス C にリダイレクトすることができます。この機能を利用する Windows App 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 App 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);
    }
}

RedirectActivationTo の UWP バージョンとは異なり、Windows App SDK の RedirectActivationToAsync の実装では、アクティブ化をリダイレクトするときに、イベント引数を明示的に渡す必要があります。 これが必要な理由は、UWP がアクティブ化を厳密に制御し、正しいアクティブ化引数が正しいインスタンスに渡されることを保証するのに対し、Windows App SDK のバージョンでは多くのプラットフォームがサポートされるため、UWP 固有の機能に依存できないためです。 このモデルの利点の 1 つは、Windows App 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クラスは、アプリの 1 つのインスタンスを表します。 現在のプレビューでは、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());
    }
}