文章目录
- Unity Mirror 从入门到入神(三)
- NetworkManagerMode
- NetworkServer
- RemoteCall.cs
- NetworkIdentity
- RemoteCall.cs
- ILPostProcessorHook
- InMemoryAssembly
- ICompiledAssembly
- TypeReference
- MethodReference
- ParameterDefinition
- ILProcessor
- Weaver
- ReaderWriterProcessor
- NetworkWriterExtensions
- ReaderWriterProcessor
- Weaver
- SyncVarAttributeProcessor
- CommandProcessor.csd
- MethodProcessor
- Weaver.cs
Unity Mirror 从入门到入神(三)
在前面我们了解了,Mirror是如何同步生成对象的,接下来我们来看看如何控制生成的对象。在此之前补充一下Command的生效逻辑,以及NetworkBehavior序列化方面的相关知识点,首先来看看Command是如何生效的,我们都知道Command是的调用方向是客户端到服务器,所以理论上来说Hander应该是服务器处理的,顺着这个逻辑找下去就能在NetworkServer.RegisterMessageHandlers
找到hander方面的逻辑,至于客户端如何传递参数过来了,我们留到后面再说,先来看看服务器部分是如何分发Command调用的。
另外我猜测Command 在客户端应该是利用了拦截切面之类的能力,来捕获方法执行的
以下代码根据服务端按照代码执行顺序,贴出代码存在删减,如需查看全部代码请访问官网,这里前提假设我们已Host模式启动
NetworkManagerMode
StartHost
public void StartHost()
{
if (NetworkServer.active || NetworkClient.active)
{
Debug.LogWarning("Server or Client already started.");
return;
}
mode = NetworkManagerMode.Host;
// StartHost is inherently ASYNCHRONOUS (=doesn't finish immediately)
//
// Here is what it does:
// Listen
// ConnectHost
// if onlineScene:
// LoadSceneAsync
// ...
// FinishLoadSceneHost
// FinishStartHost
// SpawnObjects
// StartHostClient <= not guaranteed to happen after SpawnObjects if onlineScene is set!
// ClientAuth
// success: server sends changescene msg to client
// else:
// FinishStartHost
//
// there is NO WAY to make it synchronous because both LoadSceneAsync
// and LoadScene do not finish loading immediately. as long as we
// have the onlineScene feature, it will be asynchronous!
// setup server first
SetupServer();
// scene change needed? then change scene and spawn afterwards.
// => BEFORE host client connects. if client auth succeeds then the
// server tells it to load 'onlineScene'. we can't do that if
// server is still in 'offlineScene'. so load on server first.
if (IsServerOnlineSceneChangeNeeded())
{
// call FinishStartHost after changing scene.
finishStartHostPending = true;
ServerChangeScene(onlineScene);
}
// otherwise call FinishStartHost directly
else
{
FinishStartHost();
}
}
这…,果然Here is what it does:
已经给了所有的关键事件,但是现在我们只关心Command是如何调用的。所以这里只需要关注SetupServer
SetupServer
void SetupServer()
{
// Debug.Log("NetworkManager SetupServer");
InitializeSingleton();
// apply settings before initializing anything 一些配置
NetworkServer.disconnectInactiveConnections = disconnectInactiveConnections;
NetworkServer.disconnectInactiveTimeout = disconnectInactiveTimeout;
NetworkServer.exceptionsDisconnect = exceptionsDisconnect;
if (runInBackground)
Application.runInBackground = true;
if (authenticator != null)
{
authenticator.OnStartServer();
authenticator.OnServerAuthenticated.AddListener(OnServerAuthenticated);
}
ConfigureHeadlessFrameRate();
// start listening to network connections
NetworkServer.Listen(maxConnections); //启动监听开启网络,并初始化监听方法
// this must be after Listen(), since that registers the default message handlers
RegisterServerMessages();
// do not call OnStartServer here yet.
// this is up to the caller. different for server-only vs. host mode.
}
InitializeSingleton
保持单例,启动之后判断是否是dontDestroyOnLoad
,如果是则会进行对应的设置,并把节点强制挂到根目录下,authenticator
链接阶段的鉴权,暂时不关注就当没有,ConfigureHeadlessFrameRate
如果是headless的状态下,会进行锁帧,帧数被设定为NetworkManager.sendRate
配置的数值 不管他。重点关注NetworkServer.Listen(maxConnections)
该方法包含我们我们要的hander初始化逻辑,RegisterServerMessages
服务端的ServerMessage注册,那些属于服务端的ServerMessage,包含以下事件类型。都是Action
- OnConnectEvent 当客户端连接的时候会发生调用
public static Action<NetworkConnectionToClient> OnConnectedEvent;
- OnDisconnectedEvent 当客户端断开的时候会发生调用
public static Action<NetworkConnectionToClient> OnDisconnectedEvent;
- OnErrorEvent 当出现传输异常的室友发生调用
public static Action<NetworkConnectionToClient, TransportError, string> OnErrorEvent;
- AddPlayerMessage 当客户端发送加入玩家时调用,
NetworkServer.OnServerAddPlayer
会被触发 - ReadyMessage
NetworkServer.SetClientReady
生命周期会被触发
简单了解下即可,暂时不对他们做过多的解读,下面是NetworkServer.Listen的代码
NetworkServer
Listen
public static void Listen(int maxConns)
{
Initialize();
maxConnections = maxConns;
// only start server if we want to listen
if (!dontListen)
{
Transport.active.ServerStart();
if (Transport.active is PortTransport portTransport)
{
if (Utils.IsHeadless())
{
#if !UNITY_EDITOR
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Server listening on port {portTransport.Port}");
Console.ResetColor();
#else
Debug.Log($"Server listening on port {portTransport.Port}");
#endif
}
}
else
Debug.Log("Server started listening");
}
active = true;
RegisterMessageHandlers();
}
RegisterMessageHandlers
internal static void RegisterMessageHandlers()
{
RegisterHandler<ReadyMessage>(OnClientReadyMessage);
RegisterHandler<CommandMessage>(OnCommandMessage);
RegisterHandler<NetworkPingMessage>(NetworkTime.OnServerPing, false);
RegisterHandler<NetworkPongMessage>(NetworkTime.OnServerPong, false);
RegisterHandler<EntityStateMessage>(OnEntityStateMessage, true);
RegisterHandler<TimeSnapshotMessage>(OnTimeSnapshotMessage, true);
}
终于看到RegisterHandler对应的逻辑了,这里注册了,生命周期事件,CommandMessage 就是在这里进行注册的,接着来看下具体的回调方法逻辑,另外简单介绍下其他的几个事件
- ReadyMessage 同上,在RegisterServerMessages 被覆盖了
- CommandMessage 接收来之客户端的Command调用指令
- NetworkPingMessage 字面意思
- NetworkPongMessage 字面意思,Mirror有自己的检测客户端是否在线的pingPong机制,不管他
- EntityStateMessage NetworkBehavior 数值同步暂时不管他
- TimeSnapshotMessage 时间快照暂时不管他
OnCommandMessage
static void OnCommandMessage(NetworkConnectionToClient conn, CommandMessage msg, int channelId)
{
if (!conn.isReady)
{
// Clients may be set NotReady due to scene change or other game logic by user, e.g. respawning.
// Ignore commands that may have been in flight before client received NotReadyMessage message.
// Unreliable messages may be out of order, so don't spam warnings for those.
if (channelId == Channels.Reliable)
{
// Attempt to identify the target object, component, and method to narrow down the cause of the error.
if (spawned.TryGetValue(msg.netId, out NetworkIdentity netIdentity))
if (msg.componentIndex < netIdentity.NetworkBehaviours.Length && netIdentity.NetworkBehaviours[msg.componentIndex] is NetworkBehaviour component)
if (RemoteProcedureCalls.GetFunctionMethodName(msg.functionHash, out string methodName))
{
Debug.LogWarning($"Command {methodName} received for {netIdentity.name} [netId={msg.netId}] component {component.name} [index={msg.componentIndex}] when client not ready.\nThis may be ignored if client intentionally set NotReady.");
return;
}
Debug.LogWarning("Command received while client is not ready.\nThis may be ignored if client intentionally set NotReady.");
}
return;
}
if (!spawned.TryGetValue(msg.netId, out NetworkIdentity identity))
{
// over reliable channel, commands should always come after spawn.
// over unreliable, they might come in before the object was spawned.
// for example, NetworkTransform.
// let's not spam the console for unreliable out of order messages.
if (channelId == Channels.Reliable)
Debug.LogWarning($"Spawned object not found when handling Command message {identity.name} netId={msg.netId}");
return;
}
// Commands can be for player objects, OR other objects with client-authority
// -> so if this connection's controller has a different netId then
// only allow the command if clientAuthorityOwner
bool requiresAuthority = RemoteProcedureCalls.CommandRequiresAuthority(msg.functionHash);
if (requiresAuthority && identity.connectionToClient != conn)
{
// Attempt to identify the component and method to narrow down the cause of the error.
if (msg.componentIndex < identity.NetworkBehaviours.Length && identity.NetworkBehaviours[msg.componentIndex] is NetworkBehaviour component)
if (RemoteProcedureCalls.GetFunctionMethodName(msg.functionHash, out string methodName))
{
Debug.LogWarning($"Command {methodName} received for {identity.name} [netId={msg.netId}] component {component.name} [index={msg.componentIndex}] without authority");
return;
}
Debug.LogWarning($"Command received for {identity.name} [netId={msg.netId}] without authority");
return;
}
// Debug.Log($"OnCommandMessage for netId:{msg.netId} conn:{conn}");
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(msg.payload))
identity.HandleRemoteCall(msg.componentIndex, msg.functionHash, RemoteCallType.Command, networkReader, conn);
}
首先确保连接进入的客户端connet处于ready状态,否则直接返回并打印警告异常,判断当前msg.functionHash是否需要权限,这里functionHash的生成逻辑之前也提到过
RemoteCall.cs
CommandRequiresAuthority
internal static bool CommandRequiresAuthority(ushort cmdHash) =>
GetInvokerForHash(cmdHash, RemoteCallType.Command, out Invoker invoker) &&
invoker.cmdRequiresAuthority;
invoker是 从 static readonly Dictionary<ushort, Invoker> remoteCallDelegates = new Dictionary<ushort, Invoker>();
委托字典中取出,后面再来确认Command是如何将对应的方法信息放入到remoteCallDelegates中的,这里只需要知道,remoteCallDelegates放了所有的Command方法的Hash字典即可,还记得我们之前提到的例子中有 [Command(reuiresAuthority=false)]
这里的即是设置是否需要权限,如果需权限的情况下,则必须满足调用的客户端conn是owner的前提否则直接return,并打印相关日志信息。
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(msg.payload))
identity.HandleRemoteCall(msg.componentIndex, msg.functionHash, RemoteCallType.Command, networkReader, conn);
msg.payload为具体的参数序列化的二进制表达形式,利用networkReader可以将数据按照指定的类型按照规则读取出来,
NetworkIdentity
HandleRemoteCall
internal void HandleRemoteCall(byte componentIndex, ushort functionHash, RemoteCallType remoteCallType, NetworkReader reader, NetworkConnectionToClient senderConnection = null)
{
// check if unity object has been destroyed
if (this == null)
{
Debug.LogWarning($"{remoteCallType} [{functionHash}] received for deleted object [netId={netId}]");
return;
}
// find the right component to invoke the function on
if (componentIndex >= NetworkBehaviours.Length)
{
Debug.LogWarning($"Component [{componentIndex}] not found for [netId={netId}]");
return;
}
NetworkBehaviour invokeComponent = NetworkBehaviours[componentIndex];
if (!RemoteProcedureCalls.Invoke(functionHash, remoteCallType, reader, invokeComponent, senderConnection))
{
Debug.LogError($"Found no receiver for incoming {remoteCallType} [{functionHash}] on {gameObject.name}, the server and client should have the same NetworkBehaviour instances [netId={netId}].");
}
}
identity是通过msg.netId中从spawned中获取的,HandleRemoteCall 应该是一个[ClientRPC]``[Command]
共用的方法,才会有RemoteCallType类型需要传递,了解完Command的流程基本ClientRPC就差不多知道了,两者的差别是数据的传输方向不同,一个是客户端到服务器,一个是服务器到客户端。RemoteProcedureCalls.Invoke
直接从remoteCallDelegates中拿到Action然后直接执行,至此我们跟踪完了Command生效的调用逻辑
RemoteCall.cs
Invoke
internal static bool Invoke(ushort functionHash, RemoteCallType remoteCallType, NetworkReader reader, NetworkBehaviour component, NetworkConnectionToClient senderConnection = null)
{
// IMPORTANT: we check if the message's componentIndex component is
// actually of the right type. prevents attackers trying
// to invoke remote calls on wrong components.
if (GetInvokerForHash(functionHash, remoteCallType, out Invoker invoker) &&
invoker.componentType.IsInstanceOfType(component))
{
// invoke function on this component
invoker.function(component, reader, senderConnection);
return true;
}
return false;
}
RegisterCommand RegisterRpc
接下来我们来看看这些Action是如何注册到RemoteCall.remoteCallDelegates
中的,通过查看RemoteCalls.cs中的源代码,发现了两个注册ClientRPC,以及Comand的两个方法
// pass full function name to avoid ClassA.Func <-> ClassB.Func collisions
// need to pass componentType to support invoking on GameObjects with
// multiple components of same type with same remote call.
public static void RegisterCommand(Type componentType, string functionFullName, RemoteCallDelegate func, bool requiresAuthority) =>
RegisterDelegate(componentType, functionFullName, RemoteCallType.Command, func, requiresAuthority);
// pass full function name to avoid ClassA.Func <-> ClassB.Func collisions
// need to pass componentType to support invoking on GameObjects with
// multiple components of same type with same remote call.
public static void RegisterRpc(Type componentType, string functionFullName, RemoteCallDelegate func) =>
RegisterDelegate(componentType, functionFullName, RemoteCallType.ClientRpc, func);
RegisterDelegate
internal static ushort RegisterDelegate(Type componentType, string functionFullName, RemoteCallType remoteCallType, RemoteCallDelegate func, bool cmdRequiresAuthority = true)
{
// type+func so Inventory.RpcUse != Equipment.RpcUse
ushort hash = (ushort)(functionFullName.GetStableHashCode() & 0xFFFF);
if (CheckIfDelegateExists(componentType, remoteCallType, func, hash))
return hash;
remoteCallDelegates[hash] = new Invoker
{
callType = remoteCallType,
componentType = componentType,
function = func,
cmdRequiresAuthority = cmdRequiresAuthority
};
return hash;
}
以上就是将指定的远程方法注册到remoteCallDelegates的具体实现,那是哪里调用了 RegisterCommand``RegisterRpc
,通过visual studio查看引用,发现并没有任何地方对该方法进行调用,所有我们以该关键字进行全局搜索看看,顺着关键字源头往上找,找到了ILProcessorHook.cs中Process方法的调用来源。 public class ILPostProcessorHook : ILPostProcessor
该类继承至一个叫ILPostProcessor的接口,程序集属于Unity.CompilationPipeline.Common。看名字大概就能知道是Unity编译过曾中的Hook钩子回调了,没学过Unity靠名字八九不离十吧,注释这里有解释
ILPostProcessorHook
// ILPostProcessor is invoked by Unity.
// we can not tell it to ignore certain assemblies before processing.
// add a 'ignore' define for convenience.
// => WeaverTests/WeaverAssembler need it to avoid Unity running it
public const string IgnoreDefine = "ILPP_IGNORE";
Hook有两个主要的方法,一个是WillProcess
判断当前的compiledAssembly 是否符合拦截标准,不符合的情况下直接跳过,
WillProcess
// from CompilationFinishedHook
const string MirrorRuntimeAssemblyName = "Mirror";
// ILPostProcessor is invoked by Unity.
// we can not tell it to ignore certain assemblies before processing.
// add a 'ignore' define for convenience.
// => WeaverTests/WeaverAssembler need it to avoid Unity running it
public const string IgnoreDefine = "ILPP_IGNORE";
// check if assembly has the 'ignore' define
static bool HasDefine(ICompiledAssembly assembly, string define) =>
assembly.Defines != null &&
assembly.Defines.Contains(define);
public override bool WillProcess(ICompiledAssembly compiledAssembly)
{
// compiledAssembly.References are file paths:
// Library/Bee/artifacts/200b0aE.dag/Mirror.CompilerSymbols.dll
// Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll
// /Applications/Unity/Hub/Editor/2021.2.0b6_apple_silicon/Unity.app/Contents/NetStandard/ref/2.1.0/netstandard.dll
//
// log them to see:
// foreach (string reference in compiledAssembly.References)
// LogDiagnostics($"{compiledAssembly.Name} references {reference}");
bool relevant = compiledAssembly.Name == MirrorRuntimeAssemblyName ||
compiledAssembly.References.Any(filePath => Path.GetFileNameWithoutExtension(filePath) == MirrorRuntimeAssemblyName);
bool ignore = HasDefine(compiledAssembly, IgnoreDefine);
return relevant && !ignore;
}
另外一个方法是Process
主要的处理逻辑就包含在这里
Process
public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
{
//Log.Warning($"Processing {compiledAssembly.Name}");
// load the InMemoryAssembly peData into a MemoryStream
byte[] peData = compiledAssembly.InMemoryAssembly.PeData;
//LogDiagnostics($" peData.Length={peData.Length} bytes");
using (MemoryStream stream = new MemoryStream(peData))
using (ILPostProcessorAssemblyResolver asmResolver = new ILPostProcessorAssemblyResolver(compiledAssembly, Log))
{
// we need to load symbols. otherwise we get:
// "(0,0): error Mono.CecilX.Cil.SymbolsNotFoundException: No symbol found for file: "
using (MemoryStream symbols = new MemoryStream(compiledAssembly.InMemoryAssembly.PdbData))
{
ReaderParameters readerParameters = new ReaderParameters{
SymbolStream = symbols,
ReadWrite = true,
ReadSymbols = true,
AssemblyResolver = asmResolver,
// custom reflection importer to fix System.Private.CoreLib
// not being found in custom assembly resolver above.
ReflectionImporterProvider = new ILPostProcessorReflectionImporterProvider()
};
using (AssemblyDefinition asmDef = AssemblyDefinition.ReadAssembly(stream, readerParameters))
{
// resolving a Mirror.dll type like NetworkServer while
// weaving Mirror.dll does not work. it throws a
// NullReferenceException in WeaverTypes.ctor
// when Resolve() is called on the first Mirror type.
// need to add the AssemblyDefinition itself to use.
asmResolver.SetAssemblyDefinitionForCompiledAssembly(asmDef);
// weave this assembly.
Weaver weaver = new Weaver(Log);
if (weaver.Weave(asmDef, asmResolver, out bool modified))
{
//Log.Warning($"Weaving succeeded for: {compiledAssembly.Name}");
// write if modified
if (modified)
{
// when weaving Mirror.dll with ILPostProcessor,
// Weave() -> WeaverTypes -> resolving the first
// type in Mirror.dll adds a reference to
// Mirror.dll even though we are in Mirror.dll.
// -> this would throw an exception:
// "Mirror references itself" and not compile
// -> need to detect and fix manually here
if (asmDef.MainModule.AssemblyReferences.Any(r => r.Name == asmDef.Name.Name))
{
asmDef.MainModule.AssemblyReferences.Remove(asmDef.MainModule.AssemblyReferences.First(r => r.Name == asmDef.Name.Name));
//Log.Warning($"fixed self referencing Assembly: {asmDef.Name.Name}");
}
MemoryStream peOut = new MemoryStream();
MemoryStream pdbOut = new MemoryStream();
WriterParameters writerParameters = new WriterParameters
{
SymbolWriterProvider = new PortablePdbWriterProvider(),
SymbolStream = pdbOut,
WriteSymbols = true
};
asmDef.Write(peOut, writerParameters);
InMemoryAssembly inMemory = new InMemoryAssembly(peOut.ToArray(), pdbOut.ToArray());
return new ILPostProcessResult(inMemory, Log.Logs);
}
}
// if anything during Weave() fails, we log an error.
// don't need to indicate 'weaving failed' again.
// in fact, this would break tests only expecting certain errors.
//else Log.Error($"Weaving failed for: {compiledAssembly.Name}");
}
}
}
// always return an ILPostProcessResult with Logs.
// otherwise we won't see Logs if weaving failed.
return new ILPostProcessResult(compiledAssembly.InMemoryAssembly, Log.Logs);
}
要读懂上面的代码之前我们先对ILPost方面的相关类熟悉一下,代码丢给C老师,大概能知道是一个处理中间代码的东西,类似于字节码?遇到几个核心类这里进行补充说明
InMemoryAssembly
public class InMemoryAssembly
{
public byte[] PeData { get; set; }
public byte[] PdbData { get; set; }
public InMemoryAssembly(byte[] peData, byte[] pdbData)
{
PeData = peData;
PdbData = pdbData;
}
}
- PeData: 这个属性用来存储程序集(通常是 .dll 或 .exe 文件)的二进制数据。PE (Portable Executable) 是 Windows 可执行文件格式的标准缩写。
- PdbData: 这个属性用来存储调试符号数据,通常是 .pdb (Program Database) 文件中的数据。这些调试符号数据在调试时非常有用,可以映射二进制代码到源代码。
ICompiledAssembly
public interface ICompiledAssembly
{
InMemoryAssembly InMemoryAssembly { get; }
string Name { get; }
string[] References { get; }
string[] Defines { get; }
}
在Unity中我们可以在某一个目录下新建一个程序集,并且设置程序集的名称,以及依赖关系,此时References中即包含对其他程序集的引用关系,Name就是程序集的名称,Defines中包含了条件编译符号
TypeReference
类定义,不必深究,知道是描述描述一个class编译之后的信息就可以
MethodReference
方法定义 不必深究,知道其中包含了一个方法的 方法名称,修饰符,返回类型,泛型,参数,以及方法体指令即可
ParameterDefinition
参数定义 不必深究
ILProcessor
方法体定义 该类型通过md.Body.GetILProcessor();
获取,其中包含了一个方法的所有OPCode指令信息,可以理解为字节码集合,因为IL2CPP是基于编译之后的字节码再优化编译为本地机器码,所以mirror并没有破坏il2cpp的支持。不过对于方法重载需要注意,比如Test(int i),Test(string i),需要写成TestInt,TestString
下面的内容会有些烧脑,对于第一次接触,Mono.CecilX
的同学来说会有些困难,但是只要知道这个东西可以动态编辑字节码,是一个用于读取、编写和修改 .NET 程序集的库就可以了。Process方法主要做的事情就是扫描程序集中的所有类,检查Attribute的使用是否正确,比如SyncVar需要在NetworkBehavior中使用,扫描Client,TargetRPC,Command,Server 修改原有的方法,或生成新增的方法进行替换,达到从方法层面切面的目的。
比如Client和Server这两个注解明确表示在客户端执行,在服务端执行,在编译阶段,会在ILProcessor的头部,可以理解为第一行代码处,新增指令,判断当前NetworkClient.active和NetworkServer.active 是否启用,对于ClientRPC 和Command TargetRpc,(只看了Command其他进行了类推)Mirror会新增一个前缀方法将原有方法进行替换,原有方法会变成,调用SendCommandInternal(string functionFullName, int functionHashCode, NetworkWriter writer, int channelId, bool requiresAuthority = true)
这个是所有的NetworkBehavior类都有的方法,新增的方法以 $"USER_CODE_{MethodName}"这样的名字作为方法名,然后这里有个细节就是在生成之后会将新生成方法的,内部的递归调用修改为调用新方法,在Command方法内递归调用Command方法并不会持续的触发远程调用,如果在Command方法中调用其他方法,这里将会执行原有方法的调用逻辑,以下是替换生成代码的相关逻辑Command部分
// weave this assembly.
Weaver weaver = new Weaver(Log);
整个Mirror的序列化和修改字节码都依赖这个Weaver模块,网上搜发现并灭有其他的信息,这个应该是Mirror作者自己取得一个名字主要得逻辑部分在Weaver.weaver中if (weaver.Weave(asmDef, asmResolver, out bool modified))
Weaver
weaver
// Weave takes an AssemblyDefinition to be compatible with both old and
// new weavers:
// * old takes a filepath, new takes a in-memory byte[]
// * old uses DefaultAssemblyResolver with added dependencies paths,
// new uses ...?
//
// => assembly: the one we are currently weaving (MyGame.dll)
// => resolver: useful in case we need to resolve any of the assembly's
// assembly.MainModule.AssemblyReferences.
// -> we can resolve ANY of them given that the resolver
// works properly (need custom one for ILPostProcessor)
// -> IMPORTANT: .Resolve() takes an AssemblyNameReference.
// those from assembly.MainModule.AssemblyReferences are
// guaranteed to be resolve-able.
// Parsing from a string for Library/.../Mirror.dll
// would not be guaranteed to be resolve-able because
// for ILPostProcessor we can't assume where Mirror.dll
// is etc.
public bool Weave(AssemblyDefinition assembly, IAssemblyResolver resolver, out bool modified)
{
WeavingFailed = false;
modified = false;
try
{
CurrentAssembly = assembly;
// fix "No writer found for ..." error
// https://github.com/vis2k/Mirror/issues/2579
// -> when restarting Unity, weaver would try to weave a DLL
// again
// -> resulting in two GeneratedNetworkCode classes (see ILSpy)
// -> the second one wouldn't have all the writer types setup
if (CurrentAssembly.MainModule.ContainsClass(GeneratedCodeNamespace, GeneratedCodeClassName))
{
//Log.Warning($"Weaver: skipping {CurrentAssembly.Name} because already weaved");
return true;
}
weaverTypes = new WeaverTypes(CurrentAssembly, Log, ref WeavingFailed);
// weaverTypes are needed for CreateGeneratedCodeClass
CreateGeneratedCodeClass();
// WeaverList depends on WeaverTypes setup because it uses Import
syncVarAccessLists = new SyncVarAccessLists();
// initialize readers & writers with this assembly.
// we need to do this in every Process() call.
// otherwise we would get
// "System.ArgumentException: Member ... is declared in another module and needs to be imported"
// errors when still using the previous module's reader/writer funcs.
writers = new Writers(CurrentAssembly, weaverTypes, GeneratedCodeClass, Log);
readers = new Readers(CurrentAssembly, weaverTypes, GeneratedCodeClass, Log);
Stopwatch rwstopwatch = Stopwatch.StartNew();
// Need to track modified from ReaderWriterProcessor too because it could find custom read/write functions or create functions for NetworkMessages
modified = ReaderWriterProcessor.Process(CurrentAssembly, resolver, Log, writers, readers, ref WeavingFailed);
rwstopwatch.Stop();
Console.WriteLine($"Find all reader and writers took {rwstopwatch.ElapsedMilliseconds} milliseconds");
ModuleDefinition moduleDefinition = CurrentAssembly.MainModule;
Console.WriteLine($"Script Module: {moduleDefinition.Name}");
modified |= WeaveModule(moduleDefinition);
if (WeavingFailed)
{
return false;
}
if (modified)
{
SyncVarAttributeAccessReplacer.Process(Log, moduleDefinition, syncVarAccessLists);
// add class that holds read/write functions
moduleDefinition.Types.Add(GeneratedCodeClass);
ReaderWriterProcessor.InitializeReaderAndWriters(CurrentAssembly, weaverTypes, writers, readers, GeneratedCodeClass);
// DO NOT WRITE here.
// CompilationFinishedHook writes to the file.
// ILPostProcessor writes to in-memory assembly.
// it depends on the caller.
//CurrentAssembly.Write(new WriterParameters{ WriteSymbols = true });
}
// if weaving succeeded, switch on the Weaver Fuse in Mirror.dll
if (CurrentAssembly.Name.Name == MirrorAssemblyName)
{
ToggleWeaverFuse();
}
return true;
}
catch (Exception e)
{
Log.Error($"Exception :{e}");
WeavingFailed = true;
return false;
}
}
WeaverTypes
weaverTypes = new WeaverTypes(CurrentAssembly, Log, ref WeavingFailed);
WeaverTypes 包含了可能用得methodReference,它得主要目的是,将后续修改生成代码中需要引用调用的method全部收集起来,使用使用类成员对象的形式进行引用,因为ILPostProcessor是多线程环境,所以该类是非静态实例,主要使用到了 Mono.CecilX
的两个方法,一个找类一个找方法,非静态构造函数,和静态构造函数也是方法,静态构造函数(.cctor)在类加载阶段执行用于初始化静态类对象,非静态构造方法(.ctor)在,new实例化的时候执行,用于初始化类成员对象。
TypeReference ArraySegmentType = Import(typeof(ArraySegment<>));
ArraySegmentConstructorReference = Resolvers.ResolveMethod(ArraySegmentType, assembly, Log, ".ctor", ref WeavingFailed);
TypeReference ActionType = Import(typeof(Action<,>));
ActionT_T = Resolvers.ResolveMethod(ActionType, assembly, Log, ".ctor", ref WeavingFailed);
public TypeReference ImportReference(Type type, IGenericParameterProvider context)
{
Mixin.CheckType(type);
CheckContext(context, this);
return ReflectionImporter.ImportReference(type, context);
}
public MethodReference ImportReference(MethodReference method, IGenericParameterProvider context)
{
Mixin.CheckMethod(method);
if (method.Module == this)
{
return method;
}
CheckContext(context, this);
return MetadataImporter.ImportReference(method, context);
}
下面我们会看到在编织环境,会利用OPCodes.Call的指令调用= Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "SendCommandInternal", ref WeavingFailed);
这部分的逻辑,就是通过weaverTypes.sendCommandInternal
的形式拿到的
// weaverTypes are needed for CreateGeneratedCodeClass
CreateGeneratedCodeClass();
这一行实在新建一个名字叫做Mirror.GeneratedNetworkCode
的类,动态构建的,作用暂时不管,
// WeaverList depends on WeaverTypes setup because it uses Import
syncVarAccessLists = new SyncVarAccessLists();
syncVarAccessLists一个临时存储SyncVar 生成的Setter和Getter方法,以及统计对应的class中SyncVar的数量,下面是对应的源代码
// setter functions that replace [SyncVar] member variable references. dict<field, replacement>
public Dictionary<FieldDefinition, MethodDefinition> replacementSetterProperties =
new Dictionary<FieldDefinition, MethodDefinition>();
// getter functions that replace [SyncVar] member variable references. dict<field, replacement>
public Dictionary<FieldDefinition, MethodDefinition> replacementGetterProperties =
new Dictionary<FieldDefinition, MethodDefinition>();
// amount of SyncVars per class. dict<className, amount>
// necessary for SyncVar dirty bits, where inheriting classes start
// their dirty bits at base class SyncVar amount.
public Dictionary<string, int> numSyncVars = new Dictionary<string, int>();
// initialize readers & writers with this assembly.
// we need to do this in every Process() call.
// otherwise we would get
// "System.ArgumentException: Member ... is declared in another module and needs to be imported"
// errors when still using the previous module's reader/writer funcs.
writers = new Writers(CurrentAssembly, weaverTypes, GeneratedCodeClass, Log);
readers = new Readers(CurrentAssembly, weaverTypes, GeneratedCodeClass, Log);
Writers
Readers
提供了各种类型的序列化方法,包含MessageStruct的enum,float,int等基础类型,Array,NetworkBehavior,主要管理的是对象序列化的能力,同理Readers
主要管理的是对象反序列化的能力,这中间很好理解,比如序列化一个struct,我就可以根据struct的fields,判断类型在通过Type从Writers中获取Writer方法,直到可以直接写入到byte[]中为止,比如float,int这些值类型,直接通过writerBytes进行序列化,基础类型的序列化能力由NetworkWriter提供,同理Readers和NetworkReader,另外Writers中会把生成的序列化静态函数添加到 Mirror.GeneratedNetworkCode
,拿一些需要添加是在扫描程序集的过程中懒加载的,遇到了才会调用Wirters和Readers初始化。
modified = ReaderWriterProcessor.Process(CurrentAssembly, resolver, Log, writers, readers, ref WeavingFailed)
这个方法就是核心的程序集扫描处理进程了,
ReaderWriterProcessor
Process
public static bool Process(AssemblyDefinition CurrentAssembly, IAssemblyResolver resolver, Logger Log, Writers writers, Readers readers, ref bool WeavingFailed)
{
// find NetworkReader/Writer extensions from Mirror.dll first.
// and NetworkMessage custom writer/reader extensions.
// NOTE: do not include this result in our 'modified' return value,
// otherwise Unity crashes when running tests
ProcessMirrorAssemblyClasses(CurrentAssembly, resolver, Log, writers, readers, ref WeavingFailed);
// find readers/writers in the assembly we are in right now.
return ProcessAssemblyClasses(CurrentAssembly, CurrentAssembly, writers, readers, ref WeavingFailed);
}
先处理Mirror自身的,再处理用户的,逻辑基本上是一致的,在ProcessMirrorAssemblyClasses中又调用了ProcessAssemblyClasses,只是把resolver替换成了Mirror包的自己的
ProcessAssemblyClasses
static bool ProcessAssemblyClasses(AssemblyDefinition CurrentAssembly, AssemblyDefinition assembly, Writers writers, Readers readers, ref bool WeavingFailed)
{
bool modified = false;
foreach (TypeDefinition klass in assembly.MainModule.Types)
{
// extension methods only live in static classes
// static classes are represented as sealed and abstract
if (klass.IsAbstract && klass.IsSealed)
{
// if assembly has any declared writers then it is "modified"
modified |= LoadDeclaredWriters(CurrentAssembly, klass, writers);
modified |= LoadDeclaredReaders(CurrentAssembly, klass, readers);
}
}
foreach (TypeDefinition klass in assembly.MainModule.Types)
{
// if assembly has any network message then it is modified
modified |= LoadMessageReadWriter(CurrentAssembly.MainModule, writers, readers, klass, ref WeavingFailed);
}
return modified;
}
可以看到assembly被用于提供所有的types,即类
if (klass.IsAbstract && klass.IsSealed)
{
// if assembly has any declared writers then it is "modified"
modified |= LoadDeclaredWriters(CurrentAssembly, klass, writers);
modified |= LoadDeclaredReaders(CurrentAssembly, klass, readers);
}
LoadDeclaredWriters
static bool LoadDeclaredWriters(AssemblyDefinition currentAssembly, TypeDefinition klass, Writers writers)
{
// register all the writers in this class. Skip the ones with wrong signature
bool modified = false;
foreach (MethodDefinition method in klass.Methods)
{
if (method.Parameters.Count != 2)
continue;
if (!method.Parameters[0].ParameterType.Is<NetworkWriter>())
continue;
if (!method.ReturnType.Is(typeof(void)))
continue;
if (!method.HasCustomAttribute<System.Runtime.CompilerServices.ExtensionAttribute>())
continue;
if (method.HasGenericParameters)
continue;
TypeReference dataType = method.Parameters[1].ParameterType;
writers.Register(dataType, currentAssembly.MainModule.ImportReference(method));
modified = true;
}
return modified;
}
LoadDeclaredReaders
static bool LoadDeclaredReaders(AssemblyDefinition currentAssembly, TypeDefinition klass, Readers readers)
{
// register all the reader in this class. Skip the ones with wrong signature
bool modified = false;
foreach (MethodDefinition method in klass.Methods)
{
if (method.Parameters.Count != 1)
continue;
if (!method.Parameters[0].ParameterType.Is<NetworkReader>())
continue;
if (method.ReturnType.Is(typeof(void)))
continue;
if (!method.HasCustomAttribute<System.Runtime.CompilerServices.ExtensionAttribute>())
continue;
if (method.HasGenericParameters)
continue;
readers.Register(method.ReturnType, currentAssembly.MainModule.ImportReference(method));
modified = true;
}
return modified;
}
这段代码的目的是从所有的静态类中扫描扩展方法,在C#中我们可以对某一个类型的方法进行拓展,比如之前提到的GetMethodStableHashCode
public static ushort GetStableHashCode16(this string text)
{
// deterministic hash
int hash = GetStableHashCode(text);
// Gets the 32bit fnv1a hash
// To get it down to 16bit but still reduce hash collisions we cant just cast it to ushort
// Instead we take the highest 16bits of the 32bit hash and fold them with xor into the lower 16bits
// This will create a more uniform 16bit hash, the method is described in:
// http://www.isthe.com/chongo/tech/comp/fnv/ in section "Changing the FNV hash size - xor-folding"
return (ushort)((hash >> 16) ^ hash);
}
在C#中提供了一种机制允许扩展指定类型的方法,扩展方法必须定义在静态类中,且第一个参数必须使用 this
关键字指定要扩展的类型。编译器在编译扩展方法时会自动为这些方法添加 ExtensionAttribute
属性。所以Mirror中我们可以很容易对NetworkWriter,NetworkRead中新增自定义的序列化方法,Mirror把对NetworkWriter的扩展方法同一放置到了 在Mirror.NetworkWriterExtensions.cs
这个静态类中,我们找几个例子看看
NetworkWriterExtensions
WriteGameObject
public static void WriteGameObject(this NetworkWriter writer, GameObject value)
{
if (value == null)
{
writer.WriteUInt(0);
return;
}
// warn if the GameObject doesn't have a NetworkIdentity,
if (!value.TryGetComponent(out NetworkIdentity identity))
Debug.LogWarning($"Attempted to sync a GameObject ({value}) which isn't networked. GameObject without a NetworkIdentity component can't be synced.");
// serialize the correct amount of data in any case to make sure
// that the other end can read the expected amount of data too.
writer.WriteNetworkIdentity(identity);
}
序列化一个GameObject,实际上序列化组件identity
WriteNetworkIdentity
public static void WriteNetworkIdentity(this NetworkWriter writer, NetworkIdentity value)
{
if (value == null)
{
writer.WriteUInt(0);
return;
}
// users might try to use unspawned / prefab GameObjects in
// rpcs/cmds/syncvars/messages. they would be null on the other
// end, and it might not be obvious why. let's make it obvious.
// https://github.com/vis2k/Mirror/issues/2060
//
// => warning (instead of exception) because we also use a warning
// if a GameObject doesn't have a NetworkIdentity component etc.
if (value.netId == 0)
Debug.LogWarning($"Attempted to serialize unspawned GameObject: {value.name}. Prefabs and unspawned GameObjects would always be null on the other side. Please spawn it before using it in [SyncVar]s/Rpcs/Cmds/NetworkMessages etc.");
writer.WriteUInt(value.netId);
}
而identity的序列化实际上是写入netId,如何此看来,我们完全可以在Command ClientRPC中直接传递对应的GameObj,只要GameObj上刮了Identity组件
WriteNetworkBehaviour
public static void WriteNetworkBehaviour(this NetworkWriter writer, NetworkBehaviour value)
{
if (value == null)
{
writer.WriteUInt(0);
return;
}
// users might try to use unspawned / prefab NetworkBehaviours in
// rpcs/cmds/syncvars/messages. they would be null on the other
// end, and it might not be obvious why. let's make it obvious.
// https://github.com/vis2k/Mirror/issues/2060
// and more recently https://github.com/MirrorNetworking/Mirror/issues/3399
//
// => warning (instead of exception) because we also use a warning
// when writing an unspawned NetworkIdentity
if (value.netId == 0)
{
Debug.LogWarning($"Attempted to serialize unspawned NetworkBehaviour: of type {value.GetType()} on GameObject {value.name}. Prefabs and unspawned GameObjects would always be null on the other side. Please spawn it before using it in [SyncVar]s/Rpcs/Cmds/NetworkMessages etc.");
writer.WriteUInt(0);
return;
}
writer.WriteUInt(value.netId);
writer.WriteByte(value.ComponentIndex);
}
只要是继承或实例化NetworkBehaviour的序列化,序列化方式为找到identity拿到netId,并将当前NetworkBehavior的ComponentIndex一并写入用于定位
比较有趣的是Mirror竟然可以序列化Sprite
WriteSprite
public static void WriteSprite(this NetworkWriter writer, Sprite sprite)
{
// support 'null' textures for [SyncVar]s etc.
// https://github.com/vis2k/Mirror/issues/3144
// simply send a 'null' for texture content.
if (sprite == null)
{
writer.WriteTexture2D(null);
return;
}
writer.WriteTexture2D(sprite.texture);
writer.WriteRect(sprite.rect);
writer.WriteVector2(sprite.pivot);
}
WriteTexture2D
public static void WriteTexture2D(this NetworkWriter writer, Texture2D texture2D)
{
// TODO allocation protection when sending textures to server.
// currently can allocate 32k x 32k x 4 byte = 3.8 GB
// support 'null' textures for [SyncVar]s etc.
// https://github.com/vis2k/Mirror/issues/3144
// simply send -1 for width.
if (texture2D == null)
{
writer.WriteShort(-1);
return;
}
// check if within max size, otherwise Reader can't read it.
int totalSize = texture2D.width * texture2D.height;
if (totalSize > NetworkReader.AllocationLimit)
throw new IndexOutOfRangeException($"NetworkWriter.WriteTexture2D - Texture2D total size (width*height) too big: {totalSize}. Limit: {NetworkReader.AllocationLimit}");
// write dimensions first so reader can create the texture with size
// 32k x 32k short is more than enough
writer.WriteShort((short)texture2D.width);
writer.WriteShort((short)texture2D.height);
writer.WriteArray(texture2D.GetPixels32());
}
可以看到他直接texture2D中的所有像素都序列化了,这是不是浪费带宽啊,毕竟走服务器不如直接传字符串然后再从CDN上下载合适。确实有点让我经验了,同理肯定也有叫 Mirror.NetworkReaderExtensions
不在过多提及
ReaderWriterProcessor
我们回到ProcessAssemblyClasses
方法
foreach (TypeDefinition klass in assembly.MainModule.Types)
{
// if assembly has any network message then it is modified
modified |= LoadMessageReadWriter(CurrentAssembly.MainModule, writers, readers, klass, ref WeavingFailed);
}
方式估计也差不多,之前有提到过Mirror有很多NetworkMessage,用于固定的生命周期事件消息,就是扫描继承自NetworkMessage的所有结构体,然后将对应的读写序列化方法放置到Readers,Writers中
Weaver
modified |= WeaveModule(moduleDefinition);
看名字查出来是干嘛的,看下源码
bool WeaveModule(ModuleDefinition moduleDefinition)
{
bool modified = false;
Stopwatch watch = Stopwatch.StartNew();
watch.Start();
// ModuleDefinition.Types only finds top level types.
// GetAllTypes recursively finds all nested types as well.
// fixes nested types not being weaved, for example:
// class Parent { // ModuleDefinition.Types finds this
// class Child { // .Types.NestedTypes finds this
// class GrandChild {} // only GetAllTypes finds this too
// }
// }
// note this is not about inheritance, only about type definitions.
// see test: NetworkBehaviourTests.DeeplyNested()
foreach (TypeDefinition td in moduleDefinition.GetAllTypes())
{
if (td.IsClass && td.BaseType.CanBeResolved())
{
modified |= WeaveNetworkBehavior(td);
modified |= ServerClientAttributeProcessor.Process(weaverTypes, Log, td, ref WeavingFailed);
}
}
watch.Stop();
Console.WriteLine($"Weave behaviours and messages took {watch.ElapsedMilliseconds} milliseconds");
return modified;
}
哦,好吧,对当前的程序集遍历,然后处理NetworkBehavior及其派生类,注释中说了,为什么要使用GetAllTypes,核心需要关注的就是
WeaveNetworkBehavior
ServerClientAttributeProcessor.Process
`
WeaveNetworkBehavior
bool WeaveNetworkBehavior(TypeDefinition td)
{
if (!td.IsClass)
return false;
if (!td.IsDerivedFrom<NetworkBehaviour>())
{
if (td.IsDerivedFrom<UnityEngine.MonoBehaviour>())
MonoBehaviourProcessor.Process(Log, td, ref WeavingFailed);
return false;
}
// process this and base classes from parent to child order
List<TypeDefinition> behaviourClasses = new List<TypeDefinition>();
TypeDefinition parent = td;
while (parent != null)
{
if (parent.Is<NetworkBehaviour>())
{
break;
}
try
{
behaviourClasses.Insert(0, parent);
parent = parent.BaseType.Resolve();
}
catch (AssemblyResolutionException)
{
// this can happen for plugins.
//Console.WriteLine("AssemblyResolutionException: "+ ex.ToString());
break;
}
}
bool modified = false;
foreach (TypeDefinition behaviour in behaviourClasses)
{
modified |= new NetworkBehaviourProcessor(CurrentAssembly, weaverTypes, syncVarAccessLists, writers, readers, Log, behaviour).Process(ref WeavingFailed);
}
return modified;
}
如果当前Type是从NetworkBehaviour派生的,则往上找父类直到NetworkBehaviour为止,然后优先处理parent再处理子类的顺序进行,这里回直接新建一个NetworkBehaviourProcessor的程序进行处理,同时NetworkBehaviourProcessor内会判断Type是否已经被标记过,标记其被处理过的方式是,直接再当前的Type新增一个Weaved
方法,如果有则表示被处理过了直接返回。
public bool Process(ref bool WeavingFailed)
{
// only process once
if (WasProcessed(netBehaviourSubclass))
{
return false;
}
MarkAsProcessed(netBehaviourSubclass);
// deconstruct tuple and set fields
(syncVars, syncVarNetIds, syncVarHookDelegates) = syncVarAttributeProcessor.ProcessSyncVars(netBehaviourSubclass, ref WeavingFailed);
syncObjects = SyncObjectProcessor.FindSyncObjectsFields(writers, readers, Log, netBehaviourSubclass, ref WeavingFailed);
ProcessMethods(ref WeavingFailed);
if (WeavingFailed)
{
// originally Process returned true in every case, except if already processed.
// maybe return false here in the future.
return true;
}
// inject initializations into static & instance constructor
InjectIntoStaticConstructor(ref WeavingFailed);
InjectIntoInstanceConstructor(ref WeavingFailed);
GenerateSerialization(ref WeavingFailed);
if (WeavingFailed)
{
// originally Process returned true in every case, except if already processed.
// maybe return false here in the future.
return true;
}
GenerateDeSerialization(ref WeavingFailed);
return true;
}
SyncVarAttributeProcessor
ProcessSyncVars
syncVarAttributeProcessor.ProcessSyncVars
针对syncVars
进行处理,主要流程是遍历所有的FieldDefinition
,判断字段上是否存在SyncVarAttribute
并做规则检测比如必须是非静态修饰符,不得存在泛型,如果SyncObject(如SyncList)则不需要附件[SyncVar]。然后通过ProcessSyncVar
方法,根据字段的类型生成对应的Setter Getter方法,类型判断有两个细节,如果是NetworkBehavior或者其派生类,则生成一个$“__{fd.Name}NetId"格式,类型为NetworkBehaviourSyncVar
的字段,并将对应的Setter Getter逻辑定向到该字段上。NetworkBehaviourSyncVar
自己持有NetId以及ComponentsIndex,如果是NetworkIdentity则新增 $”__{fd.Name}NetId",为行为uint的字段,也将Setter Getter的逻辑定向到该字段上,
public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary<FieldDefinition, FieldDefinition> syncVarNetIds, Dictionary<FieldDefinition, (FieldDefinition hookDelegateField, MethodDefinition hookMethod)> syncVarHookDelegates, long dirtyBit, ref bool WeavingFailed)
{
string originalName = fd.Name;
// GameObject/NetworkIdentity SyncVars have a new field for netId
FieldDefinition netIdField = null;
// NetworkBehaviour has different field type than other NetworkIdentityFields
// handle both NetworkBehaviour and inheritors.
// fixes: https://github.com/MirrorNetworking/Mirror/issues/2939
if (fd.FieldType.IsDerivedFrom<NetworkBehaviour>() || fd.FieldType.Is<NetworkBehaviour>())
{
netIdField = new FieldDefinition($"___{fd.Name}NetId",
FieldAttributes.Family, // needs to be protected for generic classes, otherwise access isn't allowed
weaverTypes.Import<NetworkBehaviourSyncVar>());
netIdField.DeclaringType = td;
syncVarNetIds[fd] = netIdField;
}
else if (fd.FieldType.IsNetworkIdentityField())
{
netIdField = new FieldDefinition($"___{fd.Name}NetId",
FieldAttributes.Family, // needs to be protected for generic classes, otherwise access isn't allowed
weaverTypes.Import<uint>());
netIdField.DeclaringType = td;
syncVarNetIds[fd] = netIdField;
}
MethodDefinition get = GenerateSyncVarGetter(fd, originalName, netIdField);
MethodDefinition set = GenerateSyncVarSetter(td, fd, originalName, dirtyBit, netIdField, syncVarHookDelegates, ref WeavingFailed);
//NOTE: is property even needed? Could just use a setter function?
//create the property
PropertyDefinition propertyDefinition = new PropertyDefinition($"Network{originalName}", PropertyAttributes.None, fd.FieldType)
{
GetMethod = get,
SetMethod = set
};
//add the methods and property to the type.
td.Methods.Add(get);
td.Methods.Add(set);
td.Properties.Add(propertyDefinition);
syncVarAccessLists.replacementSetterProperties[fd] = set;
// replace getter field if GameObject/NetworkIdentity so it uses
// netId instead
// -> only for GameObjects, otherwise an int syncvar's getter would
// end up in recursion.
if (fd.FieldType.IsNetworkIdentityField())
{
syncVarAccessLists.replacementGetterProperties[fd] = get;
}
}
代理Getter,Setter的生成逻辑在 GenerateSyncVarGetter,GenerateSyncVarSetter,原理大概类似,只选择其中一个进行了解
GenerateSyncVarGetter
public MethodDefinition GenerateSyncVarGetter(FieldDefinition fd, string originalName, FieldDefinition netFieldId)
{
//Create the get method
MethodDefinition get = new MethodDefinition(
$"get_Network{originalName}", MethodAttributes.Public |
MethodAttributes.SpecialName |
MethodAttributes.HideBySig,
fd.FieldType);
ILProcessor worker = get.Body.GetILProcessor();
FieldReference fr;
if (fd.DeclaringType.HasGenericParameters)
{
fr = fd.MakeHostInstanceGeneric();
}
else
{
fr = fd;
}
FieldReference netIdFieldReference = null;
if (netFieldId != null)
{
if (netFieldId.DeclaringType.HasGenericParameters)
{
netIdFieldReference = netFieldId.MakeHostInstanceGeneric();
}
else
{
netIdFieldReference = netFieldId;
}
}
// [SyncVar] GameObject?
if (fd.FieldType.Is<UnityEngine.GameObject>())
{
// return this.GetSyncVarGameObject(ref field, uint netId);
// this.
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldfld, netIdFieldReference);
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldflda, fr);
worker.Emit(OpCodes.Call, weaverTypes.getSyncVarGameObjectReference);
worker.Emit(OpCodes.Ret);
}
// [SyncVar] NetworkIdentity?
else if (fd.FieldType.Is<NetworkIdentity>())
{
// return this.GetSyncVarNetworkIdentity(ref field, uint netId);
// this.
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldfld, netIdFieldReference);
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldflda, fr);
worker.Emit(OpCodes.Call, weaverTypes.getSyncVarNetworkIdentityReference);
worker.Emit(OpCodes.Ret);
}
// handle both NetworkBehaviour and inheritors.
// fixes: https://github.com/MirrorNetworking/Mirror/issues/2939
else if (fd.FieldType.IsDerivedFrom<NetworkBehaviour>() || fd.FieldType.Is<NetworkBehaviour>())
{
// return this.GetSyncVarNetworkBehaviour<T>(ref field, uint netId);
// this.
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldfld, netIdFieldReference);
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldflda, fr);
MethodReference getFunc = weaverTypes.getSyncVarNetworkBehaviourReference.MakeGeneric(assembly.MainModule, fd.FieldType);
worker.Emit(OpCodes.Call, getFunc);
worker.Emit(OpCodes.Ret);
}
// [SyncVar] int, string, etc.
else
{
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldfld, fr);
worker.Emit(OpCodes.Ret);
}
get.Body.Variables.Add(new VariableDefinition(fd.FieldType));
get.Body.InitLocals = true;
get.SemanticsAttributes = MethodSemanticsAttributes.Getter;
return get;
}
新增的Getter代理,名字格式为$“get_Network{originalName}”,然后对fd的类型分别进行处理
类型 | 处理方式 |
---|---|
基础类型 | 直接返回 |
NetworkIdentity | getSyncVarNetworkIdentityReference |
NetworkBehaviour及其派生子类 | getSyncVarNetworkBehaviourReference(MakeGeneric填写调用函数时的泛型) |
GameObject | getSyncVarGameObjectReference |
getSyncVarNetworkIdentityReference,getSyncVarNetworkBehaviourReference,getSyncVarGameObjectReference
三个方法的定义都存在于NetworkBehaviour
- getSyncVarGameObjectReference依靠的还是NetId,在Setter方法中取出identity然后Getter的时候通过netId在NetworkClient.spawned查找,然后拿到gameobject,所以能进行Sync的GameObject一定是有Identity的才可以
- getSyncVarNetworkBehaviourReference 依靠新增字段类型保存的NetworkBehaviourSyncVar netId和ComponentIndex来定位
- getSyncVarNetworkIdentityReference 依靠新增字段uint保存的netId来定位
以下时相关的代码具体实现
GetSyncVarNetworkBehaviour
GetSyncVarNetworkIdentity
GetSyncVarGameObject
protected T GetSyncVarNetworkBehaviour<T>(NetworkBehaviourSyncVar syncNetBehaviour, ref T behaviourField) where T : NetworkBehaviour
{
// server always uses the field
// if neither, fallback to original field
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3447
if (isServer || !isClient)
{
return behaviourField;
}
// client always looks up based on netId because objects might get in and out of range
// over and over again, which shouldn't null them forever
if (!NetworkClient.spawned.TryGetValue(syncNetBehaviour.netId, out NetworkIdentity identity))
{
return null;
}
behaviourField = identity.NetworkBehaviours[syncNetBehaviour.componentIndex] as T;
return behaviourField;
}
protected NetworkIdentity GetSyncVarNetworkIdentity(uint netId, ref NetworkIdentity identityField)
{
// server always uses the field
// if neither, fallback to original field
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3447
if (isServer || !isClient)
{
return identityField;
}
// client always looks up based on netId because objects might get in and out of range
// over and over again, which shouldn't null them forever
NetworkClient.spawned.TryGetValue(netId, out identityField);
return identityField;
}
protected GameObject GetSyncVarGameObject(uint netId, ref GameObject gameObjectField)
{
// server always uses the field
// if neither, fallback to original field
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3447
if (isServer || !isClient)
{
return gameObjectField;
}
// client always looks up based on netId because objects might get in and out of range
// over and over again, which shouldn't null them forever
if (NetworkClient.spawned.TryGetValue(netId, out NetworkIdentity identity) && identity != null)
return gameObjectField = identity.gameObject;
return null;
}
将生成的Setter Getter Method 放置到syncVarAccessLists的replacementSetterProperties和replacementGetterProperties,ProcessSyncVar就算结束了,另外这里生成了一个Properly名称格式为$"Network{fd.Name}"放到了当前的Type中,对了GenerateSyncVarSetter中还生成了[SynVar(hook=)]的hook钩子,利用的Action。
syncObjects = SyncObjectProcessor.FindSyncObjectsFields(writers, readers, Log, netBehaviourSubclass, ref WeavingFailed);
是为了处理SyncObject,他的派生类有SyncDictionary,SyncHashSet,SyncDicitonary,SyncList,SyncSet,SyncSortedSet,逻辑基本差不多,SyncObject有自己的序列化反序列化接口,允许自行实现对数据的差量全量更新。
ProcessMethods
ProcessMethods(ref WeavingFailed);
正式开始对ClientRPC,Command,TargetRPC注解方法进行处理
void ProcessMethods(ref bool WeavingFailed)
{
HashSet<string> names = new HashSet<string>();
// copy the list of methods because we will be adding methods in the loop
List<MethodDefinition> methods = new List<MethodDefinition>(netBehaviourSubclass.Methods);
// find command and RPC functions
foreach (MethodDefinition md in methods)
{
foreach (CustomAttribute ca in md.CustomAttributes)
{
if (ca.AttributeType.Is<CommandAttribute>())
{
ProcessCommand(names, md, ca, ref WeavingFailed);
break;
}
if (ca.AttributeType.Is<TargetRpcAttribute>())
{
ProcessTargetRpc(names, md, ca, ref WeavingFailed);
break;
}
if (ca.AttributeType.Is<ClientRpcAttribute>())
{
ProcessClientRpc(names, md, ca, ref WeavingFailed);
break;
}
}
}
}
CommandProcessor.csd
ProcessCommandCall
public static MethodDefinition ProcessCommandCall(WeaverTypes weaverTypes, Writers writers, Logger Log, TypeDefinition td, MethodDefinition md, CustomAttribute commandAttr, ref bool WeavingFailed)
{
MethodDefinition cmd = MethodProcessor.SubstituteMethod(Log, td, md, ref WeavingFailed);
ILProcessor worker = md.Body.GetILProcessor();
NetworkBehaviourProcessor.WriteSetupLocals(worker, weaverTypes);
// NetworkWriter writer = new NetworkWriter();
NetworkBehaviourProcessor.WriteGetWriter(worker, weaverTypes);
// write all the arguments that the user passed to the Cmd call
if (!NetworkBehaviourProcessor.WriteArguments(worker, writers, Log, md, RemoteCallType.Command, ref WeavingFailed))
return null;
int channel = commandAttr.GetField("channel", 0);
bool requiresAuthority = commandAttr.GetField("requiresAuthority", true);
// invoke internal send and return
// load 'base.' to call the SendCommand function with
worker.Emit(OpCodes.Ldarg_0);
// pass full function name to avoid ClassA.Func <-> ClassB.Func collisions
worker.Emit(OpCodes.Ldstr, md.FullName);
// pass the function hash so we don't have to compute it at runtime
// otherwise each GetStableHash call requires O(N) complexity.
// noticeable for long function names:
// https://github.com/MirrorNetworking/Mirror/issues/3375
worker.Emit(OpCodes.Ldc_I4, md.FullName.GetStableHashCode());
// writer
worker.Emit(OpCodes.Ldloc_0);
worker.Emit(OpCodes.Ldc_I4, channel);
// requiresAuthority ? 1 : 0
worker.Emit(requiresAuthority ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0);
worker.Emit(OpCodes.Call, weaverTypes.sendCommandInternal);
NetworkBehaviourProcessor.WriteReturnWriter(worker, weaverTypes);
worker.Emit(OpCodes.Ret);
return cmd;
}
MethodProcessor
SubstituteMethod
以固定格式命名然后copy目标method元数据到新的方法中,这一步已经将新增的方法添加到了td中,即类中,且执行之后md的Body是空的,灭有任何代码逻辑
// For a function like
// [ClientRpc] void RpcTest(int value),
// Weaver substitutes the method and moves the code to a new method:
// UserCode_RpcTest(int value) <- contains original code
// RpcTest(int value) <- serializes parameters, sends the message
//
// Note that all the calls to the method remain untouched.
// FixRemoteCallToBaseMethod replaces them afterwards.
public static MethodDefinition SubstituteMethod(Logger Log, TypeDefinition td, MethodDefinition md, ref bool WeavingFailed)
{
string newName = Weaver.GenerateMethodName(RpcPrefix, md);
MethodDefinition cmd = new MethodDefinition(newName, md.Attributes, md.ReturnType);
// force the substitute method to be protected.
// -> public would show in the Inspector for UnityEvents as
// User_CmdUsePotion() etc. but the user shouldn't use those.
// -> private would not allow inheriting classes to call it, see
// OverrideVirtualWithBaseCallsBothVirtualAndBase test.
// -> IL has no concept of 'protected', it's called IsFamily there.
cmd.IsPublic = false;
cmd.IsFamily = true;
// add parameters
foreach (ParameterDefinition pd in md.Parameters)
{
cmd.Parameters.Add(new ParameterDefinition(pd.Name, ParameterAttributes.None, pd.ParameterType));
}
// swap bodies
(cmd.Body, md.Body) = (md.Body, cmd.Body);
// Move over all the debugging information
foreach (SequencePoint sequencePoint in md.DebugInformation.SequencePoints)
cmd.DebugInformation.SequencePoints.Add(sequencePoint);
md.DebugInformation.SequencePoints.Clear();
foreach (CustomDebugInformation customInfo in md.CustomDebugInformations)
cmd.CustomDebugInformations.Add(customInfo);
md.CustomDebugInformations.Clear();
(md.DebugInformation.Scope, cmd.DebugInformation.Scope) = (cmd.DebugInformation.Scope, md.DebugInformation.Scope);
td.Methods.Add(cmd);
FixRemoteCallToBaseMethod(Log, td, cmd, ref WeavingFailed);
return cmd;
}
Weaver.cs
GenerateMethodName
附带前缀的方法名称生成,使用__作为间隔符号,附加一个initialPrefix前缀,不做过多说明
// remote actions now support overloads,
// -> but IL2CPP doesnt like it when two generated methods
// -> have the same signature,
// -> so, append the signature to the generated method name,
// -> to create a unique name
// Example:
// RpcTeleport(Vector3 position) -> InvokeUserCode_RpcTeleport__Vector3()
// RpcTeleport(Vector3 position, Quaternion rotation) -> InvokeUserCode_RpcTeleport__Vector3Quaternion()
// fixes https://github.com/vis2k/Mirror/issues/3060
public static string GenerateMethodName(string initialPrefix, MethodDefinition md)
{
initialPrefix += md.Name;
for (int i = 0; i < md.Parameters.Count; ++i)
{
// with __ so it's more obvious that this is the parameter suffix.
// otherwise RpcTest(int) => RpcTestInt(int) which is not obvious.
initialPrefix += $"__{md.Parameters[i].ParameterType.Name}";
}
return initialPrefix;
}
FixRemoteCallToBaseMethod
注释说的也很清楚,一句话替换递归回调
// For a function like
// [ClientRpc] void RpcTest(int value),
// Weaver substitutes the method and moves the code to a new method:
// UserCode_RpcTest(int value) <- contains original code
// RpcTest(int value) <- serializes parameters, sends the message
//
// FixRemoteCallToBaseMethod replaces all calls to
// RpcTest(value)
// with
// UserCode_RpcTest(value)
public static void FixRemoteCallToBaseMethod(Logger Log, TypeDefinition type, MethodDefinition method, ref bool WeavingFailed)
{
string callName = method.Name;
// Cmd/rpc start with Weaver.RpcPrefix
// e.g. CallCmdDoSomething
if (!callName.StartsWith(RpcPrefix))
return;
// e.g. CmdDoSomething
string baseRemoteCallName = method.Name.Substring(RpcPrefix.Length);
foreach (Instruction instruction in method.Body.Instructions)
{
// is this instruction a Call to a method?
// if yes, output the method so we can check it.
if (IsCallToMethod(instruction, out MethodDefinition calledMethod))
{
// when considering if 'calledMethod' is a call to 'method',
// we originally compared .Name.
//
// to fix IL2CPP build bugs with overloaded Rpcs, we need to
// generated rpc names like
// RpcTest(string value) => RpcTestString(strig value)
// RpcTest(int value) => RpcTestInt(int value)
// to make them unique.
//
// calledMethod.Name is still "RpcTest", so we need to
// convert this to the generated name as well before comparing.
string calledMethodName_Generated = Weaver.GenerateMethodName("", calledMethod);
if (calledMethodName_Generated == baseRemoteCallName)
{
TypeDefinition baseType = type.BaseType.Resolve();
MethodDefinition baseMethod = baseType.GetMethodInBaseType(callName);
if (baseMethod == null)
{
Log.Error($"Could not find base method for {callName}", method);
WeavingFailed = true;
return;
}
if (!baseMethod.IsVirtual)
{
Log.Error($"Could not find base method that was virtual {callName}", method);
WeavingFailed = true;
return;
}
instruction.Operand = baseMethod;
}
}
}
}
ProcessCommandCall
// invoke internal send and return
// load 'base.' to call the SendCommand function with
worker.Emit(OpCodes.Ldarg_0);
// pass full function name to avoid ClassA.Func <-> ClassB.Func collisions
worker.Emit(OpCodes.Ldstr, md.FullName);
// pass the function hash so we don't have to compute it at runtime
// otherwise each GetStableHash call requires O(N) complexity.
// noticeable for long function names:
// https://github.com/MirrorNetworking/Mirror/issues/3375
worker.Emit(OpCodes.Ldc_I4, md.FullName.GetStableHashCode());
// writer
worker.Emit(OpCodes.Ldloc_0);
worker.Emit(OpCodes.Ldc_I4, channel);
// requiresAuthority ? 1 : 0
worker.Emit(requiresAuthority ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0);
worker.Emit(OpCodes.Call, weaverTypes.sendCommandInternal);
NetworkBehaviourProcessor.WriteReturnWriter(worker, weaverTypes);
worker.Emit(OpCodes.Ret);
这里的worker即md的方法体,请注意在代码执行之前方法体中指令是空的,先前有讲到Mirror动态生成了方法并将原方法方法体进行了替换,这里对这段代码进行解释说明不想关注OPCode的话,可以理解成
base.SendCommandInternal(md.FullName,md.FullName.GetStableHashCode(),writer,channel, requiresAuthority)
。
OpCodes.Ldarg_0 将方法的第一个参数放置到栈顶,对于非静态方法,第一个参数通常为隐式函数this,OpCodes.Ldstr 将字符串压入栈顶,OPCodes.Ldc_I4 压入4字节hashCode到栈顶,压入局部变量0位置插槽的引用(这里特指的NetworkWriter,因为在前面的代码中已经对Writer进行了初始化, NetworkBehaviourProcessor.WriteGetWriter(worker, weaverTypes);
)压入常量4bitchannel的值到栈顶,压入4bit的0或者1到栈顶,bool也占据四字节。OpCodes这个指令后面跟随的式一个MethodReference,就是一个方法引用,我们脑部下虚拟执行的时候会发生上面,通过Call指令拿到下一个4个字节,这四个字节式方法的内存地址,在内存地址,会记录函数的逻辑代码位置,以及参数的个数,虚拟机此时应该是从新开启一个新的栈帧,将参数弹出放置到对应内存位置上,然后从从该方法的逻辑代码地址处开始执行新的函数逻辑(猜测可能有误,欢迎指正)
未完待续… 这完全脱离当初想要,面向新手了解Mirror的初衷了,一定的要控制才行了