dll 的主要作用有 2 个,一个是代码复用,另外一个是节省内存。
第一个作用很容易理解,把相互关联的代码封装到一个 dll 里,就如同在源码层面封装到一个 cpp,或者其他语言的单元。需要使用的时候,import 后,你就可以直接调用里面的函数了。而 dll 就是这层逻辑的编译结果的封装。假设你需要开发一个功能,但不希望把源代码给下游调用者,你可以把 cpp 做成 dll,把函数导出成 lib,把声明写入 h 文件里。这样你只要分发上述 3 个文件就可以让下游开发直接使用了。另外,代码能复用意味着不需要把所有逻辑都编译到 exe 里。你想想,如果没有 windows 的 dll,你要创建一个程序,必须从窗口创建界面绘制鼠标交互等一系列代码逻辑开始写,既浪费时间,也导致编译出来的 exe 有 20g 这么大,一旦发现 bug,你要重新修改代码,重新分发 20g 的文件。而且这 20g 里用户用到的功能并不是 100%,但加载到内存中却需要 20g 物理内存。所以很不现实也没有益处。当拆分 dll 后,你的程序需要 ui 交互的只要链接相应的 dll,你就可以工作了;你要网络的,只要链接对应 dll 就可以了;以此类推。
上面已经给出一定的节约内存解释,但还需要进一步。windows 是运行在 ring0 层的系统,它可以使用调配任何无理资源,如内存硬盘等。如果每个 exe 都不拆 dll,那内存再多也会撑爆。所以 windows 对运行在 ring3 层的 exe 做了许多工作。加载 exe 后,系统会加载所有 exe import table 里需求的 dll,然后执行初始化。同一路经的 dll 如没被加载过,会被 windows 开出一块物理内存将其加载进去,同时在 exe 的线性地址里的高位(如 32 位程序的 2g 高位内存地址)做一个映射,把 dll 和这个地址关联起来。当第二个程序也加载同一个 dll 的时候,由于内存中已经存在了,于是只需要给 dll 引用计数加 1,再把 dll 映射到第二个 exe 的线性地址的高位处。使得两个程序使用了同一个 dll,但物理内存中只有一份拷贝的结果,这样就节省了内存(实际过程比这个复杂,以上只是简述)。
dll 的加载分静态和动态,静态是指,在编译 exe 的时候,import table 里,引入要使用的 dll,windows 在加载 exe 的时候立刻就去加载 dll,如果 dll 找不到,你就会看到经常见到的“找不到 dll,无法启动的提示”。
那么 dll 的动态加载,则是并不存 dll 的名称到 exe 的 import table 里,而是在代码里,动态使用 LoadLibrary 函数加载。这样程序启动的时候,系统不会查找加载这个 dll,当程序需要用到 dll 中的逻辑的时候,通过 LoadLibrary 加载进来,如果 dll 不存在,程序也不会崩溃,可以从容处理加载失败的逻辑。
那动态加载有什么用呢?很多用法,其中之一是,你可以开发一份同时支持个人版和商业版的程序,其中把商业逻辑封装到 enterprise.dll 里,把个人版逻辑封装到 person.dll 里,通过你分发的配置或者其他逻辑,来动态加载对应逻辑的 dll,实现不同版本的分发。
当然有 dll 的存在也使得你有机会在不全部更新所有文件的情况下对大型系统打补丁。试想如果没有 dll,windows 的更新,可能每次都要 20-30g 吧。