CLR 探查器和 Windows 应用商店应用

本主题讨论在编写用于分析 Windows 应用商店应用中运行的托管代码的诊断工具时需要考虑的事项。 其中还提供了有关修改现有开发工具的指导,以便可以继续针对 Windows 应用商店应用正常运行这些工具。 若要理解这些信息,最好先熟悉公共语言运行时探查 API,并且已在某个可针对 Windows 桌面应用程序正常运行的诊断工具中使用过此 API,而现在你有兴趣修改该工具,使其能够针对 Windows 应用商店应用正常运行。

简介

如果你能轻松理解介绍段落,则表示你熟悉 CLR 探查 API。 你已编写一个可针对托管桌面应用程序正常运行的诊断工具。 现在,你很好奇需要做些什么,才能对托管的 Windows 应用商店应用正常使用该工具。 也许你已经尝试过这种修改,但发现这不是一项简单直接的任务。 事实上,所有工具开发人员都可能会忽略一些考虑因素。 例如:

  • Windows 应用商店应用在权限严重降低的上下文中运行。

  • 与传统的托管模块相比,Windows 元数据文件具有独特的特征。

  • 当交互停止时,Windows 应用商店应用习惯于自我挂起。

  • 由于各种原因,进程间通信机制可能不再起作用。

本主题列出了需要注意的事项以及如何正确处理这些事项。

如果你不熟悉 CLR 探查 API,请跳转到本主题末尾的“资源”部分,以找到更详细的介绍信息。

有关特定 Windows API 及其用法的详细信息也不属于本主题的范畴。 请将本主题看作一个学习起点,并参阅 MSDN 来详细了解本主题中提到的任何 Windows API。

体系结构和术语

通常,诊断工具的体系结构如下图所示。 虽然此处使用了术语“探查器”,但许多此类工具的功能远远超越了普通的性能或内存探查,而涉及到了代码覆盖率、模拟对象框架、时间旅行调试、应用程序监视等方面。 为方便起见,本主题将继续将所有这些工具称作探查器。

整个主题中使用了以下术语:

应用程序

这是探查器正在分析的应用程序。 通常,此应用程序的开发人员目前正在使用探查器来帮助诊断其问题。 在传统上,此应用程序是一个 Windows 桌面应用程序,但在本主题中,我们探讨的是 Windows 应用商店应用。

探查器 DLL

这是加载到正在分析的应用程序的进程空间中的组件。 此组件(也称为探查器“代理”)实现 ICorProfilerCallbackICorProfilerCallback 接口(2、3 等)接口并使用 ICorProfilerInfo(2、3 等)接口来收集有关分析的应用程序的数据,以及在某些情况下修改应用程序行为的各个方面。

探查器 UI

这是探查器用户与之交互的桌面应用程序。 它负责向用户显示应用程序状态,并为用户提供控制分析的应用程序行为的方法。 此组件始终在其自身的进程空间中运行,该空间与所要分析的应用程序的进程空间相互独立。 探查器 UI 还可充当“附加触发器”,即,用于调用 ICLRProfiling::AttachProfiler 方法的进程,在探查器 DLL 在启动时未加载的情况下,该方法会导致分析的应用程序加载探查器 DLL。

重要

即使探查器 UI 用于控制和报告 Windows 应用商店应用,它也仍应保持为一个 Windows 桌面应用程序。 不要指望能够在 Windows 应用商店中打包和发布你的诊断工具。 你的工具需要完成 Windows 应用商店应用所不能完成的任务,而其中的许多任务都是在探查器 UI 中进行的。

在整篇文档中,示例代码假设:

  • 探查器 DLL 是根据 CLR 探查 API 的要求以 C++ 编写的,因为它必须是本机 DLL。

  • 探查器 UI 是以 C# 编写的。 虽然不一定非要如此,但既然探查器 UI 进程对语言没有要求,为何不选择一种简洁易用的语言呢?

Windows RT 设备

Windows RT 设备的封闭性相当大。 第三方探查器根本无法在此类设备上加载。 本文档重点介绍 Windows 8 电脑。

使用 Windows 运行时 API

在以下部分讨论的一些方案中,探查器 UI 桌面应用程序需要使用某些新的 Windows 运行时 API。 建议查阅文档以了解可以从桌面应用程序使用哪些 Windows 运行时 API,以及在从桌面应用程序和 Windows 应用商店应用调用这些 API 时它们的行为是否不同。

如果探查器 UI 是以托管代码编写的,则需要执行几个步骤才能轻松使用这些 Windows 运行时 API。 有关详细信息,请参阅托管桌面应用和 Windows 运行时一文。

加载探查器 DLL

本部分介绍探查器 UI 如何使 Windows 应用商店应用加载探查器 DLL。 本部分讨论的代码属于探查器 UI 桌面应用,因此涉及到使用对桌面应用安全,但对 Windows 应用商店应用不一定安全的 Windows API。

探查器 UI 可通过两种方式使探查器 DLL 加载到应用程序的进程空间中:

  • 在应用程序启动时加载,由环境变量控制。

  • 在启动完成后,通过调用 ICLRProfiling::AttachProfiler 方法附加到应用程序。

要克服的第一个阻碍是对 Windows 应用商店应用正常运行探查器 DLL 的 startup-load(启动加载)和 attach-load(附加加载)。 这两种加载形式有一些共同的特殊注意事项,因此让我们先从这些注意事项开始。

启动加载和附加加载的常见注意事项

为探查器 DLL 签名

当 Windows 尝试加载探查器 DLL 时,它会验证探查器 DLL 是否已正确签名。 如果未正确签名,则加载默认会失败。 有两种方法可以实现此目的:

  • 确保探查器 DLL 已签名。

  • 请告诉用户,在使用你的工具之前,必须在其 Windows 8 计算机上安装开发人员许可证。 这可以从 Visual Studio 自动安装,也可以从命令提示符手动安装。 有关详细信息,请参阅获取开发人员许可证

文件系统权限

Windows 应用商店应用必须有权从文件系统中探查器 DLL 所在的位置加载和执行该探查器 DLL。默认情况下,Windows 应用商店应用对大多数目录没有此类权限,尝试加载探查器 DLL 失败会在 Windows 应用程序事件日志中生成如下所示的条目:

NET Runtime version 4.0.30319.17929 - Loading profiler failed during CoCreateInstance.  Profiler CLSID: '{xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}'.  HRESULT: 0x80070005.  Process ID (decimal): 4688.  Message ID: [0x2504].

一般情况下,只允许 Windows 应用商店应用访问磁盘上有限的几个位置。 每个 Windows 应用商店应用可以访问其自身的应用程序数据文件夹,以及文件系统中所有 Windows 应用商店应用都有权访问的其他少数几个区域。 最好将探查器 DLL 及其依赖项安装在 Program Files 或 Program Files (x86) 下的某个位置,因为默认情况下,所有 Windows 应用商店应用都对这两个目录拥有读取和执行权限。

启动加载

通常,在桌面应用中,探查器 UI 通过初始化包含所需 CLR 探查 API 环境变量(即 COR_PROFILERCOR_ENABLE_PROFILINGCOR_PROFILER_PATH)的环境块,然后使用该环境块创建一个新进程,来提示对探查器 DLL 进行启动加载。 Windows 应用商店应用也是如此,不过机制不同。

不要以提升的完整性运行

如果进程 A 尝试生成 Windows 应用商店应用进程 B,则进程 A 应该以中等完整性级别运行,而不能以高完整性(即,提升的完整性)级别运行。 这意味着,探查器 UI 应以中等完整性级别运行,或者必须生成另一个中等完整性级别的桌面进程来负责启动 Windows 应用商店应用。

选择要探查的 Windows 应用商店应用

首先,需要询问探查器用户启动哪个 Windows 应用商店应用。 对于桌面应用,也许你会显示一个文件浏览对话框,而用户会查找并选择某个 .exe 文件。 但 Windows 应用商店应用与此不同,使用浏览对话框没有意义。 对于这种情况,最好向用户显示已安装的 Windows 应用商店应用列表供用户选择。

可以使用 PackageManager 类来生成此列表。 PackageManager 是可用于桌面应用的 Windows 运行时类,事实上它仅可用于桌面应用。

以下代码示例来自一个以 C# 语言编写为桌面应用的虚构探查器 UI。该示例使用 PackageManager 生成 Windows 应用列表:

string currentUserSID = WindowsIdentity.GetCurrent().User.ToString();
IAppxFactory appxFactory = (IAppxFactory) new AppxFactory();
PackageManager packageManager = new PackageManager();
IEnumerable<Package> packages = packageManager.FindPackagesForUser(currentUserSID);

指定自定义环境块

可以使用新的 COM 接口 IPackageDebugSettings 来自定义 Windows 应用商店应用的执行行为,使某些形式的诊断变得更容易。 使用该接口的方法之一 EnableDebugging,可以在 Windows 应用商店应用启动时向其传递环境块以及其他有用的效果,例如禁用自动进程暂停。 环境块很重要,因为需要指定 CLR 用来加载 Profiler DLL 的环境变量(COR_PROFILERCOR_ENABLE_PROFILINGCOR_PROFILER_PATH))。

请思考以下代码片段:

IPackageDebugSettings pkgDebugSettings = new PackageDebugSettings();
pkgDebugSettings.EnableDebugging(packageFullName, debuggerCommandLine,
                                                                 (IntPtr)fixedEnvironmentPzz);

需要正确处理以下几项:

  • 在迭代包和抓取 package.Id.FullName 时可以确定 packageFullName

  • debuggerCommandLine 更有趣一些。 若要将自定义环境块传递给 Windows 应用商店应用,你需要编写自己的极其简单的虚拟调试器。 Windows 将使 Windows 应用商店应用暂停,然后使用如以下示例中所示的命令行启动你的调试器,以此附加该调试器:

    MyDummyDebugger.exe -p 1336 -tid 1424
    

    其中,-p 1336 表示 Windows 应用商店应用的进程 ID 为 1336,-tid 1424 表示线程 ID 1424 是已暂停的线程。 虚拟调试器将从命令行分析线程 ID,恢复该线程,然后退出。

    下面是用于执行此操作的一些示例 C++ 代码(请务必添加错误检查!):

    int wmain(int argc, wchar_t* argv[])
    {
        // …
        // Parse command line here
        // …
    
        HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME,
                                                                  FALSE /* bInheritHandle */, nThreadID);
        ResumeThread(hThread);
        CloseHandle(hThread);
        return 0;
    }
    

    需要将此虚拟调试器部署为诊断工具安装的一部分,然后在 debuggerCommandLine 参数中指定此调试器的路径。

启动 Windows 应用商店应用

启动 Windows 应用商店应用的时刻终于来临。 如果你已自行尝试创建过 Windows 应用商店应用进程,你可能已发现,使用 CreateProcess 无法创建该进程。 而是需要改用 IApplicationActivationManager::ActivateApplication 方法。 为此,需要获取所要启动的 Windows 应用商店应用的应用用户模型 ID。 而这意味着需要在清单中进行更深入一点的挖掘。

在迭代包时(请参阅前面启动加载部分中的“选择要探查的 Windows 应用商店应用”),需要抓取当前包清单中包含的应用程序集:

string manifestPath = package.InstalledLocation.Path + "\\AppxManifest.xml";

AppxPackaging.IStream manifestStream;
SHCreateStreamOnFileEx(
                    manifestPath,
                    0x00000040,     // STGM_READ | STGM_SHARE_DENY_NONE
                    0,              // file creation attributes
                    false,          // fCreate
                    null,           // reserved
                    out manifestStream);

IAppxManifestReader manifestReader = appxFactory.CreateManifestReader(manifestStream);

IAppxManifestApplicationsEnumerator appsEnum = manifestReader.GetApplications();

没错,一个包可以包含多个应用程序,而每个应用程序具有自身的应用程序用户模型 ID。 因此,需要询问用户要探查哪个应用程序,并从该特定应用程序中抓取应用程序用户模型 ID:

while (appsEnum.GetHasCurrent() != 0)
{
    IAppxManifestApplication app = appsEnum.GetCurrent();
    string appUserModelId = app.GetAppUserModelId();
    //...
}

最终,你已获得了启动 Windows 应用商店应用所需的信息:

IApplicationActivationManager appActivationMgr = new ApplicationActivationManager();
appActivationMgr.ActivateApplication(appUserModelId, appArgs, ACTIVATEOPTIONS.AO_NONE, out pid);

请记得调用 DisableDebugging

在调用 IPackageDebugSettings::EnableDebugging 时,你已承诺会通过调用 IPackageDebugSettings::DisableDebugging 方法亲自进行清理,因此请务必在探查会话结束时执行此操作。

附加加载

当探查器 UI 想要将其探查器 DLL 附加到已开始运行的应用程序时,它会使用 ICLRProfiling::AttachProfiler。 对于 Windows 应用商店应用也是如此。 但除了前面列出的常见注意事项外,请确保目标 Windows 应用商店应用未暂停。

EnableDebugging

与执行启动加载时一样,调用 IPackageDebugSettings::EnableDebugging 方法。 不需要该方法来传递环境块,但需要使用它的其他一项功能:禁用自动进程暂停。 否则,当探查器 UI 调用 AttachProfiler 时,目标 Windows 应用商店应用可能会暂停。 事实上,如果用户现在正在与探查器 UI 交互,而 Windows 应用商店应用在用户的任何屏幕上都不处于活动状态,则有可能出现这种情况。 如果 Windows 应用商店应用已暂停,它将无法响应 CLR 发送给它的、要附加探查器 DLL 的任何信号。

因此需要运行如下所示的代码:

IPackageDebugSettings pkgDebugSettings = new PackageDebugSettings();
pkgDebugSettings.EnableDebugging(packageFullName, null /* debuggerCommandLine */,
                                                                 IntPtr.Zero /* environment */);

这与启动加载方案中发出的调用相同,只是没有指定调试器命令行或环境块。

DisableDebugging

如前所述,在探查会话完成时,请记得调用 IPackageDebugSettings::DisableDebugging

在 Windows 应用商店应用内部运行

Windows 应用商店应用终于加载了探查器 DLL。 现在,探查器 DLL 必须学会如何按照 Windows 应用商店应用所需的不同规则运行,包括允许哪些 API,以及如何在权限降低的情况下运行。

坚持使用 Windows 应用商店应用 API

在浏览 Windows API 时你会发现,文档中指出每个 API 适用于桌面应用、Windows 应用商店应用或此两者。 例如,InitializeCriticalSectionAndSpinCount 函数文档的“要求”部分指明该函数仅适用于桌面应用。 相比之下,InitializeCriticalSectionEx 函数同时适用于桌面应用和 Windows 应用商店应用。

在开发探查器 DLL 时,请将其视为 Windows 应用商店应用,并仅使用其文档中指出适用于 Windows 应用商店应用的 API。 分析依赖项(例如,可对探查器 DLL 运行 link /dump /imports 以进行审核),然后搜索文档以查看哪些依赖项正常,哪些不正常。 在大多数情况下,只需将冲突项替换为在文档中阐述为安全的较新形式的 API(例如,将 InitializeCriticalSectionAndSpinCount 替换为 InitializeCriticalSectionEx)即可修复冲突。

你可能已注意到,探查器 DLL 会调用某些仅适用于桌面应用的 API,但即使在 Windows Store 应用内部加载探查器 DLL,这些 API 似乎也适用。 请注意,在已加载到 Windows 应用商店应用进程的探查器 DLL 中使用任何未阐述为适用于 Windows 应用商店应用的 API 会有风险:

  • 在运行 Windows 应用商店应用的唯一上下文中调用此类 API 时,不能保证这些 API 正常运行。

  • 从不同的 Windows 应用商店应用进程内部调用时,此类 API 可能无法始终如一地正常运行。

  • 在当前版本的 Windows 中,此类 API 似乎能在 Windows 应用商店应用中良好运行,但在将来的 Windows 版本中可能会中断或被禁用。

最好是修复所有冲突并避免风险。

你可能发现,有时只有使用某个特定的 API 才能达到自己的目的,并且找不到适合 Windows 应用商店应用的替代 API。 在这种情况下,请至少:

  • 反复测试,竭尽所能地得出该 API 的用法。

  • 知道如果在将来的 Windows 版本中从 Windows 应用商店应用内部调用该 API,它可能会突然中断或消失。 Microsoft 不会将这种情况视为兼容性问题,而且不会专门为你一个人提供此 API 的支持。

降低的权限

本主题不会列出 Windows 应用商店应用与桌面应用权限的不同之处,因为这不属于本主题的范畴。 但是,每当探查器 DLL(加载到 Windows 应用商店应用而不是桌面应用中时)尝试访问任何资源时,行为肯定有所不同。 最常见的例子就是文件系统。 磁盘上有几个位置(但很少)允许给定的 Windows 应用商店应用访问(请参阅文件访问和权限(Windows 运行时应用)),而探查器 DLL 将受到同样的限制。 请全面测试你的代码。

进程内通信

如本文开头的示意图中所示,探查器 DLL(已加载到 Windows 应用商店应用进程空间)可能需要通过你自己的自定义进程间通信 (IPC) 通道来与探查器 UI(在单独的桌面应用进程空间中运行)通信。 探查器 UI 向探查器 DLL 发送信号以修改其行为,探查器 DLL 将已分析的 Windows 应用商店应用中的数据发回到探查器 UI 进行后处理,并将其显示给探查器用户。

大多数探查器需要按此方式工作,但是在将探查器 DLL 加载到 Windows 应用商店应用中时,在 IPC 机制方面的选择更有限。 例如,命名管道不是 Windows 应用商店应用 SDK 的一部分,因此不能使用它们。

但是当然,文件仍然存在,不过其存在方式更有限制。 还会提供事件。

通过文件进行通信

大部分数据可能会通过文件在探查器 DLL 与 探查器 UI 之间传递。 关键是选择探查器 DLL(在 Windows 应用商店应用的上下文中)和探查器 UI 都对其拥有读写权限的文件位置。 例如,临时文件夹路径是探查器 DLL 和探查器 UI 都有权访问的位置,但其他 Windows 应用商店应用包无法访问该位置(因此屏蔽了从其他 Windows 应用商店应用包记录的任何信息)。

探查器 UI 和探查器 DLL 都可以独立确定此路径。 当探查器 UI 迭代针对当前用户安装的所有包时(请参阅前面的示例代码),将获取对 PackageId 类的访问权限,通过此类可以使用类似于以下代码片段的代码派生临时文件夹路径。 (与前面一样,为简洁起见,此处省略了错误检查。)

// C# code for the Profiler UI.
ApplicationData appData =
    ApplicationDataManager.CreateForPackageFamily(
        packageId.FamilyName);

tempDir = appData.TemporaryFolder.Path;

同时,探查器 DLL 可以执行基本相同的操作,不过,使用 ApplicationData.Current 属性可以更轻松地访问 ApplicationData 类。

通过事件进行通信

如果需要在探查器 UI 与探查器 DLL 之间使用简单的信号语义,可以在 Windows 应用商店应用和桌面应用内部使用事件。

在探查器 DLL 中,只需调用 CreateEventEx 函数使用所需的任何名称来创建一个命名事件。 例如:

// Profiler DLL in Windows Store app (C++).
CreateEventEx(
    NULL,  // Not inherited
    "MyNamedEvent"
    CREATE_EVENT_MANUAL_RESET, /* explicit ResetEvent() required; leave initial state unsignaled */
    EVENT_ALL_ACCESS);

然后,探查器 UI 需要在 Windows 应用商店应用的命名空间下查找该命名事件。 例如,探查器 UI 可以调用 CreateEventEx,并将事件名称指定为

AppContainerNamedObjects\<acSid>\MyNamedEvent

<acSid> 是 Windows 应用商店应用的 AppContainer SID。 本主题的前面部分介绍了如何迭代针对当前用户安装的包。 从该示例代码中,可以获取 packageId。 而从 packageId 中又可以获取 <acSid>,其代码如下所示:

IntPtr acPSID;
DeriveAppContainerSidFromAppContainerName(packageId.FamilyName, out acPSID);

string acSid;
ConvertSidToStringSid(acPSID, out acSid);

string acDir;
GetAppContainerFolderPath(acSid, out acDir);

无关闭通知

在 Windows 应用商店应用内部运行时,探查器 DLL 不应依赖于调用 ICorProfilerCallback::Shutdown 甚至 DllMain(结合 DLL_PROCESS_DETACH)来向探查器 DLL 通知 Windows 应用商店应用正在退出。 事实上,应该预期永远不会调用这些方法。 在过去,许多探查器 DLL 使用这些通知作为将缓存刷新到磁盘、关闭文件、向探查器 UI 发回通知等操作的捷径。但现在需要以略微不同的方式组织探查器 DLL。

探查器 DLL 在运行过程中应会记录信息。 出于性能原因,你可能希望在内存中批处理信息,并在批的大小超过某个阈值时将信息刷新到磁盘。 但应该假设任何尚未刷新到磁盘的信息都可能丢失。 这意味着需要明智选择阈值,并需要强化探查器 UI 以处理探查器 DLL 写入的不完整信息。

Windows 运行时元数据文件

本文档不会详细介绍 Windows 运行时元数据 (WinMD) 文件是什么,因为这不属于本文档的范畴。 本部分的内容仅限于当探查器 DLL 正在分析的 Windows 应用商店应用加载 WinMD 文件时,CLR 探查 API 如何做出反应。

托管和非托管 WinMD

如果开发人员使用 Visual Studio 创建新的 Windows 运行时组件项目,则生成该项目时会生成一个 WinMD 文件,该文件描述开发人员编写的元数据(类、接口等的类型描述)。 如果此项目是用 C# 或 Visual Basic 编写的托管语言项目,则该 WinMD 文件还包含这些类型的实现(意味着它包含从开发人员的源代码编译的所有 IL)。 此类文件称为托管 WinMD 文件。 它们的有趣之处在于它们包含 Windows 运行时元数据和底层实现。

相比之下,如果开发人员创建了适用于 C++ 的 Windows 运行时组件项目,则生成该项目时会生成一个仅包含元数据的 WinMD 文件,而实现将编译到单独的本机 DLL 中。 同样,Windows SDK 中随附的 WinMD 文件仅包含元数据,实现将编译到 Windows 随附的单独本机 DLL 中。

以下信息适用于包含元数据和实现的托管 WinMD,以及仅包含元数据的非托管 WinMD。

WinMD 文件类似于 CLR 模块

在涉及到 CLR 时,所有 WinMD 文件都是模块。 因此,CLR 探查 API 会告诉探查器 DLL 何时加载 WinMD 文件及其 ModuleID 是什么,就像处理其他托管模块时一样。

探查器 DLL 可以通过调用 ICorProfilerInfo3::GetModuleInfo2 方法并检查 COR_PRF_MODULE_WINDOWS_RUNTIME 标志的 pdwModuleFlags 输出参数,将 WinMD 文件与其他模块区分开来。 (当且仅当 ModuleID 表示 WinMD 时才设置此参数。)

从 WinMD 读取元数据

与普通模块一样,WinMD 文件包含可通过元数据 API 读取的元数据。 但是,CLR 在读取 WinMD 文件时会将 Windows 运行时类型映射到 .NET Framework 类型,让在托管代码中编程并使用 WinMD 文件的开发人员可以获取更自然的编程体验。 有关这些映射的某些示例,请参阅 .NET Framework 对 Windows 应用商店应用和 Windows 运行时的支持

那么,探查器在使用元数据 API 时将获取哪个视图:是原始 Windows 运行时视图还是映射的 .NET Framework 视图? 答案是:由你确定。

在 WinMD 上调用 ICorProfilerInfo::GetModuleMetaData 方法以获取元数据接口(例如 IMetaDataImport)时,可以选择在 dwOpenFlags 参数中设置 ofNoTransform 来关闭此映射。 否则,将按默认启用映射。 通常,探查器会保持启用映射,以便探查器 DLL 从 WinMD 元数据(例如,类型的名称)获取的字符串让探查器用户感到熟悉而自然。

修改 WinMD 中的元数据

不支持修改 WinMDs 中的元数据。 如果针对 WinMD 文件调用 ICorProfilerInfo::GetModuleMetaData 方法并在 dwOpenFlags 参数中指定 ofWrite,或请求可写的元数据接口(例如 IMetaDataEmit),则 GetModuleMetaData 将会失败。 这对于 IL 重写探查器而言特别重要,因为它们需要修改元数据以支持其检测(例如,添加 AssemblyRefs 或新方法)。 因此,应该先检查 COR_PRF_MODULE_WINDOWS_RUNTIME(如上一部分所述),并避免在此类模块上请求可写的元数据接口。

解析 WinMD 的程序集引用

许多探查器需要手动解析元数据引用,以帮助进行检测或类型检查。 此类探查器需要知道 CLR 如何解析指向 WinMD 的程序集引用,因为这些引用的解析方式与标准程序集引用完全不同。

内存探查器

Windows 应用商店应用和桌面应用中的垃圾收集器和托管堆并无根本性的差异。 但是,探查器创建者需要注意某些细微的差异。

ForceGC 创建托管线程

在进行内存探查时,探查器 DLL 通常会创建一个单独的线程用于调用 ForceGC 方法。 这并不是什么新鲜事。 但令人惊讶的是,在 Windows 应用商店应用中进行垃圾收集的行为可能会将你的线程转换为托管线程(例如,为该线程创建一个探查 API ThreadID)。

若要了解其后果,必须了解 CLR 探查 API 定义的同步调用与异步调用之间的差别。 请注意,这与 Windows 应用商店应用中的异步调用概念有很大的不同。 有关详细信息,请参阅博客文章 Why we have CORPROF_E_UNSUPPORTED_CALL_SEQUENCE(CORPROF_E_UNSUPPORTED_CALL_SEQUENCE 有何作用)。

需要注意的是,对探查器创建的线程发出的调用始终被视为同步调用,即使这些调用是从探查器 DLL 的某个 ICorProfilerCallback 方法的实现外部发出的。 最起码过去往往是这样子的。 既然由于调用了 ForceGC 方法,CLR 已将探查器的线程转换为托管线程,那么,该线程就不再被视为探查器的线程。 因此,CLR 对该线程的同步调用条件实施了更严格的定义 — 即调用必须源自探查器 DLL 的某个 ICorProfilerCallback 方法才被视为同步调用。

在实践中这意味着什么? 大多数 ICorProfilerInfo 方法只能以同步方式安全调用,否则会立即失败。 因此,如果探查器 DLL 将 ForceGC 方法线程重用于通常对探查器创建的线程(例如 RequestProfilerDetachRequestReJITRequestRevert)发出的其他调用,则你会遇到问题。 在从托管线程调用时,即使是 DoStackSnapshot 之类的异步安全函数也有特殊的规则。 (有关详细信息,请参阅博客文章 Profiler stack walking: Basics and beyond(探查器堆栈漫谈:基础和延伸)。)

因此,我们建议仅出于触发 GC,然后响应 GC 回调的目的,使用探查器 DLL 为调用 ForceGC 方法而创建的任何线程。 不应调用探查 API 来执行堆栈采样或分离之类的其他任务。

ConditionalWeakTableReferences

从 .NET Framework 4.5 开始提供了一个新的 GC 回调 ConditionalWeakTableElementReferences,它为探查器提供有关依赖句柄的更完整信息。 这些句柄能够有效地添加了从源对象到目标对象的引用来进行 GC 生存期管理。 依赖句柄并不是新鲜事物,甚至在 Windows 8 和 .NET Framework 4.5 发布之前,能以托管代码编程的开发人员就可以使用 System.Runtime.CompilerServices.ConditionalWeakTable<TKey,TValue> 类创建自己的依赖句柄。

但是,托管 XAML Windows 应用商店应用现在重度使用依赖句柄。 具体而言,CLR 使用依赖句柄来帮助管理托管对象与非托管 Windows 运行时对象之间的引用循环。 这意味着,内存探查器现在比以往任何时候都更需要了解这些依赖句柄,以便可以连同堆图中的其他边缘一起可视化。 探查器 DLL 应同时使用 RootReferences2ObjectReferencesConditionalWeakTableElementReferences 来构成堆图的完整视图。

结束语

可以使用 CLR 探查 API 来分析在 Windows 应用商店应用内部运行的托管代码。 事实上,你可以采用正在开发的现有探查器并进行某些具体的更改,使该探查器能够以 Windows 应用商店应用为目标。 探查器 UI 应使用新的 API 在调试模式下激活 Windows 应用商店应用。 确保探查器 DLL 仅使用适用于 Windows 应用商店应用的 API。 在编写探查器 DLL 与探查器 UI 之间的通信机制时,应牢记 Windows 应用商店应用 API 的限制,并知道 Windows 应用商店应用有权限限制。 探查器 DLL 应了解 CLR 如何处理 WinMD,以及垃圾回收器的行为对于托管线程有何不同。

资源

公共语言运行时

CLR 与 Windows 运行时的交互

Windows 应用商店应用程序