DLL的delay load,即延迟加载,是一项提高程序启动速度,减少进程地址空间的重要技术,用通俗一点的话来讲,就是对模块“呼之即来,挥之即去”的能力。很多程序缺乏这种灵活性,从而导致软件在第一次启动时需要加载所有可能需要的模块,严重影响了启动速度。狭义的delay load,是微软提供的一项技术,让模块在其symbol第一次被引用到时才被加载。但在广义上,我们其实有不少其他技术来实现这种“需要时才加载”的灵活性。
一、插件机制
软件定义好一些预设的接口,插件按要求在自己的模块中实现这些接口,然后通过一些配置文件,或者注册表信息,软件可以动态的加载该插件,并调用相关初始化代码实现“对接”。
相信熟悉编写VS插件的同学对此不会陌生,VS预定义了IDTExtensibility2,IDTCommandTarget供插件实现,插件的一些信息则存在一个.addin文件中,你需要在该配置文件中说明“该插件的实现模块”,“是否在VS启动时加载还是按需加载”等信息,然后当VS启动或者你激活了该插件中的某个命令时,VS的插件机制代码会加载该插件,实例化实现了接口的AddIn类,并调用其初始化代码 --- 插入完成。目前,很多主流的软件都支持这种插件机制。
另外一个比较简单但是明了的例子,在这两篇文章中有不错的讲解:C语言的插件机制(上),C语言的插件机制(下)。
二、COM组件
感觉蛮多人对微软的COM技术颇有微词的。但是不论从COM的成绩,还是COM的思想上来看,其都是相当优秀的。
- COM是微软操作系统的基石,而且我们使用的很多组件,都是COM形式提供的(XML, DirectX...),各种大型桌面软件,都是以COM的形式暴露其编程接口便于二次开发(Office, VS, AutoCAD, Inventor...)
- COM规范对接口与对象的强调与支持,在“面向对象编程范式”的道路上相比于C++更进了一步。孟岩在这片文章的谈到了这一点,感觉挺认同的。
说了不少题外话来帮COM正名:), 其实这里要提到的是,COM机制对模块的自动加载与卸载的能力, 又是其一大优点。当我们创建一个COM对象时,该对象所在的DLL(甚至exe)会被自动加载,全部使用完了,又可以帮你自动卸载,而你所要做的就是操作interface完成你要完成的事,完全不用操心加载卸载 --- 要多贴心就有多贴心。
也就是说,使用COM,你就使用了delay load!!!
三、LoadLibrary & GetProcAddress
当你决定使用这种方法时,你要意识到:
- 你必须自己LoadLibrary和FreeLibrary来管理模块,而且他们调用的次数必须相等
- 这种方法只能调用导出的函数 ,类不行,数据也不行
- 为了调用某个函数,你需要:LoadLibrary - 定义函数原型 - GetProcAddress得到函数指针 - 调用, 如果说函数很多,你会很累
- 如果我修改了被调模块中某个函数的原型,你只能在运行时发现这个错误,所以维护起来不太容易
- 需要调用的函数非常少,而且调完就“跑”,这样可以用一个函数封装一下,用RAII来加载与卸载DLL
- Hack,你没有源代码 ,通过查看该DLL的符号导出表,你强制调用其内部函数以达到你“不可见人”的目的:),产品代码中不建议使用。
四、Delay Load机制
由于上面这种方式的种种不便,MS提供了一种相当聪明的机制,来帮我做掉那一系列的“维护”工作,而对你来讲,就只是简单的链接该DLL,并声明其为delay load就ok了。以下两个文件是其实现:
..\Microsoft Visual Studio 9.0\VC\include\delayhlp.cpp
..\Microsoft Visual Studio 9.0\VC\include\ delayimp.h
对于其工作原理,这篇文章有不错的解释。
可以看到,在我们第一次调用到该DLL中的代码时,该机制会帮助我们调用LoadLibrary完成加载。同时,你也可以unload一个被delay load的DLL,但需要使用/delay:unload设置和__FUnloadDelayLoadedDLL2函数,这里有详细介绍。
遗憾的是,用delay load机制调用起来的DLL,在主调模块退出时,其并不会被自动释放 - 不清楚MS没有做到这一步的原因,但做不到这一步始终是个问题,尤其当主调模块是一个COM组件时。结合上面提到的unload的方法和delayload机制提供的事件,我们可以自己实现一套方法来完成:在每个主调模块中声明一个静态对象,该对象为维护一个HMODULE链表,每load一个delay load的DLL就加入该链表,该对象析构时,unload链表中的所有模块。
以上几种delay load的机制,说到底都是对如何load以及如何获得类型信息以调用的权衡:
插件机制通过配置文件指定DLL,由插件框架读取并加载,调用是通过一系列预先设定的接口进行的。
COM机制通过注册表或配置文件指定DLL,由COM runtime加载,调用则是通过类型库tlb提供接口信息。
LoadLibrary和GetProcAddress显示指定加载DLL, 调用则是查看目标DLL的导出函数,定义相应函数指针类型
Delay load机制加载由dependency关系得到该DLL位置,由delay load机制实现加载,调用则是通过传统的链接关系,包含头文件实现。