第 22 章 CLR 寄宿和 AppDomain

第 22 章 CLR 寄宿和 AppDomain

本章内容:

本章主要讨论寄宿和 AppDomain。寄宿使应用程序能利用CLR功能,并提供自定义和扩展的能力。而AppDomain则允许第三方代码在进程中运行,而CLR确保数据结构、代码和安全上下文不受破坏。

22.1 CLR 寄宿

开发 CLR 时,Microsoft 实际是把它实现成包含一个 DLL 中的 COM 服务器。

任何 Windows 应用程序都能寄宿(容纳)CLR。你的非托管宿主应该调用 MetaHost.h 文件中声明的 CLRCreateInstance 函数。CLRCreateInstance 函数在 MSCorEE.dll 文件中实现,这个 DLL 被人们亲切地称为垫片(shim),它的工作是决定创建哪个版本的CLR;垫片 DLL 本身不包含 CLR COM 服务器。

CLRCreateInstance 函数可返回一个 ICLRMetaHost 接口。宿主应用程序可调用这个接口的 GetRuntime 函数,指定宿主要创建的 CLR 的版本。然后,垫片将所需版本的 CLR 加载到宿主的进程中。

GetRuntime 函数返回指向非托管 ICLRRuntimeInfo 接口的指针。有了这个指针后,就可利用 GetInterface 方法获得 ICLRRuntimeHost 接口。宿主应用程序可调用该接口定义的方法来做下面这些事情。

  • 设置宿主管理器。告诉 CLR 宿主想参与涉及以下操作的决策:内存分配、线程调度/同步以及程序集加载等。宿主还可声明它想获得有关垃圾回收启动和停止以及特定操作超时的通知。

  • 获取 CLR 管理器。告诉 CLR 阻止使用某些类/成员。另外,宿主能分辨哪些代码可以调试,哪些不可以,以及当特定事件(例如 AppDomain 卸载、CLR停止或者堆栈溢出异常)发生时宿主应调用哪个方法。

  • 初始化并启动 CLR。

  • 加载程序集并执行其中的代码。

  • 停止 CLR,阻止任何更多的托管代码在 Windows 进程中运行。

注意 Windows 进程完全可以不加载 CLR,只有在进程中执行托管代码时才进行加载。
一个 CLR 加载到 Windows 进程之后,便永远不能卸载,CLR 从进程中卸载的唯一途径就是终止进程,这会造成 Windows 清理进程使用的所有资源。

22.2 AppDomain

CLR COM 服务器初始化时会创建一个 AppDomain。AppDomain 是一组程序集的逻辑容器。 CLR 初始化时创建的第一个 AppDomain 称为“默认 AppDomain”,这个默认的 AppDomain 只有在 Windows 进程终止时才会被销毁。
除了默认 AppDomain,正在使用非托管 COM 接口方法或托管类型方法的宿主还可要求 CLR 创建额外的 AppDomain。AppDomain 是为了提供隔离而设计的

图 22-1 演示了一个 Windows 进程,其中运行着一个 CLR COM 服务器。该 CLR 当前管理着两个 AppDomain,每个 AppDomain 都有自己的 Loader 堆,每个 Loader 堆都记录了自 AppDomain 创建以来已访问过哪些类型。Loader 堆中的每个类型对象都有一个方法表,方法表中的每个记录项都指向 JIT 编译的本机代码(前提是方法至少执行过一次)。

类型对象的内存不会由两个 AppDomain 共享。另外,一个 AppDomain 中的代码调用一个类型定义的方法时,方法的 IL 代码会进行 JIT 编译,生成的本机代码单独与每个 AppDomain 关联,而不是由调用它的所有 AppDomain 共享。

有的程序集本来就要由多个 AppDomain 使用。最典型的例子就是 MSCorLib.dll。为了减少资源消耗,MSCorLib.dll 程序集以一种AppDomain 中立的方式加载。也就是说,针对以“AppDomain 中立”的方式加载的程序集,CLR 会为它们维护一个特殊的 Loader 堆。该 Loader 堆中的所有类型对象,以及为这些类型定义的方法 JIT 编译生成的所有本机代码,都会由进程中的所有 AppDomain 共享。

22.3 卸载 AppDomain

卸载 AppDomain 会导致 CLR 卸载 AppDomain 中的所有程序集,还会释放 AppDomain 的 Loader 堆。卸载 AppDomain 的办法是调用 AppDomain 的静态 Unload 方法。

CLR 执行一系列操作来得体地卸载指定的 AppDomain。

  1. CLR 挂起进程中执行过托管代码的所有线程。

  2. CLR 检查所有线程栈,查看哪些线程正在执行要卸载的 AppDomain 中的代码,或者哪些线程会在某个时候返回至要卸载的 AppDomain。任何栈上有要卸载的 AppDomain,CLR 都会强迫对应的线程抛出一个 ThreadAbortException(同时恢复线程的执行)。。

  3. 当第 2 步发现的所有线程都离开 AppDomain 后,CLR 遍历堆,为引用了“由已卸载的 AppDomain 创建的对象”的每个代理对象都设置一个标志(flag
    )。这些代理对象现在知道它们引用的真实对象已经不在了。现在,任何代码在无效的代理对象上调用方法都会抛出一个 AppDomainUnloadedException异常。

  4. CLR强制垃圾回收,回收由已卸载的 AppDomain 创建的任何对象的内存。这些对象的 Finalize 方法被调用,使对象有机会正确清理它们占用的资源。

  5. CLR 恢复剩余所有线程的执行。调用 AppDomain.Unload 方法的线程将继续运行;对 AppDomain.Unload 的调用是同步进行的。