Flier's Sky

天空,蓝色的天空,眼睛看不到的东西,眼睛看得到的东西

导航

GAC 中的引用计数机制

Posted on 2004-10-01 00:16  Flier Lu  阅读(2155)  评论(1编辑  收藏  举报
原文:http://www.blogcn.com/User8/flier_lu/index.html?id=4046367

    在传统的 Windows 环境中,造成 DLL Hell 灾难的因素,除了不同版本 DLL 可以在不同目录下共存且被载入的优先顺序不可知以外,还有一大问题就是在卸载程序时如何对待程序使用到的公共 DLL。如果卸载程序顺手删掉放在 Windows/System32 目录下的自己用到的公共组件,则可能造成其他以来于此组件的程序无法正常使用;但如果置之不理,长此以往必然会造成 Windows 系统目录的无限制膨胀。对象我这样喜欢乱装软件的人来说,硬盘上 Windows 系统的生命期很大程度上取决于系统目录下垃圾的膨胀速度,呵呵。一般使用半年或者更长时间,Windows 就会膨胀到不可接受的地步,也就意味着痛苦的重装系统又要开始了。:P
    好在 CLR 在设计时就重复考虑到了这类问题,为 GAC 提供了基于引用计数的垃圾组件清扫机制。每个强签名组件在安装到 GAC 的时候,都可以指定一个引用数据;而在删除时,也将给出相同的引用数据,否则无法正常删除;同时 GAC 可以根据引用数据的内容判断某个组件是否仍然被使用,在合适的时候清除不被使用的垃圾组件。而这一切都归功于 GAC 的引用计数机制,Junfeng Zhang 在 GAC Assembly Trace Reference 一文中详细介绍了这一机制。
    而我在前端时间的一片文章《使用 Fusion API 控制 GAC》里面曾经做了错误的介绍,实在是当时没有很好理解这一概念。:P 再次感谢 Junfeng Zhang 的帮助,呵呵。

    在实现上,GAC 的 Fusion API 提供了 CreateInstallReferenceEnum 函数,能够获取针对某个 Assembly 的引用枚举器接口 IInstallReferenceEnum,进而通过其 IInstallReferenceEnum::GetNextInstallReferenceItem 方法获取引用项接口 IInstallReferenceItem,最终调用 IInstallReferenceItem::GetReference 方法获得引用数据 FUSION_INSTALL_REFERENCE 结构。
    上述函数和接口定义如下:
以下内容为程序代码:

public class Fusion
{
  [ComImport, Guid("582dac66-e678-449f-aba6-6faaec8a9394"[img]/images/wink.gif[/img],
    InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IInstallReferenceItem
  {
    [PreserveSig]
    int GetReference(
      out IntPtr ppRefData,
      uint dwFlags, IntPtr pvReserved);
  }

  [ComImport, Guid("56b1a988-7c0c-4aa2-8639-c3eb5a90226f"[img]/images/wink.gif[/img],
    InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IInstallReferenceEnum
  {
    [PreserveSig]
    int GetNextInstallReferenceItem(
      out IInstallReferenceItem ppRefItem,
      uint dwFlags, IntPtr pvReserved);
  }

  public const string DLL_NAME = "fusion.dll";

  [DllImportAttribute(DLL_NAME)]
  public static extern int CreateInstallReferenceEnum(
    out IInstallReferenceEnum ppRefEnum,
    IAssemblyName pName,
    uint dwFlags,
    IntPtr pvReserved);
}

    使用方法比较简单,唯一需要注意的是 IInstallReferenceItem::GetReference 方法取得的是一个指向 FUSION_INSTALL_REFERENCE 结构的指针,因此需要通过 Marshal.PtrToStructure 强行将被其指向内存转换为托管结构。
以下内容为程序代码:

public class ReferenceCollection : IEnumerable
{
private readonly AssemblyName _name;

private ArrayList _refs;

public void Refresh()
{
  if(_refs == null)
    _refs = new ArrayList();
  else
    _refs.Clear();

  Fusion.IInstallReferenceEnum refEnum;

  ComUtil.ComCheck(Fusion.CreateInstallReferenceEnum(out refEnum, _name._name, 0, IntPtr.Zero));

  Fusion.IInstallReferenceItem item;

  while(ComUtil.SUCCEEDED(refEnum.GetNextInstallReferenceItem(out item, 0, IntPtr.Zero)) && item != null)
  {
    IntPtr pRef;

    ComUtil.ComCheck(item.GetReference(out pRef, 0, IntPtr.Zero));

    Fusion.FUSION_INSTALL_REFERENCE objRef = (Fusion.FUSION_INSTALL_REFERENCE)
      Marshal.PtrToStructure(pRef, typeof(Fusion.FUSION_INSTALL_REFERENCE));

    _refs.Add(new InstallReference(objRef));
  }
}

    定义了引用数据的 FUSION_INSTALL_REFERENCE 结构,是 GAC 引用计数的核心数据所在。

    dwFlags 字段表示此引用计数的类型,目前来说只用到一个必设的 ASSEMBLYINFO_FLAG_INSTALLED 标志;
    guidScheme 字段内容是几个预定义 GUID 之一,表示此结构内容的意义,后面详细解释;
    szIdentifier 字段则是系统级别的应用程序标识符;
    szNonCannonicalData 字段则是应用程序级别的组件标识符;
以下内容为程序代码:

public class Fusion
{
  public enum ASSEMBLYINFO_FLAG
  {
    ASSEMBLYINFO_FLAG_INSTALLED       = 1,
    ASSEMBLYINFO_FLAG_PAYLOADRESIDENT = 2
  }

  public static readonly Guid FUSION_REFCOUNT_UNINSTALL_SUBKEY_GUID = new Guid("{8cedc215-ac4b-488b-93c0-a50a49cb2fb8}"[img]/images/wink.gif[/img];
  public static readonly Guid FUSION_REFCOUNT_FILEPATH_GUID         = new Guid("{b02f9d65-fb77-4f7a-afa5-b391309f11c9}"[img]/images/wink.gif[/img];
references when you remove this.
  public static readonly Guid FUSION_REFCOUNT_OPAQUE_STRING_GUID    = new Guid("{2ec93463-b0c3-45e1-8364-327e96aea856}"[img]/images/wink.gif[/img];
  public static readonly Guid FUSION_REFCOUNT_MSI_GUID              = new Guid("{25df0fc1-7f97-4070-add7-4b13bbfd7cb8}"[img]/images/wink.gif[/img];

  [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
  public struct FUSION_INSTALL_REFERENCE
  {
    public uint cbSize;                 // The size of the structure in bytes.
    public uint dwFlags;                // Reserved, must be zero.
    public Guid guidScheme;             // contains one of the pre-defined guids. The entity that adds the reference.
    public string szIdentifier;         // unique identifier for app installing this  assembly.
    public string szNonCannonicalData;  // data is description; relevent to the guid above. A string that is only understood by the entity that adds the reference. The GAC only stores this string.
  }
}

    当 guidScheme 内容为 FUSION_REFCOUNT_MSI_GUID 时,表示此 Assembly 是通过 MSI 安装并进行维护的,其他字段内容 szIdentifier = "MSI", szNonCannonicalData = "Windows Installer"。通过 gacutil /lr 命令我们可以发现绝大多数 .NET Framework 自带的 Assembly 都是通过这种方式安装的。不过在后面要提到的手工安装中并不能使用这个为 MSI 内置的 Scheme。
    当 guidScheme 内容为 FUSION_REFCOUNT_UNINSTALL_SUBKEY_GUID 时,szIdentifier 内容字符串被认为是系统添加删除程序面板中项目的 ID。如果 GAC 工具没有在注册表键 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall 下发现相同的 ID,则可以认为此 Assembly 的引用者已经不存在。
    当 guidScheme 内容为 FUSION_REFCOUNT_FILEPATH_GUID 时,szIdentifier 内容字符串被认为是一个文件名。如果 GAC 工具发现此文件不存在,则可以认为此 Assembly 的引用者已经不存在。
    最后当 guidScheme 内容为 FUSION_REFCOUNT_OPAQUE_STRING_GUID 时,Assemlby 的安装和引用者可以任意为 szIdentifier 和 szNonCannonicalData 赋值,自行维护 Assembly 的引用生命周期。

    在安装和卸载 Assembly 时,都有一个参数可以显式参数 pRefData 指定引用数据。
以下内容为程序代码:

[ComImport, Guid("e707dcde-d1cd-11d2-bab9-00c04f8eceae"[img]/images/wink.gif[/img],
  InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
  public interface IAssemblyCache
{
  [PreserveSig]
  int UninstallAssembly(
    uint dwFlags,
    [MarshalAs(UnmanagedType.LPWStr)] string pszAssemblyName,
    IntPtr pRefData,
    out ASM_UNINSTALL_DISPOSITION pulDisposition);

  //...

  [PreserveSig]
  int InstallAssembly(
    ASM_INSTALL_FLAG dwFlags,
    [MarshalAs(UnmanagedType.LPWStr)] string pszManifestFilePath,
    IntPtr pRefData);
}

    使用时可以忽略此参数,也可以使用上述的某种模式来管理引用的生命周期。为了不造成 GAC 的膨胀,推荐所有手工安装都指定一种引用模式。使用方式如下:
以下内容为程序代码:

  public class FusionUtil : ...
  {
    public void Install(string fileName, IntPtr pRef)
    {
      IAssemblyCache cache;

      ComUtil.ComCheck(Fusion.CreateAssemblyCache(out cache, 0));

      ComUtil.ComCheck(cache.InstallAssembly(Fusion.ASM_INSTALL_FLAG.IASSEMBLYCACHE_INSTALL_FLAG_REFRESH, fileName, pRef));
    }

    public void Install(string fileName)
    {
      Install(fileName, IntPtr.Zero);
    }

    public void Install(string fileName, InstallReference objRef)
    {
      IntPtr pRef = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(Fusion.FUSION_INSTALL_REFERENCE)));

      Marshal.StructureToPtr((Fusion.FUSION_INSTALL_REFERENCE)objRef, pRef, false);
      try
      {
        Install(fileName, pRef);
      }
      finally
      {
        Marshal.DestroyStructure(pRef, typeof(Fusion.FUSION_INSTALL_REFERENCE));

        Marshal.FreeCoTaskMem(pRef);
      }
    }

    public UninstallDisposition Uninstall(string assemblyName, IntPtr pRef)
    {
      IAssemblyCache cache;

      ComUtil.ComCheck(Fusion.CreateAssemblyCache(out cache, 0));

      ASM_UNINSTALL_DISPOSITION disposition;

      ComUtil.ComCheck(cache.UninstallAssembly(0, assemblyName, pRef, out disposition));

      return (UninstallDisposition)disposition;
    }

    public UninstallDisposition Uninstall(string assemblyName)
    {
      return Uninstall(assemblyName, IntPtr.Zero);
    }

    public UninstallDisposition Uninstall(string assemblyName, InstallReference objRef)
    {
      IntPtr pRef = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(Fusion.FUSION_INSTALL_REFERENCE)));

      Marshal.StructureToPtr((Fusion.FUSION_INSTALL_REFERENCE)objRef, pRef, false);
      try
      {
        return Uninstall(assemblyName, pRef);
      }
      finally
      {
        Marshal.DestroyStructure(pRef, typeof(Fusion.FUSION_INSTALL_REFERENCE));

        Marshal.FreeCoTaskMem(pRef);
      }
    }
  }


    完整的实现例子短期内可以从这里下载:::URL::http://flier.5i4k.net/GacUtilW.rar

btw: 感谢 Junfeng Zhang 在 Fusion 方面的强力支持