C#编译后IL代码后置处理及客户端/服务端代码裁剪的正确方案
如何通过MSBuild任务实现C#游戏客户端/服务端的IL代码按需裁剪?
需求概述
开发一款包含客户端和服务端的游戏,大部分代码双方共享,部分类型、字段、方法带有Side属性标记所属端。需要通过客户端/服务端两种构建配置,在C#编译完成后、MSBuild输出前、IDE运行前,对IL代码进行后置处理,移除不属于当前配置的代码元素。
尝试过程
创建了两个项目:
DynamicCodeTest:待处理的目标游戏项目Task:包含自定义MSBuild任务类Class1的项目,依赖Microsoft.Build
遇到两个问题:
- 在
DynamicCodeTest.csproj中使用<UsingTask AssemblyName="Task">引用任务程序集时,MSBuild无法找到该程序集 - 改用
<UsingTask AssemblyFile="路径/Task.dll">指定路径后,出现Task.dll被其他进程占用,无法从obj目录复制到构建目录的问题
解决方案
1. 用AssemblyName引用MSBuild任务的正确配置方式
要让MSBuild通过AssemblyName识别任务程序集,推荐以下两种方式:
- NuGet包方式(最优):将
Task项目打包为NuGet包,在DynamicCodeTest项目中安装该包。NuGet会自动将任务程序集加入MSBuild搜索路径,此时使用<UsingTask AssemblyName="Task" TaskName="Task.Class1" />即可正常引用。 - 手动添加搜索路径:若不使用NuGet,可在
DynamicCodeTest.csproj中添加Task项目输出目录到MSBuild的程序集搜索路径:
<PropertyGroup> <AssemblySearchPaths>$(AssemblySearchPaths);$(SolutionDir)Task\bin\$(Configuration)\netstandard2.0</AssemblySearchPaths> </PropertyGroup>
注意替换为实际的Task项目输出路径,同时需确保Task项目在DynamicCodeTest之前完成构建。
2. 解决Task.dll文件占用问题
文件锁定通常由IDE或MSBuild进程导致,可通过以下方式规避:
- 分离Task项目与DynamicCodeTest的输出目录,直接引用Task的输出DLL路径,避免复制操作
- 在Task项目的csproj中禁用可能导致文件锁定的特性:
<PropertyGroup> <EnableHotReload>false</EnableHotReload> <UseSharedCompilation>false</UseSharedCompilation> </PropertyGroup>
- 自定义任务代码中避免持有自身DLL的引用,确保任务执行完成后释放所有资源
3. IL代码裁剪的核心实现
借助Mono.Cecil库(专门用于操作IL代码)实现裁剪逻辑,示例任务代码片段:
public class TrimIlTask : Task { [Required] public string TargetAssemblyPath { get; set; } [Required] public string BuildSide { get; set; } // 传入Client/Server标识 public override bool Execute() { using var assembly = AssemblyDefinition.ReadAssembly(TargetAssemblyPath); // 遍历程序集中所有类型、字段、方法 foreach (var module in assembly.Modules) { foreach (var type in module.Types.ToList()) { // 检查Side属性,移除不属于当前构建端的类型 if (HasInvalidSideAttribute(type, BuildSide)) { module.Types.Remove(type); continue; } // 处理字段和方法的裁剪逻辑 TrimTypeMembers(type, BuildSide); } } // 保存修改后的程序集 assembly.Write(TargetAssemblyPath); return true; } // 辅助方法:检查元素的Side属性是否匹配当前构建端 private bool HasInvalidSideAttribute(ICustomAttributeProvider provider, string targetSide) { var sideAttr = provider.CustomAttributes.FirstOrDefault(a => a.AttributeType.Name == "SideAttribute"); if (sideAttr == null) return false; var attrValue = sideAttr.ConstructorArguments[0].Value.ToString(); return attrValue != targetSide; } // 辅助方法:裁剪类型内的字段和方法 private void TrimTypeMembers(TypeDefinition type, string targetSide) { // 移除不符合条件的字段 foreach (var field in type.Fields.ToList()) { if (HasInvalidSideAttribute(field, targetSide)) type.Fields.Remove(field); } // 移除不符合条件的方法 foreach (var method in type.Methods.ToList()) { if (HasInvalidSideAttribute(method, targetSide)) type.Methods.Remove(method); } } }
4. 任务触发时机配置
在DynamicCodeTest.csproj中,将任务绑定到编译完成后、文件复制到输出目录前的阶段:
<Target Name="TrimIlCode" AfterTargets="CoreCompile" BeforeTargets="CopyFilesToOutputDirectory"> <TrimIlTask TargetAssemblyPath="$(TargetPath)" BuildSide="$(Configuration)" /> </Target>
这里假设构建配置名称直接对应Client/Server,可根据实际情况调整BuildSide的传入值。
内容的提问来源于stack exchange,提问作者128_bit_guy
相关产品推荐
相关产品推荐

