如果还不了解il2cpp代码裁剪请移步:Unity IL2CPP发布64位,以及代码裁剪Strip Engine Code_TopGames的博客-CSDN博客_il2cpp代码裁剪
背景:
il2cpp方式打包有个头疼的问题就是,如果项目使用了AssetBundle,Unity代码裁剪机制不会查找保留AssetBundle或热更新dll引用的引擎代码。例如内置程序集未使用UnityEngine.CanvasGroup这个类,但是热更dll里使用了这个类,打包时代码裁剪机制只从内置程序集中查找保留用到的代码,所以UnityEngine.CanvasGroup就被狠心抛弃了,程序运行时调用UnityEngine.CanvasGroup就会报错。
更麻烦的是我已经在link.xml配置了<assembly fullname="UnityEngine" preserve="all" />和<assembly fullname="UnityEngine.UI" preserve="all" />以保留UnityEngine和UnityEngine.UI两个程序集,但是UnityEngine.CanvasGroup依然报错。
点击CanvasGroup跳转才发现,原来它属于UnityEngine.UIModule程序集:
它的名字空间很容易误导以为它属于UnityEngine程序集, 那么问题来了,某个类所在程序集难以判定,难道只能等哪个类报错了再找哪个类补到link.xml里吗?那些没有暴露出来的岂不是存在极大的隐患?
那么写个工具扫描项目里所有Runtime程序集,然后把那些出现过的类揪出来放到link.xml是不是就一劳永逸了?然鹅~,其实这种“量身裁剪” 的方式对于单机游戏是完美的,即能保证运行时安全,又充分利用了代码裁剪减少了程序集大小。但是...,这对于热更新游戏来说弊大于利,越是“量身裁剪”对后续热更新的限制就越大,一旦热更新引用了新的引擎类就会报错,绕不开就只能重新发包,那么热更新存在的意义何在?
因此对于热更新项目,倒不如抛弃“量身”代码裁剪, 一不做二不休,把工程里所有必要的程序集preserve="all",全部编译打到包体里,这样留给热更新发挥的空间就大了。
工具设计思路:
Okay, 那这样问题就简单了,项目编译后会生成所有项目依赖的程序集dll,为了配置link.xml方便,我们写个UI面板,更直观得显示出全部程序集和已配置到列表里的程序集。通过勾选程序集名字,然后点击保存一键添加到link.xml里。
功能设计:
1. 打开工具界面显示全部程序集轮动列表,列表Item以 勾选框 + 程序集名称显示,已经定义到link.xml里的程序集默认勾选且文字颜色为绿色,反之为白色。一目了然
2. 需要支持一键全选/取消,程序集过多时一个一个勾是场灾难。
3. 需要支持重新加载列表。这样如果用户点击了全选/取消,又想恢复原有列表时,点一下重新加载列表就好了。
4. 保存按钮,把当前列表中勾选的程序集一键写入到link.xml
5. 自动生成不能影响用户自定义部分。把自动生成部分用标识包起来,每次只变更被标识包出来的范围。
程序实现:
1. 获取项目依赖的全部程序集:
Build项目后Unity会在Library子目录生成程序集dll文件,不同目标平台生成的位置不同。HybridCLR会把目标平台生成的程序集单独拷贝出来存放在指定位置。我这里直接读取HybridCLR备份出来的全部程序集dll
/// <summary>
/// 获取项目全部dll
/// </summary>
/// <returns></returns>
public static string[] GetProjectAssemblyDlls()
{
List<string> dlls = new List<string>();
var dllDir = BuildConfig.GetAssembliesPostIl2CppStripDir(EditorUserBuildSettings.activeBuildTarget);
if (!Directory.Exists(dllDir))
{
return dlls.ToArray();
}
var files = Directory.GetFiles(dllDir, "*.dll", SearchOption.AllDirectories);
foreach (var file in files)
{
var fileInfo = new FileInfo(file);
var fileName = fileInfo.Name.Substring(0, fileInfo.Name.Length - fileInfo.Extension.Length);
if (!dlls.Contains(fileName)) dlls.Add(fileName);
}
return dlls.ToArray();
}
上面直接使用了HybridCLR里获取裁剪后dll的方法,Unity Build后会在Library目录下生成裁剪后的dll,不同平台和Unity版本,dll输出目录不同,例如Unity 2021可以通过以下获取:
public static string GetStripAssembliesDir2021(BuildTarget target)
{
string projectDir = Directory.GetParent(Application.dataPath).FullName;
#if UNITY_STANDALONE_WIN
return $"{projectDir}/Library/Bee/artifacts/WinPlayerBuildProgram/ManagedStripped";
#elif UNITY_ANDROID
return $"{projectDir}/Library/Bee/artifacts/Android/ManagedStripped";
#elif UNITY_IOS
return $"{projectDir}/Temp/StagingArea/Data/Managed/tempStrip";
#elif UNITY_WEBGL
return $"{projectDir}/Library/Bee/artifacts/WebGL/ManagedStripped";
#elif UNITY_EDITOR_OSX
return $"{projectDir}/Library/Bee/artifacts/MacStandalonePlayerBuildProgram/ManagedStripped";
#else
throw new NotSupportedException("GetOriginBuildStripAssembliesDir");
#endif
}
2. 解析现有link.xml,拿到已经添加的程序集:
读取link.xml文本行,通过正则表达式匹配获取已配置的程序集名字。
/// <summary>
/// 获取已经配置到link.xml里的dll
/// </summary>
/// <returns></returns>
public static string[] GetSelectedAssemblyDlls()
{
List<string> dlls = new List<string>();
if (!File.Exists(LinkFile))
{
return dlls.ToArray();
}
var lines = File.ReadAllLines(LinkFile);
int generateBeginLine = lines.Length, generateEndLine = lines.Length;
for (int i = 0; i < lines.Length; i++)
{
string line = lines[i];
if (generateBeginLine >= lines.Length && line.Trim().CompareTo(STRIP_GENERATE_TAG) == 0)
{
generateBeginLine = i;
}
else if (generateEndLine >= lines.Length && line.Trim().CompareTo(STRIP_GENERATE_TAG) == 0)
{
generateEndLine = i;
}
if (((i > generateBeginLine && generateEndLine >= lines.Length) || (i > generateBeginLine && i < generateEndLine)) && !string.IsNullOrWhiteSpace(line))
{
var match = Regex.Match(line, MatchPattern);
if (match.Success)
{
var assemblyName = match.Result("$1");
if (!dlls.Contains(assemblyName)) dlls.Add(assemblyName);
}
}
}
return dlls.ToArray();
}
3. 保存程序集列表到link.xml
public static bool Save2LinkFile(string[] stripList)
{
if (!File.Exists(LinkFile))
{
File.WriteAllText(LinkFile, $"<linker>{Environment.NewLine}{STRIP_GENERATE_TAG}{Environment.NewLine}{STRIP_GENERATE_TAG}</linker>");
}
var lines = File.ReadAllLines(LinkFile);
FindGenerateLine(lines, out int beginLineIdx, out int endLineIdx);
int headIdx = ArrayUtility.FindIndex(lines, line => line.Trim().CompareTo("<linker>") == 0);
if (beginLineIdx >= lines.Length)
{
ArrayUtility.Insert(ref lines, headIdx + 1, STRIP_GENERATE_TAG);
}
if (endLineIdx >= lines.Length)
{
ArrayUtility.Insert(ref lines, headIdx + 1, STRIP_GENERATE_TAG);
}
FindGenerateLine(lines, out beginLineIdx, out endLineIdx);
int insertIdx = beginLineIdx;
for (int i = 0; i < stripList.Length; i++)
{
insertIdx = beginLineIdx + i + 1;
if (insertIdx >= endLineIdx)
{
ArrayUtility.Insert(ref lines, endLineIdx, FormatStripLine(stripList[i]));
}
else
{
lines[insertIdx] = FormatStripLine(stripList[i]);
}
}
while ((insertIdx + 1) < lines.Length && lines[insertIdx + 1].Trim().CompareTo(STRIP_GENERATE_TAG) != 0)
{
ArrayUtility.RemoveAt(ref lines, insertIdx + 1);
}
try
{
File.WriteAllLines(LinkFile, lines, System.Text.Encoding.UTF8);
return true;
}
catch (Exception e)
{
Debug.LogErrorFormat("Save2LinkFile Failed:{0}", e.Message);
return false;
}
}
自动生成link.xml:
自动添加的程序集被”<!--GENERATE_TAG--> “标签前后包裹,不影响自定义程序集
<linker>
<!--GENERATE_TAG-->
<assembly fullname="Builtin.Runtime" preserve="all" />
<assembly fullname="Cinemachine" preserve="all" />
<assembly fullname="DOTween" preserve="all" />
<assembly fullname="HybridCLR" preserve="all" />
<assembly fullname="LitJson" preserve="all" />
<assembly fullname="mscorlib" preserve="all" />
<assembly fullname="System.Core" preserve="all" />
<assembly fullname="System" preserve="all" />
<assembly fullname="UltimateJoystick" preserve="all" />
<assembly fullname="Unity.TextMeshPro" preserve="all" />
<assembly fullname="UnityEngine.CoreModule" preserve="all" />
<assembly fullname="UnityEngine" preserve="all" />
<assembly fullname="UnityEngine.TextCoreTextEngineModule" preserve="all" />
<assembly fullname="UnityEngine.TextRenderingModule" preserve="all" />
<assembly fullname="UnityEngine.UI" preserve="all" />
<assembly fullname="UnityEngine.UIModule" preserve="all" />
<assembly fullname="UnityEngine.UnityWebRequestModule" preserve="all" />
<!--GENERATE_TAG-->
<assembly fullname="Assembly-CSharp" preserve="all" />
<assembly fullname="UnityEngine">
<type fullname="UnityEngine.Animator" preserve="all"/>
<type fullname="UnityEngine.Animation" preserve="all"/>
</assembly>
</linker>
4. 编辑器界面UI布局:
private void OnGUI()
{
EditorGUILayout.BeginVertical();
if (dataList.Count <= 0)
{
EditorGUILayout.HelpBox("未找到程序集,请先Build项目以生成程序集.", MessageType.Warning);
}
else
{
EditorGUILayout.HelpBox("勾选需要添加到Link.xml的程序集,然后点击保存生效.", MessageType.Info);
}
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, false, true);
for (int i = 0; i < dataList.Count; i++)
{
EditorGUILayout.BeginHorizontal();
var item = dataList[i];
item.isOn = EditorGUILayout.ToggleLeft(item.dllName, item.isOn, item.isOn ? selectedStyle : normalStyle);
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Select All", GUILayout.Width(100)))
{
SelectAll(true);
}
if (GUILayout.Button("Cancel All", GUILayout.Width(100)))
{
SelectAll(false);
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("Reload", GUILayout.Width(120)))
{
RefreshListData();
}
if (GUILayout.Button("Save", GUILayout.Width(120)))
{
if (MyGameTools.Save2LinkFile(GetCurrentSelectedList()))
{
EditorUtility.DisplayDialog("Strip LinkConfig Editor", "Update link.xml success!", "OK");
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
Strip Config Editor完整代码:
工具类处理逻辑:
using GameFramework;
using HybridCLR;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
public partial class MyGameTools
{
public const string LinkFile = "Assets/link.xml";
public const string STRIP_GENERATE_TAG = "<!--GENERATE_TAG-->";
private const string MatchPattern = "<assembly[\\s]+fullname[\\s]*=[\\s]*\"([^\"]+)\"";
[MenuItem("Game Framework/GameTools/Strip Config Window", false, 1)]
public static void ShowStripConfigEditor()
{
EditorWindow.GetWindow<StripLinkConfigWindow>("Strip LinkConfig Editor").Show();
}
/// <summary>
/// 获取项目全部dll
/// </summary>
/// <returns></returns>
public static string[] GetProjectAssemblyDlls()
{
List<string> dlls = new List<string>();
var dllDir = BuildConfig.GetAssembliesPostIl2CppStripDir(EditorUserBuildSettings.activeBuildTarget);
if (!Directory.Exists(dllDir))
{
return dlls.ToArray();
}
var files = Directory.GetFiles(dllDir, "*.dll", SearchOption.AllDirectories);
foreach (var file in files)
{
var fileInfo = new FileInfo(file);
var fileName = fileInfo.Name.Substring(0, fileInfo.Name.Length - fileInfo.Extension.Length);
if (!dlls.Contains(fileName)) dlls.Add(fileName);
}
return dlls.ToArray();
}
/// <summary>
/// 获取已经配置到link.xml里的dll
/// </summary>
/// <returns></returns>
public static string[] GetSelectedAssemblyDlls()
{
List<string> dlls = new List<string>();
if (!File.Exists(LinkFile))
{
return dlls.ToArray();
}
var lines = File.ReadAllLines(LinkFile);
int generateBeginLine = lines.Length, generateEndLine = lines.Length;
for (int i = 0; i < lines.Length; i++)
{
string line = lines[i];
if (generateBeginLine >= lines.Length && line.Trim().CompareTo(STRIP_GENERATE_TAG) == 0)
{
generateBeginLine = i;
}
else if (generateEndLine >= lines.Length && line.Trim().CompareTo(STRIP_GENERATE_TAG) == 0)
{
generateEndLine = i;
}
if (((i > generateBeginLine && generateEndLine >= lines.Length) || (i > generateBeginLine && i < generateEndLine)) && !string.IsNullOrWhiteSpace(line))
{
var match = Regex.Match(line, MatchPattern);
if (match.Success)
{
var assemblyName = match.Result("$1");
if (!dlls.Contains(assemblyName)) dlls.Add(assemblyName);
}
}
}
return dlls.ToArray();
}
public static bool Save2LinkFile(string[] stripList)
{
if (!File.Exists(LinkFile))
{
File.WriteAllText(LinkFile, $"<linker>{Environment.NewLine}{STRIP_GENERATE_TAG}{Environment.NewLine}{STRIP_GENERATE_TAG}</linker>");
}
var lines = File.ReadAllLines(LinkFile);
FindGenerateLine(lines, out int beginLineIdx, out int endLineIdx);
int headIdx = ArrayUtility.FindIndex(lines, line => line.Trim().CompareTo("<linker>") == 0);
if (beginLineIdx >= lines.Length)
{
ArrayUtility.Insert(ref lines, headIdx + 1, STRIP_GENERATE_TAG);
}
if (endLineIdx >= lines.Length)
{
ArrayUtility.Insert(ref lines, headIdx + 1, STRIP_GENERATE_TAG);
}
FindGenerateLine(lines, out beginLineIdx, out endLineIdx);
int insertIdx = beginLineIdx;
for (int i = 0; i < stripList.Length; i++)
{
insertIdx = beginLineIdx + i + 1;
if (insertIdx >= endLineIdx)
{
ArrayUtility.Insert(ref lines, endLineIdx, FormatStripLine(stripList[i]));
}
else
{
lines[insertIdx] = FormatStripLine(stripList[i]);
}
}
while ((insertIdx + 1) < lines.Length && lines[insertIdx + 1].Trim().CompareTo(STRIP_GENERATE_TAG) != 0)
{
ArrayUtility.RemoveAt(ref lines, insertIdx + 1);
}
try
{
File.WriteAllLines(LinkFile, lines, System.Text.Encoding.UTF8);
return true;
}
catch (Exception e)
{
Debug.LogErrorFormat("Save2LinkFile Failed:{0}", e.Message);
return false;
}
}
private static string FormatStripLine(string assemblyName)
{
return $"\t<assembly fullname=\"{assemblyName}\" preserve=\"all\" />";
}
private static void FindGenerateLine(string[] lines, out int beginLineIdx, out int endLineIdx)
{
beginLineIdx = endLineIdx = lines.Length;
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i];
if (beginLineIdx >= lines.Length && line.Trim().CompareTo(STRIP_GENERATE_TAG) == 0)
{
beginLineIdx = i;
}
else if (endLineIdx >= lines.Length && line.Trim().CompareTo(STRIP_GENERATE_TAG) == 0)
{
endLineIdx = i;
}
}
}
}
编辑器GUI:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
public class StripLinkConfigWindow : EditorWindow
{
private class ItemData
{
public bool isOn;
public string dllName;
public ItemData(bool isOn, string dllName)
{
this.isOn = isOn;
this.dllName = dllName;
}
}
private Vector2 scrollPosition;
private string[] selectedDllList;
private List<ItemData> dataList;
private GUIStyle normalStyle;
private GUIStyle selectedStyle;
private void OnEnable()
{
normalStyle = new GUIStyle();
normalStyle.normal.textColor = Color.white;
selectedStyle = new GUIStyle();
selectedStyle.normal.textColor = Color.green;
dataList = new List<ItemData>();
RefreshListData();
}
private void OnGUI()
{
EditorGUILayout.BeginVertical();
if (dataList.Count <= 0)
{
EditorGUILayout.HelpBox("未找到程序集,请先Build项目以生成程序集.", MessageType.Warning);
}
else
{
EditorGUILayout.HelpBox("勾选需要添加到Link.xml的程序集,然后点击保存生效.", MessageType.Info);
}
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, false, true);
for (int i = 0; i < dataList.Count; i++)
{
EditorGUILayout.BeginHorizontal();
var item = dataList[i];
item.isOn = EditorGUILayout.ToggleLeft(item.dllName, item.isOn, item.isOn ? selectedStyle : normalStyle);
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Select All", GUILayout.Width(100)))
{
SelectAll(true);
}
if (GUILayout.Button("Cancel All", GUILayout.Width(100)))
{
SelectAll(false);
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("Reload", GUILayout.Width(120)))
{
RefreshListData();
}
if (GUILayout.Button("Save", GUILayout.Width(120)))
{
if (MyGameTools.Save2LinkFile(GetCurrentSelectedList()))
{
EditorUtility.DisplayDialog("Strip LinkConfig Editor", "Update link.xml success!", "OK");
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
private void SelectAll(bool isOn)
{
foreach (var item in dataList)
{
item.isOn = isOn;
}
}
private string[] GetCurrentSelectedList()
{
List<string> result = new List<string>();
foreach (var item in dataList)
{
if (item.isOn)
{
result.Add(item.dllName);
}
}
return result.ToArray();
}
private void RefreshListData()
{
dataList.Clear();
selectedDllList = MyGameTools.GetSelectedAssemblyDlls();
foreach (var item in MyGameTools.GetProjectAssemblyDlls())
{
dataList.Add(new ItemData(IsInSelectedList(item), item));
}
}
private bool IsInSelectedList(string dllName)
{
return ArrayUtility.Contains(selectedDllList, dllName);
}
}