从嵌入式资源加载 DLL
将 DLL 与应用程序合并到单个 EXE 文件中
- 下载 EmbeddedAssembly_1.3.zip bug 已修复
Content
- 引言
- 何时以及为何需要嵌入?
- 将 DLL 添加为嵌入式资源
- 开始编码
- 深入了解 EmbeddedAssembly.cs
- EmbeddedAssembly.cs 类
- 相关提示
- 替代方案
- 文章历史
引言
通常,当我们添加自定义构建或第三方 DLL 的引用时,它将与我们的主 EXE 应用程序一起分发到同一文件夹中。如果 DLL 不存在于 EXE 应用程序的同一文件夹中,则会抛出异常。在某些情况下,我们希望将 DLL 打包/包含在 EXE 应用程序中。因此,本文介绍了一种从嵌入式资源加载 DLL 的解决方案。
异常示例(在分发的 EXE 应用程序的同一文件夹中找不到 DLL)
何时以及为何需要嵌入?
合并 DLL 通常用于开发便携式软件。
优点
- 易于在最终用户之间分发。(复制 1 个文件而不是多个文件)
- 最终用户复制软件时,依赖的 DLL 和文件不会丢失或意外遗漏。
- 最终用户不会将文件与其他不相关的文件混淆。
缺点
- 增加了开发人员的工作复杂性。开发人员需要采取额外的步骤来合并文件。
- 如果某些组件有新版本,则需要重新打包整个内容。如果 DLL 没有合并,开发人员很容易更改这些部分。(然而,这对最终用户来说将是一件困难的事情,最终用户宁愿下载整个内容而不是手动替换 DLL。)
嵌入 DLL 的概念与大多数软件安装程序相同。大多数安装程序都打包成一个文件。这样做的原因是为了降低复杂性,使其更容易分发。
将 DLL 添加为嵌入式资源
本文中用作示例的 DLL 的基本介绍。
-
- System.Windows.Forms.Ribbon35.DLL
- - 类型:托管 DLL
- - .NET WinForm 的开源功能区控件
- - 阅读更多:http://officeribbon.codeplex.com,
- System.Data.SQLite.DLL
- - 类型:混合代码/非托管 DLL
- - .NET 的开源跨平台嵌入式数据库引擎包装器
- - 阅读更多: http://system.data.sqlite.org
- System.Windows.Forms.Ribbon35.DLL
在开始编码之前,必须将 DLL 添加到项目中。
首先,将 DLL 添加为引用。
然后,将同一 DLL 作为文件添加到项目中。右键单击项目名称 > 添加 > 现有项...
同一个 DLL 将在项目的不同文件夹中存在两次。
如下图所示,
对于引用的 DLL,在属性资源管理器中,将 <code>复制本地
= False
对于作为文件添加的 DLL,在属性资源管理器中,将 生成操作
= 嵌入式资源
对 System.Data.SQLite.DLL 执行相同的操作。将其添加到项目文件并作为引用。更改属性,如 System.Windows.Forms.Ribbon35.DLL。
注意:如果您有任何其他无法引用的非托管/本机 DLL,则无需引用它,只需将非托管 DLL 添加为嵌入式资源。
开始编码
从下载中获取 EmbeddedAssembly.cs 并将其添加到项目中。
或者浏览到本文底部并复制 EmbeddedAssembly.cs 的代码。
打开 Program.cs
Program.cs
的初始代码
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
从嵌入式资源将 DLL 加载到内存中。使用 EmbeddedAssembly.Load
将其加载到内存中。
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
string resource1 = "MyApp.System.Windows.Forms.Ribbon35.dll";
string resource2 = "MyApp.System.Data.SQLite.dll";
EmbeddedAssembly.Load(resource1, "System.Windows.Forms.Ribbon35.dll");
EmbeddedAssembly.Load(resource2, "System.Data.SQLite.dll");
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
注意资源字符串的格式。示例:
MyApp.System.Windows.Forms.Ribbon35.dll
此字符串是项目中嵌入式资源的地址。
MyApp
是项目名称,后跟 DLL 文件名。如果 DLL 添加到文件夹中,则文件夹名称必须包含在资源字符串中。示例
MyApp.NewFolder1.System.Windows.Forms.Ribbon35.dll
DLL 不与应用程序一起分发,当应用程序无法定位 DLL 时,它会引发 AppDomain.CurrentDomain.AssemblyResolve
事件。AssemblyResolve 请求缺少的 DLL。然后,我们将告诉我们的应用程序在内存中查找 DLL。使用 EmbeddedAssembly.Get
从内存中检索 DLL 并将其传递给 AssemblyResolve 来处理其余部分。
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
string resource1 = "MyApp.System.Windows.Forms.Ribbon35.dll";
string resource2 = "MyApp.System.Data.SQLite.dll";
EmbeddedAssembly.Load(resource1, "System.Windows.Forms.Ribbon35.dll");
EmbeddedAssembly.Load(resource2, "System.Data.SQLite.dll");
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);S
Application.Run(new Form1());
}
static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
return EmbeddedAssembly.Get(args.Name);
}
}
就这样。
深入了解 EmbeddedAssembly.cs
在这里,我们将了解 EmbeddedAssembly.cs
在幕后做了什么。
有两种常见的 DLL 类型将与 .NET 应用程序一起使用
- 托管 DLL
- 非托管 DLL 或混合代码 DLL
托管 DLL 完全由 .NET 语言编写(即 C#、VB.NET)
非托管 DLL 将由其他语言编写(即 C、C++)
System.Data.SQLite.DLL 是混合代码。它是 C 和 C# 的组合。因此,它可以被视为非托管 DLL。
加载托管 DLL
从嵌入式资源加载托管 DLL 的过程很简单。
static class Program
{
[STAThread]
static void Main()
{
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
return Load();
}
}
public static Assembly Load()
{
byte[] ba = null;
string resource = "MyApp.System.Windows.Forms.Ribbon35.dll";
Assembly curAsm = Assembly.GetExecutingAssembly();
using (Stream stm = curAsm.GetManifestResourceStream(resource))
{
ba = new byte[(int)stm.Length];
stm.Read(ba, 0, (int)stm.Length);
return Assembly.Load(ba);
}
}
加载非托管 DLL
非托管 DLL 不能像托管 DLL 那样直接从 stream/byte[]
加载。
- 首先将 DLL 从嵌入式资源加载到
byte[]
中。 - 将
byte[]
写入物理文件并存储在临时文件夹中。 - 使用
Assembly.LoadFile()
将文件加载到内存中。
步骤在下面的代码注释中解释:
public static Assembly Load()
{
// Get the byte[] of the DLL
byte[] ba = null;
string resource = "MyApp.System.Data.SQLite.dll";
Assembly curAsm = Assembly.GetExecutingAssembly();
using (Stream stm = curAsm.GetManifestResourceStream(resource))
{
ba = new byte[(int)stm.Length];
stm.Read(ba, 0, (int)stm.Length);
}
bool fileOk = false;
string tempFile = "";
using (SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider())
{
// Get the hash value of the Embedded DLL
string fileHash = BitConverter.ToString(sha1.ComputeHash(ba)).Replace("-", string.Empty);
// The full path of the DLL that will be saved
tempFile = Path.GetTempPath() + "System.Data.SQLite.dll";
// Check if the DLL is already existed or not?
if (File.Exists(tempFile))
{
// Get the file hash value of the existed DLL
byte[] bb = File.ReadAllBytes(tempFile);
string fileHash2 = BitConverter.ToString(sha1.ComputeHash(bb)).Replace("-", string.Empty);
// Compare the existed DLL with the Embedded DLL
if (fileHash == fileHash2)
{
// Same file
fileOk = true;
}
else
{
// Not same
fileOk = false;
}
}
else
{
// The DLL is not existed yet
fileOk = false;
}
}
// Create the file on disk
if (!fileOk)
{
System.IO.File.WriteAllBytes(tempFile, ba);
}
// Load it into memory
return Assembly.LoadFile(tempFile);
}
EmbeddedAssembly.cs
将预加载所需的 DLL 并将其存储在 Dictionary
中。当应用程序的 AssemblyResolve
事件触发时,EmbeddedAssembly.Get()
将返回请求的 DLL。
EmbeddedAssembly.cs 类
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Reflection;
using System.Security.Cryptography;
public class EmbeddedAssembly
{
// Version 1.3
static Dictionary<string, Assembly> dic = null;
public static void Load(string embeddedResource, string fileName)
{
if (dic == null)
dic = new Dictionary<string, Assembly>();
byte[] ba = null;
Assembly asm = null;
Assembly curAsm = Assembly.GetExecutingAssembly();
using (Stream stm = curAsm.GetManifestResourceStream(embeddedResource))
{
// Either the file is not existed or it is not mark as embedded resource
if (stm == null)
throw new Exception(embeddedResource + " is not found in Embedded Resources.");
// Get byte[] from the file from embedded resource
ba = new byte[(int)stm.Length];
stm.Read(ba, 0, (int)stm.Length);
try
{
asm = Assembly.Load(ba);
// Add the assembly/dll into dictionary
dic.Add(asm.FullName, asm);
return;
}
catch
{
// Purposely do nothing
// Unmanaged dll or assembly cannot be loaded directly from byte[]
// Let the process fall through for next part
}
}
bool fileOk = false;
string tempFile = "";
using (SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider())
{
string fileHash = BitConverter.ToString(sha1.ComputeHash(ba)).Replace("-", string.Empty);;
tempFile = Path.GetTempPath() + fileName;
if (File.Exists(tempFile))
{
byte[] bb = File.ReadAllBytes(tempFile);
string fileHash2 = BitConverter.ToString(sha1.ComputeHash(bb)).Replace("-", string.Empty);
if (fileHash == fileHash2)
{
fileOk = true;
}
else
{
fileOk = false;
}
}
else
{
fileOk = false;
}
}
if (!fileOk)
{
System.IO.File.WriteAllBytes(tempFile, ba);
}
asm = Assembly.LoadFile(tempFile);
dic.Add(asm.FullName, asm);
}
public static Assembly Get(string assemblyFullName)
{
if (dic == null || dic.Count == 0)
return null;
if (dic.ContainsKey(assemblyFullName))
return dic[assemblyFullName];
return null;
}
}
相关提示
提示 1:在另一个程序集内获取程序集(由 Thierry Maurel 建议)
如果 DLL 嵌入到另一个 DLL 中,则可以使用
Assembly a1 = GetType ().Assembly;
获取本地 dll 程序集,而不是可执行程序集。
替代方案
ILMerge
- ILMerge 是一个将任何托管程序集(EXE、DLL)合并为一个程序集的工具。
- http://ilmergegui.codeplex.com/
Mergebin - 将您的非托管和托管 DLL 合并到一个便捷包中
文章 - 从非托管代码调用托管代码,反之亦然
- 将非托管 DLL 转换为托管 DLL。
- https://codeproject.org.cn/Articles/9903/Calling-Managed-Code-from-Unmanaged-Code-and-vice
SmartAssembly(商业版)
- 合并托管和非托管 DLL 的商业工具。
- http://www.smartassembly.com/
Costura(由 herves 建议)
- ILMerge 的替代品。
- https://github.com/Fody/Costura
- 您可以在此处讨论: https://codeproject.org.cn/Messages/4473198/Costura.aspx
将所有额外文件/DLL 打包为存档文件 - Zip/CAB/Tar(由 Fred Flams 建议)
- 将 DLL 打包到存档文件中。在应用程序启动时,将存档文件解包到内存或磁盘上。
- 主 EXE 将更轻。将存档文件与主 EXE 的根名相同地分发。最终用户将知道这两个文件(EXE 和存档文件)必须一起使用。
将 DLL 转换为加密类并在运行时从类加载
Vitevic 程序集嵌入器
- http://vitevic.com/assembly_embedder.html
- 完全集成到 Visual Studio (2010, 2012, 2013) 和 MSBuild。
- 点击几下即可合并 DLL。
在同一解决方案中构建时自动合并所有项目的所有源文件。
- 合并多个 .NET 程序集
- 通过自定义 MSBuild 的项目文件来合并多个 .NET 程序集。
- 由 Mario Z 贡献
LibZ
- http://libz.codeplex.com/
- LibZ 是 ILMerge 的替代品。它允许将您的应用程序和库作为单个文件分发。
- 由 Brisingr Aerowing 建议
BoxedApp
- http://www.boxedapp.com
- BoxedApp SDK 模拟文件,因此开发人员创建一个虚拟文件(伪文件)——一个磁盘上不存在的“文件”,但进程的工作方式就像这个文件真的存在一样。BoxedApp 拦截文件调用以实现此目的。
- 由 User-11642949 建议
文章历史
- 2013 年 1 月 16 日 - 初次发布。