65.9K
CodeProject 正在变化。 阅读更多。
Home

CompilationCleaner:使用 CLR 接口进行高效文件搜索

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.52/5 (6投票s)

2005年8月7日

21分钟阅读

viewsIcon

49946

downloadIcon

596

用于清理多个编译的开发人员实用程序。

引言

CompilationCleaner是一个基于对话框的.NET应用程序,用于搜索非关键的编译器和链接器生成的文件,并将其显示在其ListView中以供可能删除。这类似于各种编译器的“清理”选项,不同之处在于可以搜索多个路径,并且您不必删除所有找到的文件。这允许批量删除编译器垃圾,开发人员通常在反复编译时不会删除这些垃圾。

使用CompilationCleaner可以删除数千个千兆字节的文件,特别是如果您像我一样例行编译数十个项目。我第一次使用,在连续编译多年后,删除了几个GB和数千个文件。Visual Studio生成的中间文件比大多数编译器都多,因此它可以回收大量磁盘空间。

由于CompilationCleaner本质上是一个通用的文件搜索器,可以测试文件签名,因此它可以用于查找和/或删除任何类型的文件,例如GIF、BMP、数据库等。

安全第一

尽管CompilationCleaner在关键领域设计为高效,但安全性是其首要关注点。仅仅搜索和删除具有特定扩展名的文件可能是灾难性的!许多项目(尤其是便携式或面向Unix的项目)包含具有不一致扩展名的文件。例如,Unix世界通常不遵守Windows文件扩展名。

CompilationCleaner在列出文件之前会检查每个文件的签名,除非您明确指定没有关联签名的文件模式(在可避免的情况下强烈不建议这样做)。几乎所有Visual Studio中间文件都具有独特的签名。

关于代码

CompilationCleaner是用托管和非托管C++编写的。几年前,它作为托管C++的实验而诞生,但直到Visual Studio .NET 2003使MC++更容易实现后才被搁置。我最初不打算使用任何非托管C++,因为它本应是一个快速而粗糙的实用程序(我们多久做出一次这样的假设?)。但由于性能不佳,关键文件I/O此后已在原生代码中实现。

虽然语法是托管C++,但它也可以在即将推出的Visual Studio 2005中编译。GUI代码可以通过文本替换操作转换为C#或Visual Basic,但文件操作必须保留在混合的托管/非托管C++中。

注意:一个版本在VC++ 8 Beta上进行了测试,它编译并运行了,但新的CLR仍然存在错误。

性能

对C#和MC++文件搜索的经验暴露了CLR文件I/O的性能问题。可能的罪魁祸首是将大量数据在托管代码之间进行封送的需要。因此,所有文件搜索和签名验证都是通过Win32 API在非托管和混合C++类中完成的。通过这些更改,即使是DEBUG版本,速度也提高了几个数量级。

非托管代码依赖于UNICODE宏,这提高了速度并减少了代码膨胀。这应该不是问题,因为VS.NET甚至不在Win9x平台上运行,尽管您可以编译一个非Unicode版本用于此类系统,用于非VS.NET编译器(ANSI代码已就位)。

使用CompilationCleaner

由于CompilationCleaner首先是一个开发人员实用程序,我将解释如何使用它,然后再深入研究代码。此外,对于大多数人来说,它的实用性比代码细节更重要,尤其是那些不熟悉托管或常规C++的人。

CompilationCleaner使用简单,并且与Visual Studio 2005 beta保持同步。默认情况下,您的项目目录将预加载到搜索路径的ComboBox中(如果您安装了VS 8.0、7.1、7.0或6.0)。您可以随时添加或替换目录,它们将在应用程序退出时保存。“添加路径”按钮比浏览按钮更方便,因为它会附加搜索路径,并带有分号。要获得典型的浏览行为,只需先删除ComboBox文本即可。

“活动文件过滤器”组合框用于选择扫描时要使用的文件过滤器集合。您可以通过单击“修改过滤器”按钮来添加、删除、重命名或重新排列过滤器。这将弹出以下模式对话框

上方的ListBox包含命名的过滤器集合,下方的ListBox列出了当前选定集合包含的过滤器组。“添加”、“删除”和“重命名”按钮是标准配置。TextBox和CheckBox标签有鼠标悬停工具提示,以防您忘记如何使用它们。如果这是一个商业产品,我还会创建一个HTML帮助文件。

最重要的字段是“文件签名”和“文件匹配模式”文本框。您必须至少输入一个有效的文件模式,或多个由空格分隔的模式(以标准Windows方式)。CompilationCleaner会尝试阻止您指定无效或空的模式。如果您这样做,您将收到错误提示,并且在更正模式之前无法执行任何其他操作,除非“取消”或“恢复默认”。

目前,文件匹配模式规则是:

  • 仅ASCII字符。
  • 除*外,不允许使用非法文件名字符。
  • 每个模式只能使用一个*通配符。
  • 多个模式应由空格、逗号或制表符分隔。
  • 单独的*不是有效模式,因为无法进行特定匹配。

有效模式的示例是

*.bsc    myfile.*   std*.h   input*

到目前为止,最可能的模式是扩展名,如上图所示。

文件签名

最重要的功能可能是指定文件签名的能力。当是二进制时,这些有时被称为魔术数字。不必指定签名,但仅通过名称或扩展名搜索文件可能不可靠。

许多Visual Studio生成的文件都具有ASCII签名,例如*.bsc文件的“Microsoft C/C++”。一些编译器生成的文件具有二进制签名,必须以十六进制指定。这通过为每个字节输入一个十六进制字符对(按数组顺序),并用一个空格、逗号或制表符分隔(与文件模式一样)来完成。十六进制字符可以是大小写字母的任意组合。您还必须选中“签名是十六进制”复选框,这将使文本以突出显示的颜色显示。例如

您可以通过打开多个相同文件类型的不同实例来确定文件的签名,最好在十六进制编辑器中进行。如果某种类型的文件具有签名,则所有此类文件的初始字节将相同。如果字节可读,它们可能是ASCII,否则您应该复制十六进制编辑器中显示的十六进制对,并将签名指定为二进制。

删除编译器垃圾

选择路径和模式集合后,单击“扫描垃圾”(或按ENTER键)以启动扫描。如果您希望停止正在进行的扫描,请再次单击同一按钮(扫描期间它将以红色重新标记为“停止扫描”),或按ESCAPE键。结果如下所示:

最初,所有条目都未标记,强制您选择要删除的文件。ListView有一个右键上下文菜单,允许您检查/取消检查选定的文件以及其他有用的功能。您还可以单击列标题进行排序。

注意:CLR v1.x ListView会自动检查范围选择(使用SHIFT时),因此请注意这一点。

在上图中,请注意我没有检查任何 .pdb (程序数据库) 文件,因为我有时希望保留它们用于调试目的。另一个您可能需要谨慎删除的经过签名验证的 VS 生成文件是 .obj 文件,因为有时我们会为了静态库使用而编译它们。在所示的过滤器集合中,其他所有内容都是可消耗的。

当您已勾选要删除的文件后,点击“清理已勾选”,这些文件将从ListView和您的硬盘中删除。已删除的文件不会放入回收站。

代码

CompilationCleaner 的 GUI 界面是纯 CLR 的,大部分是标准配置,只有几个值得一提的亮点。我们首先介绍这些,然后处理本地 C++ 文件搜索代码的集成。

通用ListView排序器

一个值得注意的 GUI 类是 LvSorter.h 中的 LVSorter,它为多种数据类型对 ListView 列进行升序和降序排序。它继承自 IComparer,因此可以将其分配给 ListView::ListViewItemSorter 属性。它用于窗体的标题点击处理程序中

void LV_ColumnClick(Object *pSender, 
            ColumnClickEventArgs *pColClickArgs) {

   //if already created (most likely)
   if (m_pLvSorter != NULL) {     

      m_pLvSorter->SetColumn(pColClickArgs->Column);
      m_pLV->Sort();

   }else{   //create and set the sorter just once

      m_pLvSorter = 
          __gc new LVSorter(pColClickArgs->Column);
      
      //this also inits a sort
      m_pLV->ListViewItemSorter = m_pLvSorter;  
   }
}

你可以只使用 else 块中的代码,正如 MSDN 文档所建议的那样,但我厌恶毫无理由地在 GC 堆上创建大量排序器对象。此外,这意味着上次已知的排序顺序将不会保留,因此排序顺序不会那么容易地切换。MSDN 建议保留一个 SortOrder 数据成员,但这表示我们还需要跟踪上次排序的列。我选择了封装,因此只有一个成员对象:m_pLvSorter

请注意,当您为 ListView::ListViewItemSorter 属性赋值时,ListView::Sort() 会被有效地调用(在 Win32 术语中,LVM_SORTITEMSEX)。如果此属性已设置,则必须首先使用 LVSorter::SetColumn() 设置列,以便 ListView::Sort() 将对正确的列进行排序。为什么 ListView 设计者决定不将列索引(ColumnClickEventArgs::Column)传递给所需的 Compare() 方法,我无法理解。如果他们改为指定继承自使用列进行排序的接口,LVSorter 将会简单得多,就像我的 MFC 和 WTL 版本的 LVSorter 一样,讽刺的是它们更容易使用。

该类本身实现为

#define dcast dynamic_cast   //Bjarne Stroustrup intentionally 
                             //made casts annoying to use, but 
                             //I'm a realist

private __gc class LVSorter : public IComparer {
private:
   int       m_iCol;
   SortOrder m_order;

public:
   LVSorter() : m_iCol(-1), 
               m_order(SortOrder::Ascending) {} //invalid

   LVSorter(int iColumn) : m_iCol(iColumn), 
                         m_order(SortOrder::Ascending) {}

   void SetColumn(int iColumn) {

      //if different column ascend on 
      //first sort of a column
      if (iColumn != m_iCol)    
                                
         m_order = SortOrder::Ascending;  
      else
         //toggle order
         m_order = (SortOrder)((int)m_order ^ 3);    

      m_iCol = iColumn;
   }

    //required implementation:
   int Compare(Object* pA, Object* pB) {

      typedef System::Windows::Forms::
           ListViewItem::ListViewSubItem LvSubItemT;

      LvSubItemT *pSubA = 
        (dcast<ListViewItem*>(pA))->SubItems->Item[m_iCol];
      LvSubItemT *pSubB = 
        (dcast<ListViewItem*>(pB))->SubItems->Item[m_iCol];

      int iRet;
      switch (m_iCol) {
      case 0:     //filename
      case 1:     //path
      case 2:     //extension
      String_Comparison:
         iRet = String::Compare(pSubA->Text, pSubB->Text);
         break;

      case 3:  //file size (works since no 
               //suffix like KB or MB)
               //Due to thousands separators, 
               //we have to do this special:
         iRet = (int)(Int64::Parse(pSubA->Text, NumberStyles::Number) - 
                      Int64::Parse(pSubB->Text, NumberStyles::Number));
         break;

      case 4:     //modified time (text parsing is slow, but works)
         try {
            iRet = DateTime::Compare(DateTime::Parse(pSubA->Text),
                                     DateTime::Parse(pSubB->Text));
         }catch (...) {
            goto String_Comparison;
         }
         break;

      default:
         Debug::Assert(0, 
             S"Need to implement sorting on all columns");
         goto String_Comparison;
      }

      if (m_order == SortOrder::Descending)
         iRet = -iRet;

      return iRet;
   }
};

请注意,不应使用默认构造函数,但它存在是因为 .NET 需要它。

SetColumn() 存在是因为列索引没有传递给所需的 Compare() 方法。当点击不同的列时,排序顺序设置为升序。当连续点击同一列时,顺序切换,这是标准的 Windows 行为。请注意,使用 CLR 在 ListView 标题上绘制箭头图像比平时更麻烦,所以我没有费心。

Compare() 传递的是泛型对象指针,这再次让我无法理解。除了 ListViewItem 之外,没有其他可能的类型可以传递,因此这种松散类型实现需要动态转换为实际类型(即使在 C# 中也是如此)。正如您所看到的,我根本不喜欢泛型类型,所以我迫不及待地等待 VS 2005 发布正式版(尽管 ListView 接口没有改变)。这个方法与 CLR 中几乎所有其他 Compare() 方法完全相同,但我无法想到任何好的理由;它对用户不友好。

CLR 日期解析有点笨拙,因此日期排序速度不快,但尚可接受。如果这是一个商业产品,我会将 FILETIME 值的 Int64 分配给每个项目的 Tag 属性,这将允许更快的代码进行时间排序

try {
   Int64 iA = *reinterpret_cast<Int64*>(
               (dcast<ListViewItem*>(pA))->Tag);
   Int64 iB = *reinterpret_cast<Int64*>(
               (dcast<ListViewItem*>(pA))->Tag);
   const Int64 i64diff = (iA - iB);
   iRet = (i64diff > 0) ? 1 : (i64diff < 0) ? 
                                         -1 : 0;
}catch (...) {
   goto String_Comparison;   //in case it fails
}

但现有的解析代码足以满足这个项目。

一个更好的消息框

NiceMsgBox.h 包含一个我多年来一直在使用的便利封装。类 NiceMsgBox::MsgBox 包含 static 方法,以避免冗长的编码,例如

DialogResult dr = MessageBox::Show(this, 
             S"Question text", S"Dialog Caption",
             MessageBoxButtons::YesNo,
             MessageBoxIcon::Question,
             MessageBoxDefaultButton::Button2);

而是允许

DialogResult dr = MsgBox::Question(this, 
             S"Question text", S"Dialog Caption",
             Btn::YesNo, Default::Btn2);

最简单的是

MsgBox::Info(this, S"message"); //this->Text is 
                                //the caption

我更喜欢更简洁明了的代码,只要它不牺牲可读性。代码本身非常简单,除了解释调用之外,不值得详细介绍,您将在项目的许多代码中看到这些调用。我为 C# 使用了一个类似的类。

按模式和签名查找文件

GatherFilenames.h 中,托管公共类 MFindFilesByPatternAndSig 提供了文件搜索的 CLR 接口,可用于任何 CLR 应用程序。它不仅包含 CLR 封送代码,还包含 Win32 文件查找代码,这就是 CompilationCleaner 不会慢得令人痛苦的原因,实际上它出奇地快(现在主要受 CLR ListView 的限制)。用于枚举文件和目录的 Win32 代码是标准配置,主要侧重于效率而不是政治正确的编码方式。因此,我不会在此处详细介绍这些方法。

纯C++类_CFilePatternMatcher封装了用于匹配文件的Win32代码,并专门由MFindFilesByPatternAndSig使用。由于这两个类相互操作,因此在此将它们的声明放在一起以便于阅读

typedef ::std::basic_string<TCHAR> StrT;
typedef ::std::basic_string<BYTE>  ByteStrT;
typedef ::std::vector<StrT>        StrVecT;

//==================================================
// This class, much like a regex pattern 
//  match class, finds matches to file
//  patterns. Unlike regex patterns, a file 
//  signature may also be specified, so
//  that a pattern-matching file will also be 
//  checked for correct signature.
__nogc class _CFilePatternMatcher {

   enum EPatternFlags {
      k3LtrExt    = 1,   //special suffix, since we  
                         //do integer comparison
      kEndsWith   = 2,
      kStartsWith = 4,
      kWholeMatch = 8,
   };

#ifdef UNICODE
   typedef  UINT64  _FourChrUInt;
   typedef  wchar_t UnsignedChrT;
#else
   typedef  UINT32  _FourChrUInt;
   typedef  BYTE    UnsignedChrT;
#endif

public:
   typedef ::std::vector<ByteStrT> ByteStrVecT;

   struct Pattern {
      //type of comparison (see EPatternFlags)
      size_t uFlags; 
      //name prefix or entire name    
      StrT   sPrefix; 
      //suffix (usually an extension)   
      StrT   sSuffix;   
      //signature group number 
      size_t uSigID; 
      //index of relevant signature in m_vSigs    
      size_t uSigIdx;    
   };

   typedef ::std::vector<Pattern>  PattVecT;

   PattVecT    m_vPatts;
   ByteStrVecT m_vSigs;

public:
   _CFilePatternMatcher() {}

   static void ParsePatternStr(LPCWSTR pwzPatt, 
                               Pattern &patt);
   bool FileMatches(const WIN32_FIND_DATA &fd, 
                            size_t &uSigIDRet);
   
};//_CFilePatternMatcher __nogc class

//============================================
// Managed interface to native 
// Win32 file searching.
public __gc class MFindFilesByPatternAndSig {

public:
   //data passed back to GUI thread:
   __gc struct ManagedFileData {   
      //file's name
      String *psFName;
      //path where file was found             
      String *psPath;              
      //file size
      UInt64  uSize;               
      //last modified time (FILETIME)
      UInt64  ftMod;               
      //signature group this file belongs to
      UInt32  uSigGroup;           
      //Win32 file attributes bit field
      UInt32  uAttribs;            
   };

   //data passed to this class:
   __gc struct ManagedPatternGroup {   
      UInt32  uSigID;
      Byte    signature __gc[];
      String* aPatts    __gc[];
   };

    //Delegate type used to marshal found file 
    //data back to GUI thread:
   __delegate void DelFileCB(ManagedFileData *pFD);

private:
   _CFilePatternMatcher      *m_pFPM;      //__nogc
   DelFileCB                 *m_pDelFileCB;
   ManagedFileData           *m_pFileData;
   IAsyncResult __gc*volatile m_pRes;
   volatile bool       __nogc*m_pbStop;

public:
   explicit MFindFilesByPatternAndSig(
          ManagedPatternGroup* aPattGrp __gc[],
          bool *pbStop);

   //trashes non-gc heap stuff
   ~MFindFilesByPatternAndSig();  

   UInt32 FindFiles(String *psDir, bool bSubdirs, 
                           DelFileCB *pDelFileCB);

private:
    //NOTE: This is recursive
   size_t _FindDeeperFiles(LPCTSTR ptzSubDir, 
                  ::std::wstring &wsSubPath);

   static void _AppendSubDir(::std::wstring &wsSubPath, 
                                    LPCTSTR ptzSubDir);

};//MFindFilesByPatternAndSig

MFindFilesByPatternAndSig 的性能以及与 GUI 互操作的关键在于用于在 GUI 和本机代码之间封送数据的最小数据结构。结构 ManagedFileData 是唯一被封送回 GUI 线程的数据。ManagedPatternGroup 仅用于将初始化数据传递给 MFindFilesByPatternAndSig

纯 CLR 实现需要对每个枚举的文件进行大量数据封送,而 MFindFilesByPatternAndSig 只将数据传回 匹配 的文件。它通过将 ManagedFileData 对象传递给其 DelFileCB 委托 m_pDelFileCB 来实现此目的(有关详细信息,请参阅同步和性能详细信息)。委托由 GUI 实现,并通过其唯一的公共方法 FindFiles() 传递给 MFindFilesByPatternAndSig,同时还传递要搜索的路径以及是否搜索子目录的布尔值。

文件名模式和文件签名匹配

非托管 C++ 类 _CFilePatternMatcher(也在 GatherFilenames.h 中)是 MFindFilesByPatternAndSig 如何查找特定文件的关键。当 MFindFilesByPatternAndSig 枚举文件而不是目录时,它将 WIN32_FIND_DATA 传递给 _CFilePatternMatcher::FileMatches(),后者执行困难的工作。在我们讨论其工作原理之前,我们需要讨论 _CFilePatternMatcher 如何由 MFindFilesByPatternAndSig 通过以下方式初始化:

static void _CFilePatternMatcher::ParsePatternStr(
                     LPCWSTR pwzPatt, Pattern &patt);

当创建 MFindFilesByPatternAndSig 的实例时,其显式构造函数会在非托管堆上创建 _CFilePatternMatcher 的实例 m_pFPM。将模式解析代码纯粹放在 _CFilePatternMatcher 的构造函数中被证明是困难的,因为它对 CLR 一无所知。因此,MFindFilesByPatternAndSig 的构造函数解码并封送 CLR 数据,然后调用 ParsePatternStr() 以将非托管模式和签名信息加载到 m_pFPM 中。

由于 CLR 字符串始终是 Unicode,ParsePatternStr() 只接受 Unicode 字符串(在传入之前会转换为小写)。尽管 _CFilePatternMatcher 会将模式解析为 ANSI 或 Unicode(取决于编译目标),但它目前不期望非 ASCII 字符。我可以使用 C 转换函数进行 ANSI 构建,但它很丑陋且没有真正必要,因为编译器中间文件的文件名和扩展名始终是 ASCII。

如果需要将此类别移植以实现真正的 ANSI 字符串兼容性,可以使用 WideCharToMultiByte() 将传入的 Unicode 字符串(pwzPatt)复制到 char 缓冲区中,用于 ANSI 构建。但是,这并非完整的解决方案,除非您还处理 MBCS 字符的可能性,因为在这种情况下,您不能假设每个字符一个字节(最多可能每个字符五个字节)。这将导致 isleadbyte() 和相关的多字节丑陋,或其他变通方法。即使您这样做,如果存在多字节字符,快速的“3 字符扩展名”检测也会失效。因此,如果您确实需要此类支持,我将其留作“读者的练习”(我确实是指 练习)。

我们不需要详细介绍 ParsePatternStr(),因为它没有什么新意,有点复杂,而且注释也相当充分。简而言之,其思想是查找 L'*' 通配符和 L'.' 字符,并确定模式是引用文件名前缀(前导字符)、后缀(结尾字符)还是全名匹配。如果是后缀,并且它看起来像一个带 3 个字母扩展名的点(最常见的情况),我们可以进行快速匹配验证。小写字符串数据和标志存储在非托管 Pattern 结构的 std::vector 中。

这又把我们带回到文件检测的核心(为了清晰起见,一些调试代码已被移除)

bool _CFilePatternMatcher::FileMatches(
    const WIN32_FIND_DATA &fd, size_t &uSigIDRet) {

   ATLASSERT(*fd.cFileName != 0);   //shouldn't be
   //FindNextFile() doesn't give len
   const size_t uLen = ::lstrlen(fd.cFileName);    

   const size_t uBytes = 
        (uLen * sizeof(TCHAR)) + sizeof(TCHAR);
   LPTSTR ptzName = (LPTSTR)_alloca(uBytes);
   //copy null character too
   memcpy(ptzName, fd.cFileName, uBytes); 
   //lower-case the string 
   _tcslwr(ptzName);                       
   LPCTSTR ptzNameEnd = (ptzName + uLen);

   for (size_t u = 0; u < m_vPatts.size(); u++) {

      const Pattern &patt = m_vPatts[u];
      //rule-out option that excludes others
      if (patt.uFlags != kWholeMatch) {  

         //by far most common
         if (patt.uFlags & k3LtrExt) {   

            if (uLen < 5) //if can't be 3 ltr ext 
                          //with 1 ltr name and a dot
               continue;
            LPCTSTR ptzLast4Chrs = (ptzNameEnd - 4);
            //compare last 4 characters as one integer:
            if (*(const _FourChrUInt*)ptzLast4Chrs !=
                *(const _FourChrUInt*)patt.sSuffix.c_str())
               continue;

         }else if (patt.uFlags & kEndsWith) {

            const size_t uPostLen = patt.sSuffix.length();
            if (uLen < uPostLen)
               continue;

            if (memcmp(patt.sSuffix.c_str(), 
                       ptzNameEnd-uPostLen,
                       uPostLen*sizeof(TCHAR)) != 0)
               continue;
         }

         if (patt.uFlags & kStartsWith) {

            const size_t uPreLen = 
                         patt.sPrefix.length();
            if (uLen <= uPreLen)
               continue;

            if (memcmp(patt.sPrefix.c_str(), 
                 ptzName, uPreLen*sizeof(TCHAR)) != 0)
               continue;
         }
      }else{  //otherwise do whole-name match

         if (uLen != patt.sPrefix.length())
            continue;

         if (memcmp(patt.sPrefix.c_str(), 
             ptzName, uLen*sizeof(TCHAR)) != 0)
            continue;
      }

      //If got this far, file will likely 
      //match, so set OUT param now:
      uSigIDRet = patt.uSigID;
      //If file len is zero, it has no signature, 
      //but consider it a match
      if (fd.nFileSizeLow == 0 && 
                           fd.nFileSizeHigh == 0)
         return true;

       //Test signature, if any
      ATLASSERT(patt.uSigIdx < m_vSigs.size());
      const ByteStrT &bsSig = m_vSigs[patt.uSigIdx];
      if (!bsSig.empty()) {

         HANDLE hFile = ::CreateFile(fd.cFileName,
                        GENERIC_READ | FILE_WRITE_ATTRIBUTES,
                        FILE_SHARE_READ, NULL, OPEN_ALWAYS, 
                        0, NULL);

         if (hFile != INVALID_HANDLE_VALUE) {

            DWORD dwRead;    //required by Win9x
            const DWORD dwSigBytes = (DWORD)bsSig.size();
            BYTE *pRead = (BYTE*)_alloca(dwSigBytes);
            BOOL bOK = ::ReadFile(hFile, pRead, 
                          dwSigBytes, &dwRead, NULL);

            ::SetFileTime(hFile, NULL,   //restore access time
               &fd.ftLastAccessTime, NULL);  
            ::CloseHandle(hFile);

            if (bOK && dwSigBytes == dwRead &&
                memcmp(bsSig.c_str(), pRead, dwSigBytes) == 0)
               return true;

         }else{ //otherwise skip it, and just debug report it
            ATLTRACE(_T("Unable to open file: %s\n"), 
                                          fd.cFileName);
         }
      }

   }//for each pattern

   return false;
}

此方法在文件枚举期间反复调用,因此必须相当快。我注意到的唯一显著瓶颈总是将文件名转换为小写,但至少我们将其放入堆栈缓冲区 ptzName 中,该缓冲区通过 _alloca() 分配以最大程度地减少堆栈增长。为什么不直接使用 lstrcmpi()?首先,该函数期望完整的以空字符结尾的字符串,而我们有时需要进行“BeginsWith”或“EndsWith”样式的比较,它们比较部分字符串。其次,我们的模式字符串已经是小写,那么为什么在只有一个需要调整大小写的情况下调用一个缓慢的不区分大小写的函数呢?此外,memcmp() 非常快。第三,迄今为止最常见的比较不是按字符比较,而是比较最后4个字符的整数块,用于点+3扩展名。这非常快,不需要遍历任何字符串。

总的来说,代码相当简单。将文件名转换为小写后,我们遍历存储在向量 m_vPatts 中的所有模式,并查看需要进行哪种比较。我们最常进行刚刚描述的快速扩展名检查,否则我们进行简单的 memcmp() 调用。如果文件通过所有模式测试,我们检查文件大小。如果为零,我们无法检查签名,但我们说它匹配,因为文件无论如何都无用。否则,我们进入签名检查代码。

到目前为止,最慢的部分是读取文件的签名,但是当我们到达那个点时,匹配的可能性非常大,所以它不会经常执行。如果有要验证的签名,我们只需读取文件的初始字节,并将其与当前模式关联的签名进行比较。请注意,如果打开 Visual Studio,它的一些文件将被锁定且无法读取,因此没有删除打开项目中活动、有签名文件的危险。

您可能会注意到我们同时以 GENERIC_READFILE_WRITE_ATTRIBUTES 方式打开文件。这允许我们恢复每个文件的上次访问时间。体贴的文件搜索器会这样做,因为文件扫描不像编辑或处理文件那样是完全访问。备份、碎片整理和其他实用程序可以根据文件访问时间进行操作,我们不希望给人一种错误的印象,即我们嗅探的文件经常被使用。

实现工作线程

最后值得注意的 GUI 代码是一段私有的托管类 _FinderThread,它实现在主窗体代码中。它只是封装了所需的线程数据和线程过程,以便工作线程可以调用文件查找类(如上文所述的 MFindFilesByPatternAndSig)。我通常在我的应用程序中保留一个工作线程队列(效率更高,因为线程创建可能代价高昂),但这段代码适合 CompilationCleaner 的目的

 //This wraps thread data and the thread proc:
__gc class _FinderThread {
public:
   __delegate void DelSrchDoneCB();

private:
   MFindFilesByPatternAndSig::DelFileCB *m_pDelCB;
   MFindFilesByPatternAndSig::
          ManagedPatternGroup *m_pPattGrps __gc[];
   DelSrchDoneCB *m_pSrchDoneCB;
   String        *m_aPaths __gc[];
   bool           m_bSrchSubDirs;
   bool          *m_pbStop;

public:
   _FinderThread(MFindFilesByPatternAndSig::
                 DelFileCB *pDelCB,
                 DelSrchDoneCB *pSrchDoneCB,
                 MFindFilesByPatternAndSig::
                   ManagedPatternGroup *pPattGrpArr __gc[],
                   String* aPaths __gc[], 
                   bool bSrchSubDirs, bool __nogc*pbStop)
    : m_pDelCB(pDelCB), m_pSrchDoneCB(pSrchDoneCB), 
              m_aPaths(aPaths), m_pPattGrps(pPattGrpArr), 
              m_bSrchSubDirs(bSrchSubDirs), 
              m_pbStop(pbStop) {}

   void ThreadProc() {

      MFindFilesByPatternAndSig *pGF;
      pGF = __gc new MFindFilesByPatternAndSig(
                                m_pPattGrps, m_pbStop);
      for (int i = 0; i < m_aPaths->Length; i++)
         pGF->FindFiles(
            dcast<String*>(m_aPaths->Item[i]), 
                               _bSrchSubDirs, m_pDelCB);

      m_pSrchDoneCB->Invoke();  //gather stats
   }
};

您可能会注意到频繁使用 GC 数组,因为我试图避免过多无类型的容器,例如 ArrayList,它们可能会很慢。回想起来,GC 数组处理起来很痛苦,不像普通的 C++ 数组,但我不打算在完成所有这些工作后重写所有内容,只是为了避免令人困惑的语法(在 VS 2005 中会更好),这在 C# 中更简洁一些。

构造函数接收运行搜索所需的线程数据,并简单地将其复制到成员指针中。有两个委托参数,第一个用于将找到的文件数据回调给 GUI 线程(详见下文),第二个在所有搜索完成后调用,用于收集统计信息。GC 数组 pPattGrpArraPaths 分别描述了要搜索的文件类型以及要在哪些路径下搜索它们。ManagedPatternGroup 结构定义为

__gc struct ManagedPatternGroup {
   //ID for this grouping of patterns and signature
   UInt32  uSigID;               
   //binary or ASCII signature of file
   Byte    signature __gc[];     
   //filename pattern(s), such as "*.pch"
   String* aPatts    __gc[];     
};

第一个字段主要用于将来的修改,以在回调文件数据时识别文件具有哪个签名。签名本身是一个字节数组,因此可以是二进制或 ASCII(Visual Studio 文件通常具有 ASCII 签名)。如果数组长度为零,则不会检查签名。“模式”数组 aPatts 包含一个或多个用于查找合适文件名的字符串(通常通过扩展名),但存在限制(在使用 CompilationCleaner 中有详细说明)。需要模式限制以提高搜索效率,因为 Win32 的 FindFirstFile() 及其相关函数无法接受多个模式,也无法在按模式查找时枚举目录。

回到构造函数,参数 bSrchSubDirs 指定是否搜索子目录,pbStop 是一个指向布尔值的非 GC 指针,将其设置为 true 可以轻松停止工作线程。在这种简单情况下,不需要花哨的同步对象,例如事件或临界区,因为没有要锁定的数据(所有数据都已封送)。

线程过程创建一个 MFindFilesByPatternAndSig 实例,并为每个要搜索的路径启动一个搜索。当所有搜索完成后,“搜索完成”委托 m_pSrchDoneCB 将被调用以收集和显示统计信息。

同步和性能详情

MFindFilesByPatternAndSig 设计为在工作线程上运行(如上所述),并使用其委托的 BeginInvoke()EndInvoke() 方法以异步方式安全地将数据传回 GUI 线程。顺便说一句,指向同一个 GC ManagedFileData 结构 m_pFileData 的指针在整个搜索操作中都被使用。这并非因为 GC 堆分配速度慢,它实际上非常快,而是为了避免在堆上分配大量临时数据(即使如此也需要清理)。性能优势微乎其微,如果为每个回调分配新对象,我们就不必担心数据同步。但是,我们仍然需要等待 EndInvoke(),以便在 GUI 线程尚未消费之前发送更多数据。

私有方法 _FindDeeperFiles() 只是 FindFiles() 的一个更精简版本,仅在启用子目录搜索时调用。它使用私有静态方法 _AppendSubDir() 作为一种非常快速的方法,将子目录附加到基于 wchar_t 的 C++ 字符串。通过始终使用 Unicode 字符串,封送到 CLR String 变得简单明了。实际上,字符串从未被重新分配(非托管堆速度较慢),只是有效地“截断”回其先前的长度,因为 _AppendSubDir() 沿着目录树向上回溯。为了提供更大的多功能性,提供了用于 ANSI Win9x 平台的编译功能,尽管 CompilationCleaner 最初的目标是运行 VS.NET 的基于 NT 的平台。

持久数据存储

长期以来,关于是将应用程序数据存储在 INI 文件、Windows 注册表、COM 文档、XML 文件还是序列化数据文件中存在争议。我使用了后者,因为在 INI 文件中保存二进制数据很困难,注册表已经过度使用(访问不断下降),文档是面向 MFC 的并且在这里是杀鸡用牛刀,而 XML 与二进制存储相比速度慢且使用繁琐,尽管如果我们需要进行互联网传输,它会是一个不错的选择。即使是微软,现在也大量使用序列化。

托管类 MStorage(在 Storage.h 中)封装了数据在磁盘上的 CLR 序列化。它定义了三个数据结构,如下面这个简写声明所示

public __gc class MStorage {
public:
    //Very small and efficient struct 
    //that gets passed around a lot.
    // Not a class for efficiency when 
    //passing to unmanaged code.
   [Serializable]
   //no generic objects in this struct!
   __gc struct PattGroup {       
      
      String *psName;
      //array of pattern strings
      String* aPattStrs __gc[];  
      //array of signature bytes
      Byte    aSig __gc[];       
      //whether signature is binary or not
      bool    bBinSig;           
      //signature ID, for future use
      UInt32  uSigID;            
   };

   [Serializable]
   //a named file pattern "collection"
   __gc class PattColl {       
   public:
      String    *psName;
      //string of concatenated filter patterns, for show
      String    *psAllPatts;   
      //actually PattGroup objects
      ArrayList *pGroups;      
      //default ctor (null ptrs)
      PattColl() {}            

       //Shallow-copy ctor (copies references)
      PattColl(String *psCollName, 
           String *psAllPattStrs, ArrayList *pGrpList)
       : psName(psCollName), psAllPatts(psAllPattStrs), 
                                     pGroups(pGrpList) {}

      PattColl(PattColl *pToCopy);  //deep copy ctor
   };

   [Serializable]
   //parent serialised type
   __gc struct StoredData {      
      //PattColl objs, which contain PattGroup objs
      ArrayList *pColls;         
      //paths to search
      ArrayList *pPaths;         
      //last active filter set
      int iActiveFilter;  
      //NOTE: You can add as many fields 
      //as you want here...
   };
   //THE serialised object ******
   StoredData *m_pStore;  
   //default ctor
   MStorage() : m_pStore(__gc new StoredData()) {}  


   void LoadFromDisk() {
       //Find the roaming profiles dir.
       //To make this simpler, we do as many 
       //simpler apps do, and just store
       // one data file in the %USERPROFILE% dir.
      String *psProfDir = 
         Environment::ExpandEnvironmentVariables(
                                  S"%USERPROFILE%");
      FileInfo *pFI = 
         __gc new FileInfo(String::Concat(psProfDir, 
                                S"\\CompClean1.dat"));

      m_pStore = NULL;

      if (pFI->Exists) {

         BinaryFormatter *pFmtr = 
                        __gc new BinaryFormatter();
         FileStream *pStrm;
         try {
            pStrm = pFI->OpenRead();
            m_pStore = dcast<StoredData*>(
                     pFmtr->Deserialize(pStrm));

         }catch (Exception *pE) {
            MsgBox::Error(String::Concat(
                  S"Unable to load data file: ",
                  pFI->FullName, S", Error: ", 
                  pE->Message));
         }__finally{
            pStrm->Close();
         }
      }
      //if data was corrupt or missing
      if (_PattsInvalid()) { 

         m_pStore = __gc new StoredData;
         m_pStore->pColls = 
           CreateDefaultPatterns(); //load defaults
      }
   }

   void SaveToDisk() {

      String *psProfDir = 
          Environment::ExpandEnvironmentVariables(
                                   S"%USERPROFILE%");
      String *psFullPath = String::Concat(psProfDir, 
                                S"\\CompClean1.dat");

      BinaryFormatter *pFmtr = __gc new BinaryFormatter();
      FileStream *pStrm;
      try {
         pStrm = File::Open(psFullPath, FileMode::Create);
         pFmtr->Serialize(pStrm, m_pStore);
      }catch (Exception *pE) {
         MsgBox::Error(String::Concat(
                 S"Unable to save data to: \"",
                 psFullPath, S"\", Error: ", 
                 pE->Message));
      }__finally{
         pStrm->Close();
      }
   }

    // Creates default patterns 
    // for first-time users
   static ArrayList* CreateDefaultPatterns();

    // Returns new gc array of String*, 
    // from any non-typed object based on IList
   static String* CopyToGCStrArr(IList *pList) __gc[];

    // Returns deep copy of m_pStore->pColls 
    // and all nested members
   ArrayList* GetDeepCopyOfPatternsCollection();

private:
   //returns true if patterns cannot be validated
   bool _PattsInvalid();  
};//class MStorage

[Serializable] 属性是允许数据自动序列化的原因。实际上,只有一个整体可序列化对象 MStorage::StoredData *m_pStore。它可以包含任意数量的数据,但主要包含 ArrayList 类型的 PattColl 对象(其中又包含 PattGroup 对象)和搜索路径字符串。可以向 StoredData 添加更多可序列化成员以满足其他需求。

MStorage 的核心方法是 LoadFromDisk()SaveToDisk()。两者都通过调用 CLR 的 Environment::ExpandEnvironmentVariables(S"%USERPROFILE%") 来确定数据存储路径,该方法调用同名的 Win32 对应方法。BinaryFormatterFileStream 对象一起用于反序列化到和序列化 m_pStore。这再简单不过了,而且速度非常快!

其余方法只是普通的构造函数和带有常规样板代码的辅助函数。

注意事项

ListView 和事件

CLR ListView 是一个围绕原生 Windows ListView 常用控件的对象包装器。然而,始终需要将项目作为完整对象访问,这使得一些通常明显且直观的功能难以使用。变通方法需要巧妙的代码或实验(这正是 CLR 旨在防止的)。某些原生功能没有 CLR 等效项。

在原生 ListView 中选择所有或不选择任何项目都很容易(勾选和取消勾选同样容易)。CLR ListView 没有这样的功能。相反,必须遍历整个项目集合。这也意味着应该调用 ListViewBeginUpdate()EndUpdate() 方法,以避免逐个重绘项目缓慢得令人痛苦。

原生 ListView 还会生成 LVN_ITEMCHANGINGLVN_ITEMCHANGED 事件。CLR 包装器有一些“更改中”事件,但没有对应的“已更改”事件,而其他控件有“已更改”事件,但没有对应的“更改中”事件。例如,启用/禁用“清理已勾选”按钮的代码依赖于 ListViewItemCheck 事件(“更改中”)。代码必须推测性地预测勾选计数是增加还是减少,并在任何更改实际发生之前启用按钮。预测在此简单实例中有效,但这不是一个好的编码实践。如果出现捕获的异常阻止更改发生,则推测将失效。

CLR ListBox 具有 SelectedIndexChanged 事件,但没有 SelectedIndexChange(“正在更改”)事件。这使得过滤器对话框的处理比应有的复杂。不应允许的更改必须被阻止,并且在事件处理程序中计算出的不可接受的结果需要撤消选择更改,以及由于原始选择更改而发生的其他控件和数据中的任何更改。如果有一个简单的选择“正在更改”事件,所有这些都会很简单(并且调试起来远不那么令人困惑)。

此外,当用户选择多个项目时,CLR 的 ListView 会自动检查除焦点所在选定项目之外的所有项目。这本身可能令人沮丧,但如果选择了许多项目,ListView 更新所需的时间会令人难以置信地长。

PtrToStringAuto 和相关函数

在为 Unicode 和 ANSI 构建编写代码时,人们倾向于使用带“Auto”后缀的封送方法。这并非不正确,但是如果您尝试在 Unicode 平台上调试或运行 ANSI 构建,您将获得损坏的数据。

为什么?“Auto”封送是根据正在运行的平台进行的,而不是目标平台。因此,当使用混合托管和非托管 C++ 进行编译时,您可能更喜欢调用类似这样的代码

inline String* StringFromLPCTSTR(LPCTSTR ptzStr) {
#ifdef UNICODE
   return ::System::Runtime::InteropServices::
            Marshal::PtrToStringUni((int*)ptzStr);
#else//ANSI
   return ::System::Runtime::InteropServices::
           Marshal::PtrToStringAnsi((int*)ptzStr);
#endif
}

结论

我要感谢以下贡献者提供了宝贵的反馈

  • Peter Ritchie - 指出了控件事件问题解释不足之处。
© . All rights reserved.