本分享的想法源于看了這篇分享
由于在對Unity項(xiàng)目后期進(jìn)行l(wèi)ua熱更新方案實(shí)施, 我也不想造成源代碼的修改, 故在此對上文提及到的后續(xù)方案進(jìn)行補(bǔ)充
本文轉(zhuǎn)載請注明出處: http://www.reibang.com/p/4bef7f66aefd
1.我為何有IL[1]代碼注入的想法
- Unity項(xiàng)目如果初期沒有很好的規(guī)劃代碼熱更, 基本都會選擇C#作為開發(fā)語言, 那么項(xiàng)目后期引入lua機(jī)制, 把舊模塊用lua重寫并非很好的方案, 此時更希望是給舊代碼留一個lua熱更入口.
- 為了減少重復(fù)代碼, 借鑒J2EE領(lǐng)域中AOP[2]實(shí)現(xiàn)思路, 應(yīng)用到此次需求上.
2.lua補(bǔ)丁代碼雛形
public class FooBar
{
public void Foo(string params1, int params2, Action params3)
{
if(LuaPatch.HasPatch("path/to/lua/file", "luaFuncName"))
{
LuaPatch.CallPatch("path/to/lua/file", "luaFuncName", params1, params2, params3);
return;
}
// the old code here
Debug.Log("這里是原來的邏輯代碼, 無返回值");
}
public Vector2 Bar(string params1, int params2, Action params3)
{
if (LuaPatch.HasPatch("path/to/lua/file", "luaFuncName"))
{
return (Vector2)LuaPatch.CallPatch("path/to/lua/file", "luaFuncName", params1, params2, params3);
}
// the old code here
Debug.Log("這里是原來的邏輯代碼, 有返回值");
return Vector2.one;
}
}
至于是使用sLua或者toLua方案, 大家各自根據(jù)項(xiàng)目需要自由選擇.
https://github.com/pangweiwei/slua
https://github.com/topameng/tolua
如果沒有使用lua做大量向量,三角函數(shù)運(yùn)算, 兩個方案沒有太大差異
3.初識IL
IL語法參考文章:http://www.cnblogs.com/Jax/archive/2009/05/29/1491523.html
上面LuaPatch判斷那一段先使用IL語法重新書寫
由于大家時間都很寶貴, 為了節(jié)省時間這里不精通IL語法也行, 這里有一個取巧的方法
- 請自行下載利器: .NET Reflector
- 我們使用Reflector打開Unity工程下\Library\ScriptAssemblies\Assembly-CSharp.dll
找到你事先寫好的希望注入到代碼模板, 這里我以上面Foobar.cs為例
- 篇幅限制, 我把核心的IL代碼貼出并加上注釋, 大家根據(jù)具體情況自行使用Reflector獲取
# 代碼后附帶MSDN文檔鏈接
L_0000: ldstr "path/to/lua/file" -- 壓入string參數(shù)
L_0005: ldstr "luaFuncName"
L_000a: call bool LuaPatch::HasPatch (string, string) -- 調(diào)用方法, 并指定參數(shù)形式
L_000f: brfalse L_0040 -- 相當(dāng)于 if(上述返回值為false) jump L_0040行
L_0014: ldstr "path/to/lua/file" -- 同樣壓入?yún)?shù)
L_0019: ldstr "luaFuncName"
L_001e: ldc.i4.3 -- 對應(yīng)params不定參數(shù), 需要根據(jù)具體不定參個數(shù)聲明對應(yīng)數(shù)組, 這里newarr object, 長度為3
L_001f: newarr object
L_0024: dup -- 復(fù)制棧頂(數(shù)組)的引用并壓入計算堆棧中
L_0025: ldc.i4.0 -- 0下標(biāo)存放本函數(shù)傳入第一個參數(shù)的引用
L_0026: ldarg.1 -- #這里要注意static方法ldarg.0是第一個參數(shù), 非static的ldarg.0存放的是"this"
L_0027: stelem.ref -- 聲明上述傳入數(shù)組的參數(shù)為其對象的引用
L_0028: dup -- 作用同上一個dup
L_0029: ldc.i4.1
L_002a: ldarg.2
L_002b: box int32
L_0030: stelem.ref
L_0031: dup
L_0032: ldc.i4.2
L_0033: ldarg.3
L_0034: stelem.ref
L_0035: call object LuaPatch ::CallPatch (string, string, object[])
L_003a: unbox.any [UnityEngine]UnityEngine.Vector2
L_003f: ret
對IL語法有個大致理解, 有助于稍后用C#進(jìn)行代碼注入, 對于指令可以參考msdn的OpCodes文檔.
4.Mono.Ceil庫
- 能夠標(biāo)記需要注入的類或者方法
利用C#的 特性(Attribute)
1)聲明特性如下:
using System;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class LuaInjectorAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Method)]
public class LuaInjectorIgnoreAttribute : Attribute
{
}
2)使用特性進(jìn)行標(biāo)記
[LuaInjector]
public class CatDog
{
public void Cat()
{
// 這個類所有函數(shù)都會被注入
}
[LuaInjectorIgnore]
public static void Dog()
{
// 只有LuaInjectorIgnore標(biāo)記的會被忽略
}
}
上述作為實(shí)現(xiàn)參考, 當(dāng)然你也可以對Namespace, cs代碼目錄進(jìn)行遍歷, 或者通過代碼主動Add(Type targetType)等方式來進(jìn)行注入標(biāo)記.
3)遍歷dll中所有的類型
var assembly = AssemblyDefinition.ReadAssembly("path/to/Library/ScriptAssemblies/Assembly-CSharp.dll");
foreach (var type in assembly.MainModule.Types)
{
// 判斷Attribute是否LuaInjector等等
}
- C#進(jìn)行IL代碼注入的核心代碼
// 代碼片段
private static bool DoInjector(AssemblyDefinition assembly)
{
var modified = false;
foreach (var type in assembly.MainModule.Types)
{
if (type.HasCustomAttribute<LuaInjectorAttribute>())
{
foreach (var method in type.Methods)
{
if (method.HasCustomAttribute<LuaInjectorIgnoreAttribute>()) continue;
DoInjectMethod(assembly, method, type);
modified = true;
}
}
else
{
foreach (var method in type.Methods)
{
if (!method.HasCustomAttribute<LuaInjectorAttribute>()) continue;
DoInjectMethod(assembly, method, type);
modified = true;
}
}
}
return modified;
}
private static void DoInjectMethod(AssemblyDefinition assembly, MethodDefinition method, TypeDefinition type)
{
if (method.Name.Equals(".ctor") || !method.HasBody) return;
var firstIns = method.Body.Instructions.First();
var worker = method.Body.GetILProcessor();
// bool result = LuaPatch.HasPatch(type.Name)
var hasPatchRef = assembly.MainModule.Import(typeof(LuaPatch).GetMethod("HasPatch"));
var current = InsertBefore(worker, firstIns, worker.Create(OpCodes.Ldstr, type.Name));
current = InsertAfter(worker, current, worker.Create(OpCodes.Ldstr, method.Name));
current = InsertAfter(worker, current, worker.Create(OpCodes.Call, hasPatchRef));
// if(result == false) jump to the under code
current = InsertAfter(worker, current, worker.Create(OpCodes.Brfalse, firstIns));
// else LuaPatch.CallPatch(type.Name, method.Name, args)
var callPatchMethod = typeof(LuaPatch).GetMethod("CallPatch");
var callPatchRef = assembly.MainModule.Import(callPatchMethod);
current = InsertAfter(worker, current, worker.Create(OpCodes.Ldstr, type.Name));
current = InsertAfter(worker, current, worker.Create(OpCodes.Ldstr, method.Name));
var paramsCount = method.Parameters.Count;
// 創(chuàng)建 args參數(shù) object[] 集合
current = InsertAfter(worker, current, worker.Create(OpCodes.Ldc_I4, paramsCount));
current = InsertAfter(worker, current, worker.Create(OpCodes.Newarr, assembly.MainModule.Import(typeof(object))));
for (int index = 0; index < paramsCount; index++)
{
var argIndex = method.IsStatic ? index : index + 1;
// 壓入?yún)?shù)
current = InsertAfter(worker, current, worker.Create(OpCodes.Dup));
current = InsertAfter(worker, current, worker.Create(OpCodes.Ldc_I4, index));
var paramType = method.Parameters[index].ParameterType;
// 獲取參數(shù)類型定義, 用來區(qū)分是否枚舉類 [若你所使用的類型不在本assembly, 則此處需要遍歷其他assembly以取得TypeDefinition]
var paramTypeDef = assembly.MainModule.GetType(paramType.FullName);
// 這里很重要, 需要判斷出 值類型數(shù)據(jù)(不包括枚舉) 是不需要拆箱的
if (paramType.IsValueType && (paramTypeDef == null || !paramTypeDef.IsEnum))
{
current = InsertAfter(worker, current, worker.Create(OpCodes.Ldarg, argIndex));
}
else
{
current = InsertAfter(worker, current, worker.Create(OpCodes.Ldarg, argIndex));
current = InsertAfter(worker, current, worker.Create(OpCodes.Box, paramType));
}
current = InsertAfter(worker, current, worker.Create(OpCodes.Stelem_Ref));
}
current = InsertAfter(worker, current, worker.Create(OpCodes.Call, callPatchRef));
var methodReturnVoid = method.ReturnType.FullName.Equals("System.Void");
var patchCallReturnVoid = callPatchMethod.ReturnType.FullName.Equals("System.Void");
// LuaPatch.CallPatch()有返回值時
if (!patchCallReturnVoid)
{
// 方法無返回值, 則需先Pop出棧區(qū)中CallPatch()返回的結(jié)果
if (methodReturnVoid) current = InsertAfter(worker, current, worker.Create(OpCodes.Pop));
// 方法有返回值時, 返回值進(jìn)行拆箱
else current = InsertAfter(worker, current, worker.Create(OpCodes.Unbox_Any, method.ReturnType));
}
// return
InsertAfter(worker, current, worker.Create(OpCodes.Ret));
// 重新計算語句位置偏移值
ComputeOffsets(method.Body);
}
/// <summary>
/// 語句前插入Instruction, 并返回當(dāng)前語句
/// </summary>
private static Instruction InsertBefore(ILProcessor worker, Instruction target, Instruction instruction)
{
worker.InsertBefore(target, instruction);
return instruction;
}
/// <summary>
/// 語句后插入Instruction, 并返回當(dāng)前語句
/// </summary>
private static Instruction InsertAfter(ILProcessor worker, Instruction target, Instruction instruction)
{
worker.InsertAfter(target, instruction);
return instruction;
}
private static void ComputeOffsets(MethodBody body)
{
var offset = 0;
foreach (var instruction in body.Instructions)
{
instruction.Offset = offset;
offset += instruction.GetSize();
}
}
- 能夠在Unity打包時自動執(zhí)行IL注入
使用特性PostProcessScene進(jìn)行標(biāo)記, 不過注意如果你的項(xiàng)目中有多個Scene需要打包, 這里避免重復(fù)調(diào)用, 需要添加一個_hasMidCodeInjectored用來標(biāo)記, 達(dá)到只在一個場景時機(jī)執(zhí)行注入處理.
// 代碼片段
[PostProcessScene]
private static void MidCodeInjectoring()
{
if (_hasMidCodeInjectored) return;
D.Log("PostProcessBuild::OnPostProcessScene");
// Don't CodeInjector when in Editor and pressing Play
if (Application.isPlaying || EditorApplication.isPlaying) return;
//if (!EditorApplication.isCompiling) return;
BuildTarget buildTarget = EditorUserBuildSettings.activeBuildTarget;
if (buildTarget == BuildTarget.Android)
{
if (DoCodeInjectorBuild("Android"))
{
_hasMidCodeInjectored = true;
}
else
{
D.LogWarning("CodeInjector: Failed to inject Android build!");
}
}
else if (buildTarget == BuildTarget.iPhone)
{
if (DoCodeInjectorBuild("iOS"))
{
_hasMidCodeInjectored = true;
}
else
{
D.LogWarning("CodeInjector: Failed to inject iOS build!");
}
}
}
4.完整源碼
https://github.com/rayosu/UnityDllInjector
-
Unity中不管使用C#還是其他語言, 都會編譯為IL代碼存放為dll形式, iOS打包會進(jìn)行IL2Cpp轉(zhuǎn)換為C++代碼, 所以此處對IL這一中間代碼(dll文件)的修改, 可以達(dá)成注入的目的. ?
-
IL代碼注入只是AOP的一種實(shí)現(xiàn)方案, AOP(面向切面編程)的思想源自GOF設(shè)計模式, 你可以理解為: 用橫向的思考角度, 來統(tǒng)一切入一類相同邏輯的某個"切面"(Aspect), 讓使用者(邏輯程序員)無需重復(fù)關(guān)注這個"橫向面"需要做的工作.這里的切面就是"判斷是否有對應(yīng)Lua補(bǔ)丁" ?