Unity中dll引用与NuGet包安装的完整实操与原理详解

发布时间:2026/9/19 17:35:39
Unity中dll引用与NuGet包安装的完整实操与原理详解 很多Unity项目做到一半都会撞上一堵墙某个功能需要第三方库网站上写着“Install-Package SomePackage”你兴冲冲打开NuGet然后发现Unity根本没有“管理NuGet程序包”这个按钮或者你辛辛苦苦找到一个dll拖进工程后编译直接给你砸一堆红色报错。这篇文章就把这件事彻底讲明白——Unity的C#项目里添加dll引用和安装NuGet包到底该怎么操作每种方式背后的原理是什么以及那些网上零碎教程不会告诉你的坑。先说清楚适用范围无论你是刚开始接触Unity的初学者还是已经在做主程、经常要给项目引第三方SDK的人这篇文章都值得看完。我会把“托管dll”“原生DLL”“NuGet包在Unity里的三种引入路径”“IL2CPP下dll冲突和平台部署问题”这些事拆开讲每一步都给出能直接照做的操作也会解释为什么这么做避免你今天抄作业成功、明天换个库又翻车。1. 引用之前先判断dll有三种操作方式完全不同很多人第一反应是把dll直接拖进Assets文件夹就完事然后祈祷它能工作。这个思路对了一半但前提是你手里的dll是“托管程序集”。如果不是你会踩进一个完全不同的坑里。所以第一步不是动手拖文件而是先搞清楚你拿到的dll到底是哪一类。1.1 托管程序集NuGet包里绝大多数是这一种托管程序集Managed Assembly是由C#、VB.NET等.NET语言编译出来的产物里面装的是IL中间语言。Unity的Mono运行时或者IL2CPP后端可以直接加载执行这类dll。判断方法很简单用ILSpy或dnSpy打开这个dll如果能正常看到里面的类、命名空间还能看到引用了一堆System系列的dll那基本就是托管程序集。这类dll在Unity里的使用本质上和普通C#工程里“添加引用”是一样的只是Unity没有VS里那个右键“Add Reference”按钮你需要手动把它放进Assets目录Unity会自动把它编译成项目可引用的预编译程序集。NuGet上下载的绝大多数包直接把lib目录下的dll拿过来用走的就是这条路。1.2 原生DLL扩展名相同生态完全不同原生DLL本质是C/C代码编译出来的二进制文件Windows下常见的是MSVC编译的dllAndroid下是.somacOS下是.dylibiOS下会被打包成静态库或framework。这类文件Unity不能直接“引用”只能通过P/Invoke方式调用也就是在C#里用DllImport声明函数入口点。举个例子你从某硬件厂商拿到一个串口通信SDK里面往往是Windows平台的原生dll加一个C#封装层。原生dll要放在Assets/Plugins/x86_64这样的特定架构目录下C#封装层里通过DllImport调用它。如果你把原生dll当成托管程序集随处乱放编译不会报错但运行时一定会抛DllNotFoundException。1.3 混合模式程序集最容易埋雷的一类还有一类比较阴间的dll叫混合模式程序集通常由C/CLI编译生成。这类dll里面既有IL代码又包含原生机器码Unity对它们的支持非常有限。实测下来很多混合模式的dll在编辑器里加载就会报“System.BadImageFormatException”或者“The module was expected to contain an assembly manifest”即使能勉强引用成功打了IL2CPP包之后也大概率崩溃。遇到这种dll我的建议是别折腾了去找纯托管的替代品或者要求SDK厂商提供.NET Standard版本。Unity本质上不是Windows桌面那样的通用CLR宿主它对程序集的加载方式更挑剔。1.4 用一张表快速判断手里的dll属于谁所以在动手之前建议先花30秒做一次判断对照下面这张表判断维度托管程序集原生DLL混合模式程序集文件图标通常显示为应用程序扩展带齿轮或盾牌标识类似托管但属性怪ILSpy打开能看见类、命名空间打不开或全是导出函数能看到类但引用了C运行库能否被Unity直接引用能不能必须DllImport大概率失败平台相关跨平台按目标框架区分每个平台单独提供只支持Windows且受限运行时依赖需要对应的.NET兼容层需要系统运行库需要C运行库等记住一个铁律拿到dll先确认类别再决定下一步怎么放。类别判断错了后面每一步都在给错误方案加码问题只会越滚越大。2. 托管DLL导入Assets的完整规范目录、Importer、asmdef一个不能少好确认了是托管程序集接下来就是标准的导入流程。我见过太多人把dll随手丢进Assets根目录今天能跑明天就崩。这个章节把正确的操作和背后的原因讲透你照着做之后基本不会再出幺蛾子。2.1 放对目录为什么我更推荐Assets/Plugins而不是随手丢根目录托管dll理论上可以放在Assets下任意目录但项目里我强烈建议统一放到Assets/Plugins下并且按平台建子目录。原因有三个第一Plugins目录是Unity特设的“插件目录”放在里面的程序集默认会被所有平台引用但如果放进Plugins/Android、Plugins/iOS、Plugins/x86_64这样的子目录Unity会在对应平台构建时自动包含对应的dll版本。这给多平台分发提供了天然的组织结构。第二Assets根目录下的dll会直接参与项目的预编译如果以后要切换到asmdef体系根目录里的dll很难隔离控制引用关系会很混乱。第三Plugins目录本身就是个“插件信号区”团队里的其他人一看路径就知道这里是外部依赖不会误改。实际操作中单平台项目我习惯放在Assets/Plugins/Managed下虽然这个目录名不是强制的但它能明确表达“这里放的是托管程序集”。多平台项目需要为不同平台提供不同dll版本就按Assets/Plugins/Android、Assets/Plugins/iOS这样分。2.2 Plugin Inspector每处设置分别意味着什么把dll放进Assets后在Inspector面板会看到Plugin Importer。第一次接触的人容易被面板上一堆勾选框绕晕。其实关键就两个判断这个dll在哪些平台生效以及CPU架构要求是什么。面板上的平台分类主要是Standalone、Android、iOS、WebGL等。如果dll是纯托管程序集理论上任何平台都能用你可以直接勾上Any Platform或保持默认——Unity默认会把它包含在除Editor外的所有平台中。注意这句反直觉的话默认状态下“Editor”是被排除的。如果你需要在编辑器里也正常使用这个dll比如写编辑器工具脚本必须勾选Include Platforms里的Editor否则运行时编辑器里会报FileNotFoundException。CPU架构这个下拉选项也要留意。很多纯托管dll会显示为AnyCPU或者无此选项不用管。但如果dll是按x86_64或ARM64单独编译的这里一定要选对不然构建到真机上会崩溃。还有一点容易被忽略面板最下面有一个“Exclude Editor”的选项。我做编辑器工具类库时反而建议只勾Editor取消其他所有平台这样dll只参与编辑器代码编译不会被打进最终包体能省一点包体空间也减少运行时被意外加载的风险。2.3 程序集定义asmdef的作用与一个可以直接用的模板程序集定义文件Assembly Definition File是Unity把项目代码按模块划分的核心工具很多初学者一听到asmdef就觉得头大但引入第三方dll时它是真的救命。默认情况下Unity会把Assets下的所有C#脚本编译成Assembly-CSharp这个超大程序集第三方dll则以“预编译程序集”的身份被所有程序集自动引用。问题来了如果你有两个库各自依赖了同一个dll但版本不同或者某个dll只想让特定模块用这种混乱的全局引用会直接影响编译速度和可维护性。把第三方dll放进一个用asmdef包裹的文件夹里等于给这些外来的程序集画了一个“隔离区”。这个asmdef可以独立控制是否自动引用、在哪些平台生效、引用哪些其他程序集。项目越来越大之后这套隔离方案能省掉你至少一个通宵的排查时间。下面是个可以直接用的最小asmdef模板放在包含dll的文件夹根目录命名为比如ThirdPartyDependencies.asmdef{ name: ThirdPartyDependencies, rootNamespace: , references: [], includePlatforms: [], excludePlatforms: [], allowUnsafeCode: false, overrideReferences: false, precompiledReferences: [], autoReferenced: true, defineConstraints: [], versionDefines: [], noEngineReferences: false }几个关键字段说清楚autoReferenced设为true时其他程序集都能直接using这个asmdef里的命名空间如果你希望只有指定程序集能用这里的dll把这个字段改成false然后在需要的asmdef的references数组里填入这个asmdef的名字。includePlatforms和excludePlatforms留空表示所有平台都包含如果你希望某个库只在编辑器里存在includePlatforms里填Editor即可。allowUnsafeCode看库的需求有些库内部用了unsafe代码记得对应打开。2.4 添加完dll后的标准验证流程导入dll后不要直接开写业务代码花两分钟做个系统验证可以省掉后面大量排查时间。打开Unity右下角的Console面板确认没有编译错误。然后在任意脚本里写一行using语句比如using Newtonsoft.Json;随便声明一个变量编译通过基本说明引用成功。但编译通过只代表“元数据可见”不代表运行时真的能加载。正确做法是写一段简单的初始化代码在Awake或OnEnable里new一个目标对象用Debug.Log输出一句标记并在编辑器里运行一次。我还习惯看一眼Player Settings里的Api Compatibility Level这个选项在Player设置面板的Other Settings下。它决定了项目运行时对dll的兼容标准推荐使用.NET Standard 2.1。如果你的dll是给.NET 6或.NET 8编译的即使编译通过运行时大概率会因为缺少类型而抛MethodNotFoundException。这个细节经常被忽略但它几乎是dll运行时报错的头号来源。3. NuGet包进入Unity的三条可靠路线及选型逻辑托管的dll聊完了接下来说NuGet包。平常做.NET开发时一条Install-Package命令能解决的事到了Unity里怎么就这么难原因很简单Unity并没有完整实现NuGet的依赖解析与还原机制它连自己包管理的依赖解析都刚刚才普及更别说NuGet了。所以想用NuGet包必须绕路。目前看来有三条可靠路线各有取舍。3.1 手动解压.nupkg可控性最强但要选对目标框架这是最朴素也最可控的方法。去nuget.org搜索你需要的包点击Download package下载一个.nupkg文件。这个文件本质是zip压缩包直接改后缀为.zip再解压就能看到里面有个lib目录。lib目录下通常按目标框架分了好几个子文件夹比如net462、netstandard2.0、net6.0、net8.0等。我的建议永远是优先选择netstandard2.0目录下的dll。为什么因为netstandard2.0是Unity 2018.4以后都支持的.NET兼容标准覆盖面最广。千万别贪新鲜选net8.0Unity不是.NET 8运行时塞进去根本跑不了。如果你的Unity版本高一些Api Compatibility Level设成了.NET Standard 2.1那netstandard2.1的dll也可以考虑但还是那句netstandard2.0容错率最高。手动方式的隐患在依赖链。很多现代库不是单一一个dll它会依赖其他包。比如你用System.Text.Json它依赖System.Memory、System.Buffers、System.Runtime.CompilerServices.Unsafe等一堆Syste系dll。如果你只拷主力dll进去编译期可能不报错运行期必定报FileNotFoundException或者TypeLoadException。所以选择这个方案要顺着依赖链把所有dll逐个拖下来工作量大而且容易遗漏。3.2 用NuGetForUnity插件自动化依赖处理的主力方案手动方式只适合只依赖零个或一两个子包的库项目一复杂就轮到NuGetForUnity登场了。这个开源插件把NuGet的还原流程做进了Unity编辑器是我目前最推荐的一条路。安装方式在Unity Package Manager里点击左上角加号 - Add package from git URL填入https://github.com/GlitchEnzo/NuGetForUnity.git。安装成功后菜单栏会多出一个NuGet菜单里面有Manage NuGet Packages。点击这个菜单会打开一个类似Package Manager的窗口左侧搜索你要的包右侧能看到版本和依赖信息选中后点Install插件就会自动做下面这几件事下载.nupkg并解析依赖、把主dll和所有依赖dll一起放进Assets/Packages目录、为每个包生成独立的asmdef文件避免程序集冲突、在Packages目录下记录包信息方便后续更新或移除。NuGetForUnity帮了大忙的地方正是依赖解析和asmdef隔离这两点恰恰是手动方案最容易翻车的地方。它生成的asmdef把每个NuGet包都隔离成独立程序集依赖关系和平台开关都保持默认不需要你额外调整。实测下来整个安装链路基本是稳定的唯一的门槛是首次从外网拉包时偶尔会慢多点几次重试通常能解决。3.3 优先检查官方包管理很多常见库已被Unity官方收录在动手下载第三方工具之前永远先做一件事打开Unity Package Manager窗口在左上角切换包来源看看有没有官方或社区维护好的包。这个习惯能替你节省大量时间。最典型的是Newtonsoft.Json。早年间大家都在手动塞dll还要处理版本冲突。后来Unity官方把Newtonsoft.Json以com.unity.nuget.newtonsoft-json的包名收录进了包管理器直接在UPM里搜索并安装最新版本所有依赖都由引擎的管理机制处理不会再出现“重复引用”的问题。很多常用库比如序列化相关的、日志相关的、HTTP通信相关的都陆续以官方或已验证包的身份进入了UPM生态。养成“先搜UPM再搜NuGet”的习惯至少能避开一半的dll纠纷。3.4 三条路线怎么选一个真实场景下的对比表最终怎么选取决于你的项目阶段和依赖复杂度。我这里给一个自己常用的决策表情形推荐方案原因网络环境好项目用Unity 2019NuGetForUnity自动化依赖省心省力只想用一两个无依赖的包比如简单的JSON库手动解压.nupkg可控不引入额外插件官方UPM仓库已收录优先UPM引擎原生管理无冲突风险需要离线开发或内网限制手动解压本机缓存NuGetForUnity联网受限制懒人模式且项目网络OKNuGetForUnity最接近Visual Studio的NuGet体验有一类特殊场景是团队协作如果你用NuGetForUnity它会在Assets/Packages里生成实际文件这些文件是实实在在放进版本库的队友拉代码后不需要再执行任何还原操作直接用。这一点反而比纯.NET项目里的NuGet还原更省事因为Unity没有还原步骤文件在场就能编译。4. 构建阶段的连环坑IL2CPP限制、dll冲突排查与跨平台部署终于到构建了但构建才是很多dll问题真正爆发的时刻。你在编辑器里跑得飞起的库切到IL2CPP或发到真机上突然就出问题。这个章节集中讲构建阶段最常见的四类坑全都来自实际踩坑经验。4.1 GameAssembly.dll是怎么产生的以及对第三方库的连锁要求先搞明白一个底层事实Unity的默认脚本后端是Mono编辑器里和在Mono模式下托管程序集是被直接加载执行的。但做Android、iOS、微信小游戏等平台构建时绝大多数项目会切到IL2CPP脚本后端。IL2CPP会把所有用到的托管程序集包括你自己写的代码和第三方dll先转成C再交给各平台原生编译器编译。以Windows平台为例最终产物就是GameAssembly.dll你游戏的全部C#逻辑都在这一个大个的原生二进制里。这个流程对大多数常规dll是没问题的但有三类库在IL2CPP下会翻车依赖运行时动态生成代码的库比如大量使用System.Linq.Expressions或Emit的依赖复杂反射的库IL2CPP的AOT编译特性决定了部分反射场景受限以及那些依赖非托管内存和平台特定API的库。如果你的库在IL2CPP构建后出现MissingMethodException、ExecutionEngineException这类诡异报错很大概率就是撞上这些限制了。处理方法也没有捷径要么在项目设置里退回Mono只适合不需要AOT的平台要么换库要么找库作者确认是否支持IL2CPP。我见过不少项目在这个问题上的处理方法是给库作者提issue不少成熟库后来都专门做了IL2CPP兼容修复所以引入一个冷门库之前先确认它的说明文档里有没有IL2CPP兼容性声明是个很好的习惯。4.2 最典型的dll冲突场景和完整排查链路dll冲突是Unity里最常见的“编译没过”类问题而且报错信息很有误导性。经典报错长这样error CS0433: The type JsonConvert exists in both Newtonsoft.Json, Version13.0.2... and Newtonsoft.Json, Version12.0.1...。看到这个first reaction往往是“版本不对换个版本”但真正的病灶往往是你项目里同时存在两份Newtonsoft.Json一份可能来自你自己手动拖的dll另一份来自某个包自带的依赖。排查链路按下面这个顺序走最快定位第一步在Unity Project窗口里切换到Project视图搜索Newtonsoft.Json.dll看Assets目录下总共有哪些副本第二步打开Packages/manifest.json文件查看UPM包列表确认是否引用了com.unity.nuget.newtonsoft-json第三步检查所有名为Plugins的目录有些第三方SDK会自己塞一份dll进来第四步找到重复的所有位置后保留你真正需要的那个其他全部删除注意如果你用的是NuGetForUnity它管理的那份不要手动删用插件卸载接口处理第五步关闭Unity编辑器删除工程根目录下的Library文件夹再重新打开。第五步看起来粗暴但Unity对预编译程序集的缓存相当顽固有时候你明明已经删了冲突文件它还会用缓存报错清Library是成本最低的暴力解法。处理完冲突后建议全局搜索项目里using对应命名空间的所有代码跑一次完整编译确认无报错。同时检查两个程序集是否真的版本不兼容最稳妥的方法是到GAC里翻版本号但Unity里最简单的是直接在代码里打印某个关键类型的Assembly.GetName().Version验证和预期一致。4.3 原生DLL在不同平台上的部署差异如果你的项目涉及Android设备比如Pico4或主流安卓手机原生DLL的坑就变成ABI匹配问题。Unity在Android上支持的ABI包括armeabi-v7a、arm64-v8a、x86、x86_64你放入Plugins/Android的原生dll实际通常是.so文件必须明确标注支持的ABI。如果你只提供了x86_64版本在绝大多数现代手机上是加载不出来的结果就是运行时DllNotFoundException。反过来如果你在编辑器里用Windows版本dll测试通过、发到Android上却失败了绝大多数原因是.so文件缺失或ABI不匹配。iOS平台会换成静态库或framework需要在Plugin Importer里设置平台为iOS并确保链接方式正确。WebGL和微信小游戏更特殊这两个平台本质上在浏览器沙箱里跑可执行代码和文件访问都受到严格限制。微信小游戏转换工具会在底层把Unity的WebGL产物再包一层很多第三方原生dll和文件读写库在这种环境里根本没法工作。所以做微信小游戏或WebGL导出的项目第三方dll数量能少就少能用官方包管理解决的坚决不往项目里塞原生依赖。4.4 我每次引入外部dll之前都会过一遍的检查清单踩过足够多的坑之后我现在给自己定了一条规矩任何第三方dll进项目前先过一遍下面这张检查清单全绿再动手。这张清单也分享给你[ ] 确认dll是托管程序集还是原生DLL类别判断正确[ ] 确认dll的目标框架是netstandard2.0或项目兼容级别支持的范围[ ] 在UPM官方源里搜索过确认没有官方替代包[ ] 查过目标库的文档确认支持目标平台和IL2CPP[ ] 确认依赖链完整所有依赖dll都一并引入[ ] 检查项目内是否已存在同名或同功能的不同版本[ ] 平台相关dll已放入对应平台目录并设置好CPU架构[ ] 编辑器里做了运行时验证不只编译通过[ ] 切换构建平台测试一次确认无ABI和加载问题[ ] 记录dll版本和来源同步到项目的依赖说明文档这套流程看起来麻烦但一次到位所花的五分钟远比让问题在构建或真机阶段爆发后再花一个下午排查来得划算。尤其是团队项目一份明确的第三方依赖记录能帮后面接手的人省掉大量“这个dll是哪来的”的困惑。最后再分享一个实际体会我在自己项目里现在的习惯是先把项目Api Compatibility Level固定在.NET Standard 2.1然后把NuGetForUnity作为标准工具装好能走UPM官方的先走UPM官方没有的再走NuGetForUnity原生DLL一律独立放在Plugins目录并按平台管理。这套组合拳用下来dll相关的夜没怎么熬过。你按这个思路把流程建立起来Unity项目里引用第三方库这件事真的可以变成一个几分钟就能完成的标准动作。