ArcObjects 开发人员的 COM 简介
一、概述
ArcObjects基于Microsoft的组件对象模型(COM)。ArcGIS应用程序的终端用户不必了解COM,但如果您是一名开发人员,希望开发基于ArcObjects的应用程序,或使用ArcObjects扩展现有的ArcGIS应用程序,即使您计划使用.NET或Java API,而不是专门使用COM,也需要了解COM。所需的知识水平取决于您想要进行的定制或开发的深度。尽管本主题不涵盖整个COM环境,但它为开发人员提供了足够的知识,使他们能够有效地使用ArcObjects。有许多编码技巧和指导原则可以让您更有效地使用ArcObjects。
二、Microsoft 组件对象模型简介
在具体讨论 COM 之前,值得讨论一下一般性软件组件的使用。推动软件组件发展,背后因素有很多,但最主要的因素是软件开发是一项昂贵且耗时的冒险。
在理想的世界中,即使在原始开发人员没有预见到的情况下,也可以编写一次代码并使用各种开发工具重复使用它。理想情况下,可以部署原始开发人员对代码功能所做的更改,而无需现有用户更改或重新编译他们的代码。 生成可重用代码块的早期尝试围绕类库的创建展开,通常用 C++ 开发。这些早期的尝试受到了一些限制,特别是系统部分共享困难(很难共享二进制 C++ 组件,大多数尝试只共享源代码),持久化和更新 C++ 组件而无需重新编译的问题,缺乏良好的建模语言和工具,以及专有接口和定制工具。
为了解决这些和其他问题,许多软件工程师采用基于组件的方法进行系统开发。软件组件是可重用代码的二进制单元。
出现了几种不同但重叠的标准来开发和共享组件。 对于构建交互式桌面应用程序,Microsoft 的 COM 是事实上的标准。 在 Internet 上,JavaBeans 是一种可行的技术。 在适用于应用程序级互操作性的粗粒度上,对象管理组 (OMG) 指定了公共对象请求代理体系结构 (CORBA)。
//ESRI 选择COM 作为ArcGIS 的组件技术,因为它是一种成熟的技术,提供良好的性能,当今许多开发工具都支持它,并且有大量的第三方组件可用于扩展ArcObjects 的功能。
要理解 COM(以及所有基于 COM 的技术),重要的是要认识到它不是面向对象的语言,而是一种协议或标准。 COM 不仅仅是一种技术。它是一种软件开发方法论。 COM 定义了一种将一个软件组件或模块与另一个连接的协议。通过使用该协议,可以构建可在分布式系统中动态互换的可重用软件组件。
COM 还定义了一种称为基于接口的编程的编程模型。对象封装了操作方法和数据,这些数据表征了定义良好的接口背后的每个实例化对象。这促进了结构化和安全的系统开发,因为对象的客户端不受保护,不知道如何实现特定方法的任何细节。 COM 没有指定应用程序的结构。作为“使用 COM 的应用程序的”程序员,语言、结构和实现细节由您决定。
//组件成功的关键在于它们以实用的方式实现了许多现在软件工程中普遍接受的面向对象原则。 组件有助于软件重用,因为它们是自包含的构建块,可以轻松组装到更大的系统中。
COM 确实指定了“使 COM 对象能够与其他 COM 对象交互的”对象模型和编程要求。 这些对象可以在单个进程内、其他进程中,甚至在远程机器上。 它们可以用其他语言编写,并且可能以不同的方式开发。 这就是 COM 被称为二进制规范或标准的原因——它是在程序被转换为二进制机器代码后应用的标准。
COM 允许在二进制级别重用这些对象,这意味着第三方开发人员不需要访问源代码、头文件或对象库来扩展系统,即使在最低级别也是如此。
三、组件、对象、客户端和服务器
不同的文本文档,使用术语组件、对象、客户端和服务器来表示不同的事物。 (为了增加混淆,又有各种文本文档使用所有这些术语来指代同一事物)因此,对术语进行定义是值得的。
COM 是一种客户端/服务器架构。 服务器(或对象)提供一些功能,客户端使用该功能。 COM 促进了客户端和对象之间的通信。 一个对象可以同时是“客户端的服务器”和“其他对象服务的客户端”。 请参阅下图:
//对象是使服务可供客户端使用的 COM 类的实例。 因此,用“客户端和对象”,而不是用“客户端和服务器”,是正常的。 这些对象通常称为 COM 对象和组件对象。 本主题将它们简称为对象。
客户端和它的服务器可以存在于同一个进程中,也可以存在于不同的进程空间中。 在同一进程内,服务器以动态链接库 (DLL) 的形式打包,当客户端第一次访问服务器时,这些 DLL 被加载到客户端的地址空间中。 在进程外,服务器打包在可执行文件 (EXE) 中并在其地址空间中运行。 COM 使 这些差异【是否存在于同一进程的差异】 对客户端透明。 请参阅下图:
创建 COM 对象时,开发人员必须了解对象所在的服务器类型,但如果对象的创建者已正确实现它们,则打包不会影响客户端对对象的使用。
每种打包的方法各有利弊:(1)DLL 加载到内存的速度更快,调用 DLL 函数的速度也更快;(2)EXE 提供了更强大的解决方案(如果服务器出现故障,客户端不会崩溃),并且由于服务器具有自己的安全上下文,因此可以更好地处理安全性。
在分布式系统中,EXE 更灵活,服务器的字节顺序可以与客户端的不同。大多数 ArcObjects 服务器都打包为进程内服务器 (DLL)。稍后,您将看到与进程内服务器相关的性能优势。
在 COM 系统中,“功能的提供者(对象)”与“使用功能的客户端或用户”是完全隔离的。客户只需要知道该功能是否可用——若知道可用,客户端就可以调用对象的方法,并期望对象尊重它们。通过这种方式,COM 被认为是客户端和对象之间的契约。如果对象违反了该契约,则系统的行为将是未指定的。这样,COM 开发基于“对象功能的实现者”和“用户”之间的信任。
在 ArcGIS 应用程序中,有许多对象通过它们的接口提供数以千计的属性和方法。当您使用 ESRI 对象库时,您可以假设所有这些属性和接口都已完全实现,如果它们在对象图中,它们就可以使用。
四、类工厂
在每个服务器中,都有一个称为类工厂的对象,COM 运行时与之交互以实例化特定类的对象。 对于每个对应的 COM 类,都有一个类工厂。 通常,当客户端从服务器请求对象时,相应的类工厂会创建一个对象并将该对象传递给客户端。 请参阅下图:
服务器是一个二进制文件,其中包含“一个或多个 COM 类”所需的所有代码。 这包括“使用 COM 将对象实例化到内存中的代码”,以及方法的代码【服务器所包含的对象的方法】。
尽管这是正常的实现,但它并不是唯一可能的实现,还可以实现单例模式: 类工厂在第一次被调用时创建对象的实例,然后在随后的调用中,将同一个对象传递给客户端。 这种类型的实现,创建了所谓的单例对象。
五、GUID(全球唯一标识符)
分布式系统可能有数千个接口、类和服务器。在运行时,定位和“绑定客户端和对象”时必须引用所有这些接口、类和服务器。 显然,使用人类可读的名称,会导致潜在的冲突。 因此,COM 使用全球唯一标识符 (GUID),128 位数字实际上保证它们在世界上是唯一的。
//首字母缩略词 GUID 通常发音为“gwid”。
COM API 定义了一个可用于生成 GUID 的函数。 此外,所有符合 COM 的开发工具都会在适当的时候自动分配 GUID。 GUID 与通用唯一标识符 (UUID) 相同,由 Open Group 的分布式计算环境 (DCE) 规范定义。 下面显示了注册表格式的示例 GUID 和“创建 GUID”对话框:
- {E6BDAA76-4D35-11D0-98BE-00805F7CED21}
GUIDGEN.EXE 是随 Microsoft 的 Visual Studio 一起提供的实用程序,并提供易于使用的用户界面 (UI) 来生成 GUID。 它可以在目录\Common\Tools 中找到。
六、COM 类和接口
使用 COM 开发,即 UI 开发,即 基于接口的编程模型。 对象之间的所有通信都是通过它们的接口进行的。 COM 接口是抽象的(虚拟的,abstract),这意味着没有与接口相关联的实现; 与接口关联的代码来自类的实现。
接口的实现方式因对象而异。 因此,对象是继承了接口的类型,而不是接口的实现,这称为类型继承。 功能用接口抽象建模并在类实现中实现。 类和接口通常被称为 COM 的“内容”和“方式”。 接口定义了对象可以做什么,而类定义了它是如何完成的。 请参阅下图:
这是地理数据库对象模型的简化部分,显示了抽象类、协同类和类的实例化之间的类型继承。
COM 类提供“与一个或多个接口相关联的”代码,从而将功能完全封装在类中。两个类可以具有相同的接口,但它们的实现方式可能完全不同。通过以这种方式实现这些接口,COM 显示了经典的面向对象的多态行为。 COM 不支持多重继承的概念【没有多个基类】;然而,这不是缺点,因为单个类可以实现多个接口。
以下是开发人员必须了解的 ArcObjects 中的三种类:
-
- Abstract classes(抽象类)
- CoClass (协同类)
- Class(类)
Abstract class 不能被创建。它仅仅是子类实例的规范(通过类型继承)。 ArcObjects “ Dataset 和 Geometry 类”是 Abstract class 的例子。不能创建 Geometry 类型的对象,但可以创建 Polyline 类型的对象。反过来,这个 Polyline 对象实现了“在 Geometry 基类中定义的”接口;因此,在基于对象的类中定义的任何接口都可以从 coclass 访问。
CoClass 是一个可公开创建的类。换句话说,COM 可以创建该类的实例,并将结果对象提供给客户端,以使用由该类的接口定义的服务。
Class 不能公开创建,但此类的对象可以由 ArcObjects 中的其他对象创建,并提供给客户端使用。
下图显示了实现接口时 COM 类中表现出的多态行为。请注意,Human 和 Parrot 类都实现了 Italk 接口。 ITalk 接口定义了方法和属性,例如 StartTalking、StopTalking 或 Language,但显然这两个类以不同的方式实现这些。该插图显示了如何在多个对象和动物之间共享表示为接口的常见行为以支持多态性。
七、内部接口
COM 接口是 COM 对象相互通信的方式。使用 COM 对象时,开发人员从不直接使用 COM 对象,而是通过其接口之一访问该对象。 COM 接口是一组逻辑相关的功能。虚函数由客户端调用并由服务器实现。这样,对象的接口就是客户端和对象之间的契约。对象的客户端持有指向该对象的接口指针。该接口指针被称为不透明指针,因为客户端无法获得对象内实现细节的任何知识或直接访问对象的状态数据。客户端必须通过接口的成员函数进行通信。这允许 COM 提供一个二进制标准,所有对象都可以通过该标准进行有效通信。
接口允许开发人员抽象地建模功能。 Visual C++ 开发人员将接口视为纯虚函数的集合,而 VB 开发人员将接口视为属性、函数和子例程的集合。
接口的概念是 COM 中的基本概念。 COM 规范(Microsoft,1995)在讨论 COM 接口时强调了以下四点:
-
- 接口不是类。接口不能自己实例化,因为它没有实现。
- 接口不是对象。接口是一组相关的功能,是客户端和对象进行通信的二进制标准。
- 接口是强类型的。每个接口都有自己的接口标识符,从而消除了具有相同“人类可读名称的”接口之间发生冲突的可能性。
- 接口是不可变的。接口永远不会被版本化。一旦定义和发布,接口就不能改变。
一旦发布了接口,就无法更改该接口的外部签名。可以随时更改公开接口的对象的实现细节。此更改可能是一个小错误修复或底层算法的完全重新设计。接口的客户并不关心,因为接口对他们来说是一样的。这意味着,当以新 DLL 和 EXE 的形式部署对服务器的升级时,现有客户端无需重新编译即可使用新功能。如果接口的外部签名不再足够,则会创建一个接口来公开新功能。旧的或不推荐使用的接口不会从类中删除,以确保所有现有的客户端应用程序可以继续与新升级的服务器通信。较新的客户可以选择使用旧的或新的接口。
//接口的持久性不仅限于其方法签名,还扩展到其语义行为。 //例如,一个接口定义了两个方法 A 和 B,对它们的使用没有限制。 //如果在后续版本中方法 A 要求首先执行方法 B,则它违反了 COM 约定。像这样的更改会强制客户端重新编译。
八、IUnknown 接口
所有 COM 接口都派生自 IUnknown 接口,并且所有 COM 对象都必须实现此接口。 IUnknown 接口执行以下两个任务:
-
- 控制对象生命周期
- 提供运行时类型支持
通过 IUnknown 接口,客户端在对象使用时维护对对象的引用,将实际的生命周期管理留给对象。
//IUnknown 这个名字来自于 1988 年由 Anthony Williams 撰写的 Microsoft 内部论文,论文名为:
//Object Architecture: Dealing with the Unknown -or- Type Safety in a Dynamically Extensible Class
//(“对象体系结构:处理动态可扩展类中的未知或类型安全”)。
对象生命周期由两个方法 AddRef 和 Release 以及一个内部引用计数器来控制。每个对象都必须有一个 IUnknown 的实现来控制它的生命周期。每当创建或复制接口指针时,都会调用 AddRef 方法,当客户端不再需要此指针时,会调用相应的 Release 方法。当引用计数达到零时,对象会自行销毁。
客户端还使用 IUnknown 来获取对象上的其他接口。 QueryInterface 是客户端在需要对象上的另一个接口时调用的方法。当客户端调用 QueryInterface 时,该对象提供一个接口并调用 AddRef。事实上,任何返回接口的 COM 方法都有责任代表调用者增加对象的引用计数。当不再需要该接口时,客户端必须调用 Release 方法。客户端仅在接口重复时显式调用 AddRef。
//方法 QueryInterface 通常用首字母缩略词 QI 来指代。
在开发 COM 对象时,开发者必须遵守 QueryInterface 的规则。这些规则规定对象的接口是对称的(symmetric)、可传递的(transitive)和自反的(reflexive),并且在对象的生命周期内始终可用。对于客户端,这意味着给定一个对象的有效接口,通过调用 QueryInterface 向该对象询问该对象上的任何其他接口(包括其自身)总是有效的。不可能支持一个接口然后拒绝访问该接口——如果被拒绝,这可能是因为时间或安全限制。
必须使用其他机制来提供此级别的功能。一些类支持可选接口。根据 coclass,他们可以选择实现一个接口。这不会违反此规则,因为接口在类上始终可用或始终不可用。请参阅下图:
QueryInterface 的规则规定对象的接口是自反的、对称的和可传递的。始终有可能在对象上持有有效的接口指针,以获取该对象上的任何其他接口。
当请求特定接口时,QueryInterface 方法可以为“这个被请求的接口”返回一块已经分配的内存,或者它可以分配一块新内存并返回接口。返回同一块内存的唯一情况是请求 IUnknown 接口时。通过比较两个接口指针以查看它们是否指向同一个对象,不要进行简单的比较,正确做法是,查询它们的 IUnknown 接口,然后对 IUnknown 指针进行比较。 IUnknown 接口是 COM 对象的标识。
//由于 IUnknown 是所有 COM 对象的基础,因此通常在任何 ArcObjects 文档和类图中都没有对 IUnknown 的引用。
在 VB 中通过分配一个等于 Nothing 的接口来显式调用 Release 来释放它持有的任何资源是一种很好的做法。即使您不调用Release,当您不再需要该对象时,即当它超出范围时,VB 也会自动调用它。对于全局变量,您必须显式调用 Release。在 VB 中,系统会为您执行所有这些引用计数操作,从而使 COM 对象的使用相对简单。
但是,在 C++ 中,您必须递增和递减引用计数以允许对象正确控制其生命周期。同样,在请求另一个接口时必须调用 QueryInterface 方法。在 C++ 中,智能指针的使用大大简化了这一点。这些智能指针是基于类的,因此具有适当的构造函数、析构函数和重载运算符来自动化大部分引用计数和 QI 操作。
//智能指针是一种基于类的智能类型。
九、接口定义语言(IDL,Interface definition language)
Microsoft 通过 接口定义语言 (MIDL) 来描述 COM 对象,包括描述COM 对象的接口。 MIDL 是由“ DCE 定义的 IDL ”扩展而来——DCE 定义的 IDL 主要用于定义客户端和服务器之间的远程过程调用(RPC)。 MIDL 包含了大多数 ODL(对象定义语言 )的 语句和属性—— ODL 主要用于“早期的
OLE(对象链接和嵌入)自动创建类型库”。
//MIDL 通常被称为 IDL。 IDL 定义了开发人员在使用 ArcObjects 时使用的公共接口。 编译时,IDL 会创建一个类型库。
十、类型库(TLB,Type library)
最好将类型库视为 IDL 文件的二进制版本。 它包含所有coclass、接口、方法和types的二进制描述。 Microsoft 提供了几个与类型库一起使用的 COM 接口。 其中两个接口是 ITypeInfo 和 ITypeLib。 通过利用这些标准 COM 接口,各种开发工具和编译器可以获得“被特定库支持的 coclass 和接口的”信息。
为了支持独立于语言的开发组件集的概念,所有与 ArcObjects libraries 相关的信息都在类型库中提供。 没有头文件、源文件或目标文件是外部开发人员必需的,也未为外部开发人员提供这些。
十一、入站接口和出站接口
接口可以是入站的或出站的。入站接口是最常见的类型(客户端调用“对象的接口的函数”)。出站接口是对象调用客户端的接口(一种类似于传统回调机制的技术)。请参阅下图:
在本主题的插图和 ArcObjects 对象模型图 (OMD) 中,出站接口用实心圆表示。
入站接口和出站接口的实现方式存在差异。入站接口的实现者必须实现接口的所有功能。如果不这样做,将违反 COM 的协议。对于出站接口也是如此。如果您使用 VB,则不必实现接口上存在的所有功能,因为它为您未实现的方法提供了存根方法(stub methods)。另一方面,如果使用 C++,则必须实现所有纯虚函数来编译类。
连接点(Connection points)是一种用于处理出站 COM 接口的特定方法。连接点架构定义了如何建立和取消对象之间的通信。连接点不是初始化双向对象通信的最有效方式,但它们被普遍使用,因为许多开发工具和环境都支持它们。
十二、调度事件接口
ArcObjects 中有一些对象支持两个看起来很相似出站事件接口——它们支持的方法看起来很相似。 两个此类接口的示例是 IDocumentEvents 和 IDocumentEventsDisp。 Disp 后缀表示纯调度接口。 Visual Basic for Applications (VBA) 在处理某些应用程序事件(例如加载文档)时使用这些纯调度接口。 VBA 程序员使用纯调度接口,而使用另一种开发语言的开发人员使用非纯调度接口。
十三、 默认接口
每个 COM 对象都有一个默认接口,如果没有指定其他接口,则在创建对象时返回该默认接口。 ESRI 对象库中的所有对象都将 IUnknown 作为其默认接口,但有一些例外。
//将 IUnknown 设为默认界面的原因是因为 VB 对象浏览器隐藏了默认界面的信息。 它隐藏 IUnknown 的事实对于 VB 开发人员来说并不重要。
ArcCatalog 和 ArcMap 的 Application 对象的默认接口是 IApplication 接口。 这些非 IUnknown 默认接口的使用是 VBA 的要求,可在 ArcMap 和 ArcCatalog 应用程序级对象中找到。 这意味着必须以某种方式声明保存接口指针的变量。 创建 COM 对象时,可以在创建时请求任何受支持的接口。
十四、IDispatch 接口
“绑定”是一个术语,用于描述一个“匹配函数位置”过程(给定“指向对象的指针”)。
COM 支持以下三种类型的绑定:
-
- Late - 直到运行时,才知道对象的类型。客户端进行方法调用,但对象并未实现方法,则在执行时会失败。
- ID - 在编译时存储“方法 ID” ,但 方法 仍由 “更高级别的函数(Invoke)” 来执行。
- Custom vTable (early) - 在编译时执行绑定,客户端可以直接对对象进行方法调用。
IDispatch 接口用于支持 Late 和 ID 绑定语言。 IDispatch 接口具有“允许客户端询问对象支持哪些方法”的方法。假设对象支持所需的方法,客户端通过调用 IDispatch.Invoke 方法 来调用 该所需的方法。Invoke 方法 调用所需的方法,并在所需的方法调用完成后,将状态和任何参数返回给客户端。显然,这不是调用 COM 对象的最有效方法。
Late 绑定需要客户端先调用对象,以检索对象支持的方法的 ID 列表;然后,客户端再构造对 Invoke 方法的调用;最后,Invoke 方法 又必须解压缩方法参数并调用 所需的函数。所有这些步骤都会显着增加执行方法所需的时间。此外,每个对象都必须有 IDispatch 的实现,这会使所有对象变大并增加其开发时间。
ID 绑定比 Late 绑定略有改进,因为方法 ID 在编译时缓存,这意味着不需要初始调用来检索 ID。但是,仍然存在大量调用开销,因为仍会调用 IDispatch.Invoke 方法来执行对象所需的方法。
early 绑定,通常称为 Custom vTable 绑定,不使用 IDispatch 接口。相反,类型库在编译时提供所需的信息,以允许客户端了解服务器对象的布局。在运行时,客户端直接对对象进行方法调用。这是调用对象方法的最快方法,并且还具有编译时类型检查的好处。
下表显示了在典型的 Pentium III 机器上每秒可以进行的函数调用的数量:
Binding type | In process DLL | Out of process DLL |
Late binding | 22,250 | 5,000 |
Custom vTable binding | 825,000 | 20,000 |
支持 IDispatch 和 Custom vTable 的对象称为双接口对象。 ESRI 对象库中的对象类不实现 IDispatch 接口。 这意味着这些对象库不能与 Late 绑定脚本语言(如 JavaScript 或 VBScript)一起使用,因为这些语言要求访问的所有 COM 服务器都支持 IDispatch 接口。
下图总结了 ArcObjects 中两个类的 Custom 接口和 IDispatch 接口。 vTable 的布局显示了差异。 下图还说明了实施所有方法的重要性。 如果缺少一种方法,vTable 将出现错误的布局,从而将错误的函数指针返回给客户端,从而导致系统崩溃。
仔细检查 ArcGIS 类图,会发现“应用程序对象”支持 IDispatch,因为 VBA 中需要 IDispatch 接口。
所有 ActiveX 控件都支持 IDispatch。 这意味着可以使用 ArcObjects 附带的各种 ActiveX 控件,来访问脚本环境中的功能。
十五、接口继承
接口由一组方法和属性组成。如果一个接口从另一个接口继承,则父对象中的所有方法和属性都可以直接在继承对象中使用。
//直接从 IUnknown 以外的接口继承的接口,不能在 VB 中实现。
这里的基本原理是接口继承,而不是您可能在语言(如 SmallTalk 和 C++)中看到的实现继承。在实现继承中,对象从其父对象继承实际代码。在接口继承中,传递的是对象方法的定义。实现接口的 coclass 必须为所有继承的接口提供实现。
由于需要访问源文件和头文件,因此在异构开发环境中不支持实现继承。为了重用代码,COM 使用聚合和包含的原则。这两种都是二进制重用技术。
十六、聚合和约束
第三方开发人员用包含或聚合方式使用现有对象,唯一的要求是,在开发人员和目标发布机器上,安装【“容纳了 被包含或被聚合对象的”服务器】。并非所有开发语言都支持聚合。
二进制重用的最简单形式是包含。包含允许修改原始对象的方法行为,但不允许修改方法的签名。对于包含,被包含的对象(内部)不知道它包含在另一个对象(外部)中。外部对象必须实现内部对象支持的所有接口。当请求这些接口时,外部对象将它们委托给内部对象。为了支持新功能,外部对象可以在不传递调用的情况下实现其中一个接口,或者实现一个全新的接口。请参阅下图:
COM 聚合涉及一个外部对象,该对象控制它选择从内部对象公开的接口。 聚合不允许修改原始对象的方法行为。 内部对象知道它正在被聚合到另一个对象中,并将任何 QueryInterface 调用转发到外部(控制)对象,因此对象作为一个整体遵守 COM 法则。
对于“使用聚合的对象“的客户端,无法区分外部对象实现哪些接口和内部对象实现哪些接口。 请参阅下图:
自定义功能使用包含和聚合。 开发人员聚合不需要定制的接口,并包含要定制的接口。 然后可以在自定义类中实现包含接口上的各个方法,从而提供自定义功能,或者可以将方法调用传递给包含接口上的适当方法。 请参阅下图:
在这种情况下,聚合很重要,因为在无法包含的功能上定义了一些隐藏的接口。
VB6 不支持聚合;因此,它不能用于创建自定义功能。
十七、线程、单元和编组
线程是"通过应用程序[流经应用程序]"的进程流。 Windows 应用程序中可能有许多线程。单元(apartment)是一组与进程内的上下文一起工作的线程。使用 COM+,一个上下文属于一个单元(apartment)。可能存在多种类型的上下文(安全性是一种上下文类型的示例)。在成功相互通信之前,对象必须具有兼容的上下文。
//虽然理解单元(apartment)和线程在 ArcObjects 的使用中“不是”必不可少的,但基本知识可以帮助您理解某些开发环境的一些含义。
COM 支持以下两种类型的单元(apartment):
-
- 单线程单元(STA)
- 多线程单元 (MTA)
COM+ 支持附加的线程中立单元 (TNA)。
一个进程可以有任意数量的 STA——每个进程创建一个称为主单元的 STA。“作为单元(as apartments)”而创建的线程放置在 STA 中。所有 UI 代码都放在一个 STA 中以防止死锁情况。
一个进程只能有一个 MTA。作为多线程启动的线程放置在 MTA 中。
TNA 没有与其永久关联的线程。相反,线程在适当的时候进入和离开TNA。
进程内对象在注册表中有一个条目,即 ThreadingModel,它通知 COM 服务控制管理器 (SCM) 将对象放置到哪个单元。如果对象请求的单元与创建者的单元兼容,则将对象放置在该单元中(创建者的单元);否则,SCM 会找到或创建合适的单元(apartment)。如果没有定义线程模型,对象将被放置在进程的主单元中。
ThreadingModel 注册表项可以具有以下值:
-
- Apartment - 对象必须在 STA 内执行。通常由 UI 对象使用。
- Free - 对象必须在 MTA 内执行。创建线程的对象通常放置在 MTA 中。
- Both - 对象与所有单元类型兼容。该对象将在与创建者相同的单元中创建。
- Neutral(中立) - 对象必须在 TNA 中执行。由对象使用以确保从其他单元调用时没有线程切换。这仅在 COM+ 下可用。
请参阅下图:
将 SCM(读作 scum)视为 COM 运行时环境。 SCM 与对象、服务器和操作系统交互,并提供客户端和它们工作的对象之间的透明性。
编组(Marshalling)使客户端能够透明地对其他套间中的对象进行接口函数调用。 编组可以发生在不同机器上的 COM 单元之间、不同进程空间中的 COM 单元之间以及同一进程空间中的 COM 单元之间(例如,STA 到 MTA)。
COM 提供了一个标准的编组器来处理使用符合自动化的数据类型的函数调用(见下表)。 只要生成代理存根代码(stub code),标准编组器就可以处理非自动化数据类型; 否则,需要自定义编组代码。
类型 | 描述 |
Boolean | 可以具有值的数据项,真或假。 |
unsigned char | 8 位无符号数据项。 |
double | 64 位 IEEE 浮点数。 |
float | 32 位 IEEE 浮点数。 |
int | 有符号整数,其大小取决于系统。 |
long | 32 位有符号整数。 |
short | 16 位有符号整数。 |
BSTR | 长度前缀字符串。 |
CURRENCY | 8 字节,定点数。 |
DATE | 自 1899 年 12 月 30 日以来的 64 位浮点小数天数。 |
SCODE | 对于 16 位系统,对应于 VT_ERROR 的内置错误。 |
Typeof enum myenum | 有符号整数,其大小取决于系统。 |
Interface IDispatch * | 指向 IDispatch 接口的指针。 |
Interface IUnknown * | 指向不是从 IDispatch 派生的接口的指针。 |
dispinterface Typename * | 指向从 IDispatch 派生的接口的指针。 |
Coclass Typename * | 指向 coclass 名称 (VT_UNKNOWN) 的指针。 |
[oleautomation] interface Typename * | 指向派生自 IDispatch 的接口的指针。 |
SAFEARRAY(TypeName) | TypeName 是上述任何类型。这些类型的数组。 |
TypeName* | TypeName 是上述任何类型。指向类型的指针。 |
TypeName* | 96 位无符号二进制整数,按 10 的可变幂进行缩放。提供数字大小和比例的十进制数据类型(如坐标)。 |
十八、组件类别
客户端应用程序使用“组件类别”来有效地查找系统上安装的特定类型的所有 COM 类。例如,客户端应用程序支持数据导出功能,您需要在其中指定输出格式。组件类别可用于查找各种格式的所有“支持数据导出的类”。
如果不使用组件类别,应用程序必须实例化每个对象并询问它是否支持所需的功能,这不是一种实用的方法。组件类别通过允许客户端应用程序的开发人员创建和使用属于特定类别的类来支持 COM 的可扩展性。如果以后将新类添加到类别中,则无需更改客户端应用程序即可利用新类。下次读取类别时,它将自动选取新类别。
十九、COM 和注册表
COM 使用 Windows 系统注册表,来存储有关组成 COM 系统的各个部分的信息。类、接口、DLL、EXE、类型库等都被分配了唯一标识符 (GUID),SCM 在引用这些组件时使用这些标识符 (GUID)。例如,运行 regedit,然后打开 HKEY_CLASSES_ROOT。这将打开系统上注册的所有类的列表。请参阅以下屏幕截图,其中显示了注册表编辑器中的 ESRI 项:
COM 将注册表用于许多“COM内务管理”的任务,但最重要和最容易理解的是在将 COM 对象实例化到内存中时使用注册表。一个进程内服务器的最简单情况:
- 客户端请求 COM 对象的服务。
- SCM 搜索类 ID(GUID),查找”请求的对象“的注册表项。
- DLL 被定位并加载到内存中。 SCM 调用 DLL 中名为 DllGetClassObject 的函数,将所需要的类作为第一个参数传递。
//函数 DllGetClassObject 是使 DLL 成为 COM DLL 的函数。 //其他函数,如 DllRegisterServer 和 DllUnregisterServer等,虽然很好,但对于 DLL 用作 COM DLL 来说,却不是必需的。
- 类对象通常实现接口 IClassFactory。 SCM 调用此接口上的 CreateInstance 方法将相应的对象实例化到内存中。
- 最后,SCM 向“新创建的对象”请求“客户端请求的”接口,并将该接口传递回客户端。在这个阶段,SCM 退出等式,之后,客户端和对象直接通信。
从前面的步骤序列,很容易想象对象包装(DLL 与 EXE)的变化对对象的客户端几乎没有影响。 COM 处理【(DLL 与 EXE之间的)这些差异】。
二十、自动化
自动化是"单个对象或整个应用程序"用于“通过后期绑定语言访问他们自己封装的功能的”技术。通常,自动化被认为是编写宏,这些宏可以访问许多应用程序来完成任务。如前所述,ArcObjects 不支持 IDispatch 接口。因此,它不能由自动化控制器单独使用。
不是必需的