앱 수명 주기 API를 사용하여 앱 인스턴스화

앱의 인스턴스화 모델은 앱 프로세스의 여러 인스턴스를 동시에 실행할 수 있는지 여부를 결정합니다.

필수 조건

Windows App SDK에서 앱 수명 주기 API를 사용하려면 다음을 수행합니다.

  1. Windows App SDK의 최신 릴리스를 다운로드하여 설치합니다. 자세한 내용은 Windows 앱 SDK용 도구 설치를 참조하세요.
  2. 지침에 따라 첫 번째 WinUI 3 프로젝트를 만들거나 기존 프로젝트에서 Windows 앱 SDK를 사용합니다.

단일 인스턴스 앱

참고 항목

C#을 사용하여 WinUI 3 앱에서 단일 인스턴스를 구현하는 방법에 대한 예제는 Windows 개발자 블로그에서 앱 단일 인스턴스 만들기를 참조하세요.

한 번에 하나의 주 프로세스만 실행할 수 있는 앱은 단일 인스턴스 앱입니다. 단일 인스턴스 앱의 두 번째 인스턴스를 시작하려고 하면 일반적으로 첫 번째 인스턴스의 주 창이 대신 활성화됩니다. 이는 기본 프로세스에만 적용됩니다. 단일 인스턴스 앱은 여러 백그라운드 프로세스를 만들 수 있지만 여전히 단일 인스턴스로 간주됩니다.

UWP 앱은 기본적으로 단일 인스턴스 앱입니다. 하지만 실행할 때 추가 인스턴스를 만들 것인지 아니면 기존 인스턴스를 활성화할 것인지를 결정하여 다중 인스턴스 앱이 될 수 있습니다.

Windows 메일 앱은 단일 인스턴스 앱의 좋은 예입니다. 처음으로 메일을 시작하면 새 창이 생성됩니다. 메일을 다시 시작하려고 하면 기존 메일 창이 대신 활성화됩니다.

다중 인스턴스 앱

기본 프로세스를 동시에 여러 번 실행할 수 있는 앱은 다중 인스턴스 앱입니다. 다중 인스턴스 앱의 두 번째 인스턴스를 시작하려고 하면 새 프로세스와 주 창이 만들어집니다.

일반적으로, 압축을 푼 앱은 기본적으로 다중 인스턴스 앱이지만 필요한 경우 단일 인스턴스화를 구현할 수 있습니다. 일반적으로 이 작업은 명명된 단일 뮤텍스를 사용하여 앱이 이미 실행 중인지 여부를 표시합니다.

메모장은 다중 인스턴스 앱의 대표적인 예입니다. 메모장을 시작하려고 할 때마다 이미 실행 중인 인스턴스 수에 관계없이 메모장의 새 인스턴스가 생성됩니다.

Windows App SDK 인스턴스화와 UWP 인스턴스화의 차이점

Windows App SDK의 인스턴스화 동작은 UWP의 모델, 클래스를 기반으로 하지만 몇 가지 중요한 차이점이 있습니다.

AppInstance 클래스

인스턴스 목록

  • UWP: GetInstances는 앱에서 잠재적 리디렉션을 위해 명시적으로 등록한 인스턴스만 반환합니다.
  • Windows App SDK: GetInstances는 키 등록 여부에 관계없이 AppInstance API를 사용하는 앱의 모든 실행 중인 인스턴스를 반환합니다. 여기에는 현재 인스턴스가 포함될 수 있습니다. 현재 인스턴스를 목록에 포함하려면 AppInstance.GetCurrent를 호출합니다. 동일한 앱의 여러 버전에 대한 별도의 목록과 여러 사용자가 시작한 앱의 인스턴스가 유지됩니다.

키 등록

다중 인스턴스 앱의 각 인스턴스는 FindOrRegisterForKey 메서드를 통해 임의의 키를 등록할 수 있습니다. 키에는 내재된 의미가 없습니다. 앱에서 원하는 양식이나 방법으로 키를 사용할 수 있습니다.

앱의 인스턴스는 언제든지 키를 설정할 수 있지만, 각 인스턴스에 대해 하나의 키만 허용되며 새 값을 설정하면 이전 값을 덮어씁니다.

앱의 인스턴스는 다른 인스턴스가 이미 등록한 것과 동일한 값으로 해당 키를 설정할 수 없습니다. 기존 키를 등록하려고 하면 이미 해당 키를 등록한 앱 인스턴스가 반환되는 FindOrRegisterForKey가 발생합니다.

  • UWP: GetInstances에서 반환된 목록에 키를 포함하려면 인스턴스가 키를 등록해야 합니다.
  • Windows App SDK: 키를 등록하면 인스턴스 목록에서 분리됩니다 키를 목록에 포함하기 위해 인스턴스에서 키를 등록하지 않아도 됩니다.

키 등록 취소

앱의 인스턴스는 키의 등록을 취소할 수 있습니다.

  • UWP: 인스턴스가 키의 등록을 취소하면 키를 더 이상 활성화 리디렉션에 사용할 수 없으며 GetInstances에서 반환된 인스턴스 목록에 키가 포함되지 않습니다.
  • Windows App SDK: 키의 등록을 취소한 인스턴스는 여전히 활성화 리디렉션에 사용할 수 있으며 GetInstances에서 반환된 인스턴스 목록에 계속 포함됩니다.

인스턴스 리디렉션 대상

앱의 여러 인스턴스는 "활성화 리디렉션"이라는 프로세스를 통해 서로를 활성화할 수 있습니다. 예를 들어 앱은 시작 시 앱의 다른 인스턴스를 찾을 수 없으면 앱 자체를 초기화하여 단일 인스턴스를 구현할 수 있으며, 다른 인스턴스가 있으면 다른 인스턴스를 대신 리디렉션하고 종료합니다. 다중 인스턴스 앱은 해당 앱의 비즈니스 논리에 부합하는 경우 활성화를 리디렉션할 수 있습니다. 활성화가 다른 인스턴스로 리디렉션되면 다중 인스턴스 앱은 해당 인스턴스의 Activated 콜백을 사용합니다. 이 콜백은 다른 모든 활성화 시나리오에 사용되는 것과 동일한 콜백입니다.

  • UWP: 키를 등록한 인스턴스만이 리디렉션의 대상이 될 수 있습니다.
  • Windows App SDK: 키 등록 여부에 관계없이 모든 인스턴스는 리디렉션 대상이 될 수 있습니다.

리디렉션 후 동작

  • UWP: 리디렉션은 터미널 작업입니다. 리디렉션이 실패하더라도 활성화를 리디렉션한 후 앱이 종료됩니다.

  • Windows App SDK: Windows App SDK에서는 리디렉션이 터미널 작업이 아닙니다. 이는 부분적으로는 일부 메모리를 이미 할당했을 수 있는 Win32 앱을 임의로 종료하는 잠재적 문제를 반영하지만, 보다 정교한 리디렉션 시나리오를 지원할 수도 있습니다. 대량의 CPU 집약적 작업을 수행하는 동안 인스턴스가 활성화 요청을 수신하는 다중 인스턴스 앱을 생각해 보세요. 이 앱은 활성화 요청을 다른 인스턴스로 리디렉션하고 계속 처리할 수 있습니다. 리디렉션 후에 앱이 종료된 경우에는 이 시나리오가 불가능합니다.

활성화 요청은 여러 번 리디렉션할 수 있습니다. 인스턴스 A는 인스턴스 B로 리디렉션할 수 있고, 인스턴스 B는 인스턴스 C로 리디렉션할 수 있습니다. 이 기능을 활용하는 Windows App SDK 앱은 순환 리디렉션을 방지해야 합니다. 위의 예제에서 C가 A로 리디렉션되는 경우 무한 활성화 루프의 가능성이 있습니다. 앱에서 지원하는 워크플로에 적합한 내용에 따라 순환 리디렉션을 처리하는 방법을 결정하는 것은 앱의 몫입니다.

활성화 이벤트

다시 활성화를 처리하기 위해 앱에서 활성화된 이벤트를 등록할 수 있습니다.

예제

활성화 처리

다음 예제에서는 앱이 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);
    }
}

UWP 버전의 RedirectActivationTo와 달리, Windows App SDK에서 RedirectActivationToAsync를 구현하려면 활성화를 리디렉션할 때 이벤트 인수를 명시적으로 전달해야 합니다. 이렇게 해야 하는 이유는 UWP는 활성화를 엄격하게 제어하고 올바른 활성화 인수가 올바른 인스턴스에 전달되도록 보장할 수 있으며, Windows App SDK 버전은 여러 플랫폼을 지원하고 UWP 관련 기능을 사용할 수 없기 때문입니다. 이 모델의 장점 중 하나는 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();
}

Warning

프로세스가 종료될 때 키가 자동으로 등록 취소되기는 하지만, 종료된 인스턴스가 등록 취소되기 전에 또 다른 인스턴스가 종료된 인스턴스에 대한 리디렉션을 시작한 경우 경합 상태가 발생할 수 있습니다. 이러한 가능성을 줄이기 위해 앱에서 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());
    }
}