别再掉进DLL地狱的陷阱里(DLL Hell)~.NET解决之道

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

DLL 陷阱是一个恶梦,是一种相当奇怪的问题。

相信很多读者都有这样的经验,如果你的软体今天原本运作顺畅,当你安装某个新软体之后,突然间电脑就无法运作了。

这绝对不是你的硬体有问题,也不是应用程序的问题,而是作业系统设计上的缺失,这样的问题层出不穷,这通常是因为新的应用程序版本覆盖掉共享的程序库(DLL),而且往往修改了一些现存应用程序所必需的「bug」,这个缺失有了一个名字叫做DLL Hell (DLL地狱)。

开发人员与系统管理者(以及使用者)面临最大的挑战就是版本更新的问题,他们花很多时间在Windows 登录档(Regedit) 上试着解决其问题而吃尽苦头。

.在Microsoft .NET的世界里,软体元件再也不需要登录(Registry)了!NET Framework包含了一些功能,可以实际消除「DLL Hell」的问题,一项称之为「side-by-side」开发模式的新功能。

DLL & DLL Hell
为什么要使用DLL (Dynamic Linking Library) - 动态链接库?
微软当初为Windows设计动态链接库主要是撷取它的两项优点:一是动态连结、一是资源共享。

资源共享的例子相当显而易见,例如之前曾经提过Windows有三个核心的动态链接库:Kernel主要是负责系统和应用程序的记忆体、行程和执行绪等等的管理工作;User主要负责使用者介面和讯息的传递;GDI则负责系统的任何图形绘制、显示等工作。

而这些动态链接库所提供的任何函数都可以在必要的时候,让每一个Windows环境底下的执行档使用。

因为DLL 具备节省记忆体的特性,因此自从Windows 3.1版以来,它就逐渐成为Windows程序设计的主流
˙动态链接库可以资源共享
许多大型软体厂商的众多软体产品可能都会有许多可以共用的模组,如果每一套软体各自拥有一份这些可以共用的模组,不仅会造成磁碟空间的浪费,还会让维护这些模组的工作变的既复杂、又凌乱。

最好的方法就是仅保持一份程序码,然后透过共享的方式让其他自家的应用程序也可以存取其中共用的模组。

共用模组的方法之一就是将模组制作成动态链接库,然后透过软体的安装程序复制到电脑,那么只要安装了其中一套软体之后,其他自家的产品就可以互相共
用这一套动态链接库。

假设有一函数库X供三个应用程序A、B、C使用,如果函数库为目的码或原始程序码,则程序编译之后,函数库X将会各自成为执行档A、B、C的一部份,而将来如果应用程序A、B、C同时执行,函数库X也会各自占用一份记忆体,显然这是比较浪费记忆体的方式。

如果函数库为DLL形式,则编译之后,函数库并不会成为执行档的一部分,而将来如果应用程序A、B、C同时被执行,则系统只会载入一份函数库让程序A、程序B、程序C共用,如图。

Figure: 程序与DLL的共用架构图
˙动态链接库节省记忆体空间
动态链接库的资源共享可以节省磁碟空间,而动态载入的连结方式则可以节省记忆体空间。

动态链接库采用动态载入的连结方式,动态载入让程序档在需要相关的函数或资源的时候,才载入放置在动态链接库里面的函数或资源,这种方式将可以有效地使用记忆体。

不论是节省磁碟空间或记忆体空间,都是希望利用动态链接库所提供的共享函数与系统资源的方式,降低整个Windows环境对于硬体设备的需求。

DLL的问题- DLL Hell
˙动态链接库到底出了什么问题?
其实DLL的优点(程序码共用、节省记忆体),正是其缺点的起源。

原本是立意良好的DLL,有一天会变成DLL Hell,恐怕是当初DLL的设计者所始料未及的。

而之所以会出现DLL Hell,也是因为动态链接库可以与其他程序共用函数、共享资源所引起,可谓「成也共用、败也共用」。

此话怎讲呢?为了要让其他程序共用动态链接库所提供的函数或资源,动态链接库的设计者必须相当谨慎地、缜密地考虑到功能的一致性、回溯相容等细节问题,否则一旦程序所使用动态链接库没有提供所预期的功能,那么使用者就会为此而掉入地狱了。

但是要完全考虑到一致性或回溯相容,实在是困难重重,就算真的要做到,也会让利用动态链接库的软体厂商付出相当的成本;但,有必要付出这些成本吗?想想现今的电脑执行环境,与当初微软设计动态链接库的时候已经有相当、相当大的变化。

现在的硬体比起当初已经便宜太多、太多了,个人电脑的记忆体都是从64MB起跳,配备128MB记忆体的电脑更是比比皆是,而硬碟容量更是以GB计算。

在如此的硬体环境之下,Windows程序设计师还需要这么刻苦地考虑共用的问题吗?而且动态链接库的动态载入,其实已经替Windows系统节省了不少系统资源,因此微软也重新调整动态链接库的设计理念,而且也针对作业系统进行改善,希望不要再有任何使用者掉入因为共用动态链接库而起的地狱深渊。

˙数种DLL Hell 的状况
让我们想一想,如果某一副程序或物件类别有90%符合我们的需求,却有10%不符合,怎么办呢?对副程序来说,大概只有修改「原始程序码」一途。

假设程序A会使用物件X,在程序A安装到系统时,会同时安装物件X,假设另一个程序B也会使用到物件X,那么程序B直接复制到硬碟中即可正常运作,因为物件X已经存在于系统中,这听起来很好,因为程序A与程序B可以共用物件X。

然而对程序A来说,原本在安装后,执行得好好的,却可能在未知的一天变成无法执行,这就是所谓的DLL Hell。

以下为描述DLL Hell的两种状态。

状况 1. 动态链接库没有善尽回溯相容的责任
如果程序A使用的是1.0版的物件X,而程序B使用的是2.0 版的物件X(通常是因为程序B开发的时间较晚,使用较新的版本),结果会怎样呢?结果在程序B被安装到系统时,物件X 2.0版也必须安装到系统中,此时系统中 1.0 版的物件X将会被 2.0 版所取代。

在大部分的情况下,物件X 2.0版相容于1.0版,所以程序A依然可以正常运作,但有时候却会出现 2.0 版及 1.0 版不相容的情况,此时程序A便无法正常执行了。

此种DLL Hell 的起因则是的设计者,原因在于动态链接库没有善尽回溯相容的原则。

试着想想A.exe 需要X.dll 所提供的功能,但是在新版的X.dl l里面,功能竟然被取消了,这时候也极可能发生DLL Hell。

但是诚如之前所讨论,有时往往很难保证百分之百的回溯相容,而且目前个人电脑的硬体配备已经不再像以前简陋,因此微软也提出了所谓Side-By-Side的动态链接库,让程序都能拥有自己专属的动态链接库,进而减少共用动态链接库以避免这种DLL Hell的发生。

状况 2. 动态链接库善尽回溯相容的责任, 但动态链接库本身出现bug
另一种情况,物件X的提供者确实考虑到版本相容的问题,而根据物件的规格来看,新旧版也的确相容,但程序A使用新版的物件X就是有问题,毕竟程序A并没有与新版的物件X一起运作过,谁知道会发生什么情况?
Client
Server DLL
(X.DLL)
Server Codes
程序 A 用X 1.0(X 1.0) (原始版
本)
Public SetValue(ByVal Value As Integer)
If Value < 0 Then
Value = 0
End If
m_Value = Value End Property
程序 B 用X 2.0
(X 2.0) (更新
版本)
Public SetValue(ByVal Value As Integer)
'Fix the bug
If Value < 0 Then
Err.Raise Number:=APP_ERROR, Description:="Negative Value "
End If
m_Value = Value
End Property
如上表所发生的不幸的事, 纵使X 2.0 的开发人员小心翼翼透过VB6.0 的二进位相容模式控制DLL版本,且所有的内部GUID值与方法和参数都完全相同,由于X 1. 0 之中有一个名为Value 的属性名称, 当此一属性设为负数时, 该属性就会变成零, 但却不会出现错误讯息。

这个做法是错误的, X 2.0版将此臭从解决了- 若将Value属性设为负数, 则会抛出错误。

当程序B 以X 2.0 散布时, 这支程序B 当然也可以正常运作。

不过, 如果将X 2.0 安装在系统之中, 程序A 会当掉。

之前程序将Value 属性设为-1 不会有问题, 但现在会出现执行时期的错误。

同时程序A的开发人员并未在这设计错误检视的机制。

在许多真实的案例中, 要善尽二进位相容模式控制DLL版是非常困难的, 亘何况即使是二进位相容模式控制DLL版本还是有可能造成DLL Hell。

.NET 如何解决DLL Hell 的问题
自我描述的Assembly(Self-describing Assembly)
Assembly(组合)是简化部署与版本管理的关键。

Assembly是部署与版本管理的基本单元,其中包含一群资源(Resource)与型别(Type),以及它们内含的Metadata,同时也包括此组合在建造时所依赖的其他组合的版本资讯。

有了独立完整的组态资讯,同一组合的不同版本也可安装在同一部机器上,搭配共通语言执行环境具备根据各组合的组态资讯,载入正确版本的依赖组合(Dependent Assemblies)的能力,安装与解除安装的过程,就如同复制档案与删除档案一样单纯,以往因先后安装彼此覆盖而产生的所谓“DLL Hell”的版本失控的现象不复存在。

Figure . Self-Describing Assembly 可包括多个档案
Metadata(定义)是Assembly能自我描述的关键。

Metadata是编辑器在产生执行码时,伴随产生的定义性资讯,举凡元件所使用的型别、属性、方法、事件甚至辅助与备注等资讯都可包含在内,而且保证与执行码的一致性,完全取代并且超越了传统分离式的IDL(Interface Definition Language)档案与型别库(Type Library)所扮演的功能,同时元件服务要求与执行所需的资讯皆动态来自Metadata 。

.asseembly directive 作为辨识assembly 本身, .assembly extern directives 定义此assembly 所依赖的其他Assemblies。

Figure . 可使用IL Disassembler (Ildasm)呈现DLL 的metadata
简化的部署与版本管理
˙Application-Private Assemblies (Isolated Assembly)
Application-Private Assemblies (or 被隔离的assembly) 只能被一个应用程序所使用- 它不能被其他的应用程序所共用。

隔离assembly 让程序开发者有着对应用程序绝对的控制权,这也是.NET 应用程序的预设方式。

开发好的Application-Private Assemblies 要在另一个.NET 环境进行安装时,手续只有一个,就是Copy And Paste。

只要把编译好的程序,无论是EXE执行档、DLL元件、 的.aspx网页或Web Service的.asmx档,全部都是以复制/贴上的方式部署在和应用程序相同的目录,这些档案复制完成后,不需额外注册或设定。

.NET程序执行时,如果需要额外的元件,首先会自本身执行档下的同一目录开始寻找,因此,每套应用程序预设都是使用本身同一目录下的元件,不同应用程序间不会相互干扰,也消除了DLL Hell 的困扰。

(注: The CLR finds these assemblies through a process called probing. Probing is simply a mapping of the assembly name to the name of the file that contains the manifest.)
˙Shared Assemblies - Side by side execution (并排执行)
然而,应用程序共享assembly 还是有其必要性, 因为让每个应用程序都有自己一份copy (如System.Windowns.Forms, System.Web or a common Web Forms control )是件很奇怪的事。

为了解决DLL Hell的问题,.NET增加了一种新的技术,称为Side by side execution,意思是应用程序可以拥有各自版本的DLL,例如程序A使用版本1.0的物件、而程序B使用版本2.0的物件,1.0版与2.0版的物件可以同时在系统执行。

透过Side by side execution的技术,应用程序只要安装成功之后,就不用担心DLL更新版本,或规格的改变,因为就算DLL改朝了,应该程序也不用换代。

以下简述Side by side
execution的过程图及应用步骤:
Figure . Process for implementing strong names
step 1. create a key pair using the Strong Name tool (Sn.exe) .
Sn –k MyKey.snk
严格名称(Strong names)是一种在.NET 架构下可减少.NET DLL 陷阱的功能
(注: The author of an assembly generates a key pair, signs the file containing the manifest with the private key, and makes the public key available to callers by the Strong Name tool. The key pair is passed to the compiler using the custom Assembly attribute )
Step 2. sign an assembly with a strong name and version number:
In Assemblyinfo.vb,
<assembly:AssemblyKeyFileAttribute("MyKey.snk")>
<assembly:AssemblyVersion("1.0.0.1")
Step 3. Install Assembly to Global assembly cache (GAC)
gacutil /i:myassembly.dll
(注: See the .NET Framework SDK documentation for a full description of the options supported by gacutil.)
Figure : Global Assembly Cache
Step 4. 应用程序与Assembly 的版本系结(Version Policy)
.NET 提供有组态设定机制可以控制Assembly 的系结,故在程序中可以载入相关Assembly的升级版本。

组态设定机制是由XML设定档来负责,透过这种安全机制可以控制程序的安全、版本以及远端的功能。

每一个应用程序可以有XML程序设定档来指定应用程序所要系结Assembly 的不同版本
程序设定档的档案名称为程序名称加上.config 副档名, 如Myapp.exe.config。

<bindingRedirect> 的标记用来指是.NET 载入更新版的Assembly 。

例如以下范例设定档会载入MarineCtrl 版本 5.0.0.1 而非版本 5.0.0.0
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly
<assemblyIdentity name="MarineCtrl" publicKeyToken="9335a2124541cfb9" />
<bindingRedirect oldVersion="5.0.0.0" newVersion="5.0.0.1" />
</dependentAssembly>
</assemblyBinding>
Summary
The .NET Framework NET Assembly 自我描述与版本管理功能让zero-impact 的部署安装成为可能,同时也终结了DLL Hell 。

Application-Private Assemblies (or 被隔离的assembly) 只能被一个应用程序所使用- 它不会被其他的应用程序所影响。

隔离的assembly 让程序开发者对应用程序有着绝对的控制权,开发好的Application-Private Assemblies只要部署在和应用程序同一目录即可。

透过Side by side execution的技术,应用程序只要安装成功之后,就不用担心DLL更新版本,或规格的改变,它允许一个assembly 的多个版本在一个机器上同时被安装并执行,而且每一个应用程序都可以要求和不同的Assembly 版本系结。

The .NET Framework 纪录应用程序版本资讯,并在执行应用程序时使用此资讯载入应用程序所需依赖的正确版本的Assemblies。

相关文档
最新文档