• 【Unity编辑器扩展】il2cpp代码裁剪(Strip Engine Code)配置工具


    如果还不了解il2cpp代码裁剪请移步:Unity IL2CPP发布64位,以及代码裁剪Strip Engine Code_TopGames的博客-CSDN博客_il2cpp代码裁剪

    背景: 

    il2cpp方式打包有个头疼的问题就是,如果项目使用了AssetBundle,Unity代码裁剪机制不会查找保留AssetBundle或热更新dll引用的引擎代码。例如内置程序集未使用UnityEngine.CanvasGroup这个类,但是热更dll里使用了这个类,打包时代码裁剪机制只从内置程序集中查找保留用到的代码,所以UnityEngine.CanvasGroup就被狠心抛弃了,程序运行时调用UnityEngine.CanvasGroup就会报错。

    更麻烦的是我已经在link.xml配置了以保留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. 自动生成不能影响用户自定义部分。把自动生成部分用标识包起来,每次只变更被标识包出来的范围。

    Strip Config Editor

     程序实现:

     1. 获取项目依赖的全部程序集:

    Build项目后Unity会在Library子目录生成程序集dll文件,不同目标平台生成的位置不同。HybridCLR会把目标平台生成的程序集单独拷贝出来存放在指定位置。我这里直接读取HybridCLR备份出来的全部程序集dll

    1. ///
    2. /// 获取项目全部dll
    3. ///
    4. ///
    5. public static string[] GetProjectAssemblyDlls()
    6. {
    7. List<string> dlls = new List<string>();
    8. var dllDir = BuildConfig.GetAssembliesPostIl2CppStripDir(EditorUserBuildSettings.activeBuildTarget);
    9. if (!Directory.Exists(dllDir))
    10. {
    11. return dlls.ToArray();
    12. }
    13. var files = Directory.GetFiles(dllDir, "*.dll", SearchOption.AllDirectories);
    14. foreach (var file in files)
    15. {
    16. var fileInfo = new FileInfo(file);
    17. var fileName = fileInfo.Name.Substring(0, fileInfo.Name.Length - fileInfo.Extension.Length);
    18. if (!dlls.Contains(fileName)) dlls.Add(fileName);
    19. }
    20. return dlls.ToArray();
    21. }

     上面直接使用了HybridCLR里获取裁剪后dll的方法,Unity Build后会在Library目录下生成裁剪后的dll,不同平台和Unity版本,dll输出目录不同,例如Unity 2021可以通过以下获取:

    1. public static string GetStripAssembliesDir2021(BuildTarget target)
    2. {
    3. string projectDir = Directory.GetParent(Application.dataPath).FullName;
    4. #if UNITY_STANDALONE_WIN
    5. return $"{projectDir}/Library/Bee/artifacts/WinPlayerBuildProgram/ManagedStripped";
    6. #elif UNITY_ANDROID
    7. return $"{projectDir}/Library/Bee/artifacts/Android/ManagedStripped";
    8. #elif UNITY_IOS
    9. return $"{projectDir}/Temp/StagingArea/Data/Managed/tempStrip";
    10. #elif UNITY_WEBGL
    11. return $"{projectDir}/Library/Bee/artifacts/WebGL/ManagedStripped";
    12. #elif UNITY_EDITOR_OSX
    13. return $"{projectDir}/Library/Bee/artifacts/MacStandalonePlayerBuildProgram/ManagedStripped";
    14. #else
    15. throw new NotSupportedException("GetOriginBuildStripAssembliesDir");
    16. #endif
    17. }

    2. 解析现有link.xml,拿到已经添加的程序集:

    读取link.xml文本行,通过正则表达式匹配获取已配置的程序集名字。

    1. ///
    2. /// 获取已经配置到link.xml里的dll
    3. ///
    4. ///
    5. public static string[] GetSelectedAssemblyDlls()
    6. {
    7. List<string> dlls = new List<string>();
    8. if (!File.Exists(LinkFile))
    9. {
    10. return dlls.ToArray();
    11. }
    12. var lines = File.ReadAllLines(LinkFile);
    13. int generateBeginLine = lines.Length, generateEndLine = lines.Length;
    14. for (int i = 0; i < lines.Length; i++)
    15. {
    16. string line = lines[i];
    17. if (generateBeginLine >= lines.Length && line.Trim().CompareTo(STRIP_GENERATE_TAG) == 0)
    18. {
    19. generateBeginLine = i;
    20. }
    21. else if (generateEndLine >= lines.Length && line.Trim().CompareTo(STRIP_GENERATE_TAG) == 0)
    22. {
    23. generateEndLine = i;
    24. }
    25. if (((i > generateBeginLine && generateEndLine >= lines.Length) || (i > generateBeginLine && i < generateEndLine)) && !string.IsNullOrWhiteSpace(line))
    26. {
    27. var match = Regex.Match(line, MatchPattern);
    28. if (match.Success)
    29. {
    30. var assemblyName = match.Result("$1");
    31. if (!dlls.Contains(assemblyName)) dlls.Add(assemblyName);
    32. }
    33. }
    34. }
    35. return dlls.ToArray();
    36. }

    3. 保存程序集列表到link.xml

    1. public static bool Save2LinkFile(string[] stripList)
    2. {
    3. if (!File.Exists(LinkFile))
    4. {
    5. File.WriteAllText(LinkFile, $"{Environment.NewLine}{STRIP_GENERATE_TAG}{Environment.NewLine}{STRIP_GENERATE_TAG}");
    6. }
    7. var lines = File.ReadAllLines(LinkFile);
    8. FindGenerateLine(lines, out int beginLineIdx, out int endLineIdx);
    9. int headIdx = ArrayUtility.FindIndex(lines, line => line.Trim().CompareTo("") == 0);
    10. if (beginLineIdx >= lines.Length)
    11. {
    12. ArrayUtility.Insert(ref lines, headIdx + 1, STRIP_GENERATE_TAG);
    13. }
    14. if (endLineIdx >= lines.Length)
    15. {
    16. ArrayUtility.Insert(ref lines, headIdx + 1, STRIP_GENERATE_TAG);
    17. }
    18. FindGenerateLine(lines, out beginLineIdx, out endLineIdx);
    19. int insertIdx = beginLineIdx;
    20. for (int i = 0; i < stripList.Length; i++)
    21. {
    22. insertIdx = beginLineIdx + i + 1;
    23. if (insertIdx >= endLineIdx)
    24. {
    25. ArrayUtility.Insert(ref lines, endLineIdx, FormatStripLine(stripList[i]));
    26. }
    27. else
    28. {
    29. lines[insertIdx] = FormatStripLine(stripList[i]);
    30. }
    31. }
    32. while ((insertIdx + 1) < lines.Length && lines[insertIdx + 1].Trim().CompareTo(STRIP_GENERATE_TAG) != 0)
    33. {
    34. ArrayUtility.RemoveAt(ref lines, insertIdx + 1);
    35. }
    36. try
    37. {
    38. File.WriteAllLines(LinkFile, lines, System.Text.Encoding.UTF8);
    39. return true;
    40. }
    41. catch (Exception e)
    42. {
    43. Debug.LogErrorFormat("Save2LinkFile Failed:{0}", e.Message);
    44. return false;
    45. }
    46. }

    自动生成link.xml:

    自动添加的程序集被” “标签前后包裹,不影响自定义程序集

    1. "Builtin.Runtime" preserve="all" />
    2. "Cinemachine" preserve="all" />
    3. "DOTween" preserve="all" />
    4. "HybridCLR" preserve="all" />
    5. "LitJson" preserve="all" />
    6. "mscorlib" preserve="all" />
    7. "System.Core" preserve="all" />
    8. "System" preserve="all" />
    9. "UltimateJoystick" preserve="all" />
    10. "Unity.TextMeshPro" preserve="all" />
    11. "UnityEngine.CoreModule" preserve="all" />
    12. "UnityEngine" preserve="all" />
    13. "UnityEngine.TextCoreTextEngineModule" preserve="all" />
    14. "UnityEngine.TextRenderingModule" preserve="all" />
    15. "UnityEngine.UI" preserve="all" />
    16. "UnityEngine.UIModule" preserve="all" />
    17. "UnityEngine.UnityWebRequestModule" preserve="all" />
    18. "Assembly-CSharp" preserve="all" />
    19. "UnityEngine">
    20. "UnityEngine.Animator" preserve="all"/>
    21. "UnityEngine.Animation" preserve="all"/>

    4. 编辑器界面UI布局:

    1. private void OnGUI()
    2. {
    3. EditorGUILayout.BeginVertical();
    4. if (dataList.Count <= 0)
    5. {
    6. EditorGUILayout.HelpBox("未找到程序集,请先Build项目以生成程序集.", MessageType.Warning);
    7. }
    8. else
    9. {
    10. EditorGUILayout.HelpBox("勾选需要添加到Link.xml的程序集,然后点击保存生效.", MessageType.Info);
    11. }
    12. scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, false, true);
    13. for (int i = 0; i < dataList.Count; i++)
    14. {
    15. EditorGUILayout.BeginHorizontal();
    16. var item = dataList[i];
    17. item.isOn = EditorGUILayout.ToggleLeft(item.dllName, item.isOn, item.isOn ? selectedStyle : normalStyle);
    18. EditorGUILayout.EndHorizontal();
    19. }
    20. EditorGUILayout.EndScrollView();
    21. EditorGUILayout.BeginHorizontal();
    22. if (GUILayout.Button("Select All", GUILayout.Width(100)))
    23. {
    24. SelectAll(true);
    25. }
    26. if (GUILayout.Button("Cancel All", GUILayout.Width(100)))
    27. {
    28. SelectAll(false);
    29. }
    30. GUILayout.FlexibleSpace();
    31. if (GUILayout.Button("Reload", GUILayout.Width(120)))
    32. {
    33. RefreshListData();
    34. }
    35. if (GUILayout.Button("Save", GUILayout.Width(120)))
    36. {
    37. if (MyGameTools.Save2LinkFile(GetCurrentSelectedList()))
    38. {
    39. EditorUtility.DisplayDialog("Strip LinkConfig Editor", "Update link.xml success!", "OK");
    40. }
    41. }
    42. EditorGUILayout.EndHorizontal();
    43. EditorGUILayout.EndVertical();
    44. }

    Strip Config Editor完整代码:

    工具类处理逻辑:

    1. using GameFramework;
    2. using HybridCLR;
    3. using System;
    4. using System.Collections;
    5. using System.Collections.Generic;
    6. using System.IO;
    7. using System.Text.RegularExpressions;
    8. using UnityEditor;
    9. using UnityEngine;
    10. public partial class MyGameTools
    11. {
    12. public const string LinkFile = "Assets/link.xml";
    13. public const string STRIP_GENERATE_TAG = "";
    14. private const string MatchPattern = ";
    15. [MenuItem("Game Framework/GameTools/Strip Config Window", false, 1)]
    16. public static void ShowStripConfigEditor()
    17. {
    18. EditorWindow.GetWindow("Strip LinkConfig Editor").Show();
    19. }
    20. ///
    21. /// 获取项目全部dll
    22. ///
    23. ///
    24. public static string[] GetProjectAssemblyDlls()
    25. {
    26. List<string> dlls = new List<string>();
    27. var dllDir = BuildConfig.GetAssembliesPostIl2CppStripDir(EditorUserBuildSettings.activeBuildTarget);
    28. if (!Directory.Exists(dllDir))
    29. {
    30. return dlls.ToArray();
    31. }
    32. var files = Directory.GetFiles(dllDir, "*.dll", SearchOption.AllDirectories);
    33. foreach (var file in files)
    34. {
    35. var fileInfo = new FileInfo(file);
    36. var fileName = fileInfo.Name.Substring(0, fileInfo.Name.Length - fileInfo.Extension.Length);
    37. if (!dlls.Contains(fileName)) dlls.Add(fileName);
    38. }
    39. return dlls.ToArray();
    40. }
    41. ///
    42. /// 获取已经配置到link.xml里的dll
    43. ///
    44. ///
    45. public static string[] GetSelectedAssemblyDlls()
    46. {
    47. List<string> dlls = new List<string>();
    48. if (!File.Exists(LinkFile))
    49. {
    50. return dlls.ToArray();
    51. }
    52. var lines = File.ReadAllLines(LinkFile);
    53. int generateBeginLine = lines.Length, generateEndLine = lines.Length;
    54. for (int i = 0; i < lines.Length; i++)
    55. {
    56. string line = lines[i];
    57. if (generateBeginLine >= lines.Length && line.Trim().CompareTo(STRIP_GENERATE_TAG) == 0)
    58. {
    59. generateBeginLine = i;
    60. }
    61. else if (generateEndLine >= lines.Length && line.Trim().CompareTo(STRIP_GENERATE_TAG) == 0)
    62. {
    63. generateEndLine = i;
    64. }
    65. if (((i > generateBeginLine && generateEndLine >= lines.Length) || (i > generateBeginLine && i < generateEndLine)) && !string.IsNullOrWhiteSpace(line))
    66. {
    67. var match = Regex.Match(line, MatchPattern);
    68. if (match.Success)
    69. {
    70. var assemblyName = match.Result("$1");
    71. if (!dlls.Contains(assemblyName)) dlls.Add(assemblyName);
    72. }
    73. }
    74. }
    75. return dlls.ToArray();
    76. }
    77. public static bool Save2LinkFile(string[] stripList)
    78. {
    79. if (!File.Exists(LinkFile))
    80. {
    81. File.WriteAllText(LinkFile, $"{Environment.NewLine}{STRIP_GENERATE_TAG}{Environment.NewLine}{STRIP_GENERATE_TAG}");
    82. }
    83. var lines = File.ReadAllLines(LinkFile);
    84. FindGenerateLine(lines, out int beginLineIdx, out int endLineIdx);
    85. int headIdx = ArrayUtility.FindIndex(lines, line => line.Trim().CompareTo("") == 0);
    86. if (beginLineIdx >= lines.Length)
    87. {
    88. ArrayUtility.Insert(ref lines, headIdx + 1, STRIP_GENERATE_TAG);
    89. }
    90. if (endLineIdx >= lines.Length)
    91. {
    92. ArrayUtility.Insert(ref lines, headIdx + 1, STRIP_GENERATE_TAG);
    93. }
    94. FindGenerateLine(lines, out beginLineIdx, out endLineIdx);
    95. int insertIdx = beginLineIdx;
    96. for (int i = 0; i < stripList.Length; i++)
    97. {
    98. insertIdx = beginLineIdx + i + 1;
    99. if (insertIdx >= endLineIdx)
    100. {
    101. ArrayUtility.Insert(ref lines, endLineIdx, FormatStripLine(stripList[i]));
    102. }
    103. else
    104. {
    105. lines[insertIdx] = FormatStripLine(stripList[i]);
    106. }
    107. }
    108. while ((insertIdx + 1) < lines.Length && lines[insertIdx + 1].Trim().CompareTo(STRIP_GENERATE_TAG) != 0)
    109. {
    110. ArrayUtility.RemoveAt(ref lines, insertIdx + 1);
    111. }
    112. try
    113. {
    114. File.WriteAllLines(LinkFile, lines, System.Text.Encoding.UTF8);
    115. return true;
    116. }
    117. catch (Exception e)
    118. {
    119. Debug.LogErrorFormat("Save2LinkFile Failed:{0}", e.Message);
    120. return false;
    121. }
    122. }
    123. private static string FormatStripLine(string assemblyName)
    124. {
    125. return $"\t{assemblyName}\" preserve=\"all\" />";
    126. }
    127. private static void FindGenerateLine(string[] lines, out int beginLineIdx, out int endLineIdx)
    128. {
    129. beginLineIdx = endLineIdx = lines.Length;
    130. for (int i = 0; i < lines.Length; i++)
    131. {
    132. var line = lines[i];
    133. if (beginLineIdx >= lines.Length && line.Trim().CompareTo(STRIP_GENERATE_TAG) == 0)
    134. {
    135. beginLineIdx = i;
    136. }
    137. else if (endLineIdx >= lines.Length && line.Trim().CompareTo(STRIP_GENERATE_TAG) == 0)
    138. {
    139. endLineIdx = i;
    140. }
    141. }
    142. }
    143. }

    编辑器GUI:

    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using UnityEditor;
    6. using UnityEngine.UIElements;
    7. public class StripLinkConfigWindow : EditorWindow
    8. {
    9. private class ItemData
    10. {
    11. public bool isOn;
    12. public string dllName;
    13. public ItemData(bool isOn, string dllName)
    14. {
    15. this.isOn = isOn;
    16. this.dllName = dllName;
    17. }
    18. }
    19. private Vector2 scrollPosition;
    20. private string[] selectedDllList;
    21. private List dataList;
    22. private GUIStyle normalStyle;
    23. private GUIStyle selectedStyle;
    24. private void OnEnable()
    25. {
    26. normalStyle = new GUIStyle();
    27. normalStyle.normal.textColor = Color.white;
    28. selectedStyle = new GUIStyle();
    29. selectedStyle.normal.textColor = Color.green;
    30. dataList = new List();
    31. RefreshListData();
    32. }
    33. private void OnGUI()
    34. {
    35. EditorGUILayout.BeginVertical();
    36. if (dataList.Count <= 0)
    37. {
    38. EditorGUILayout.HelpBox("未找到程序集,请先Build项目以生成程序集.", MessageType.Warning);
    39. }
    40. else
    41. {
    42. EditorGUILayout.HelpBox("勾选需要添加到Link.xml的程序集,然后点击保存生效.", MessageType.Info);
    43. }
    44. scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, false, true);
    45. for (int i = 0; i < dataList.Count; i++)
    46. {
    47. EditorGUILayout.BeginHorizontal();
    48. var item = dataList[i];
    49. item.isOn = EditorGUILayout.ToggleLeft(item.dllName, item.isOn, item.isOn ? selectedStyle : normalStyle);
    50. EditorGUILayout.EndHorizontal();
    51. }
    52. EditorGUILayout.EndScrollView();
    53. EditorGUILayout.BeginHorizontal();
    54. if (GUILayout.Button("Select All", GUILayout.Width(100)))
    55. {
    56. SelectAll(true);
    57. }
    58. if (GUILayout.Button("Cancel All", GUILayout.Width(100)))
    59. {
    60. SelectAll(false);
    61. }
    62. GUILayout.FlexibleSpace();
    63. if (GUILayout.Button("Reload", GUILayout.Width(120)))
    64. {
    65. RefreshListData();
    66. }
    67. if (GUILayout.Button("Save", GUILayout.Width(120)))
    68. {
    69. if (MyGameTools.Save2LinkFile(GetCurrentSelectedList()))
    70. {
    71. EditorUtility.DisplayDialog("Strip LinkConfig Editor", "Update link.xml success!", "OK");
    72. }
    73. }
    74. EditorGUILayout.EndHorizontal();
    75. EditorGUILayout.EndVertical();
    76. }
    77. private void SelectAll(bool isOn)
    78. {
    79. foreach (var item in dataList)
    80. {
    81. item.isOn = isOn;
    82. }
    83. }
    84. private string[] GetCurrentSelectedList()
    85. {
    86. List<string> result = new List<string>();
    87. foreach (var item in dataList)
    88. {
    89. if (item.isOn)
    90. {
    91. result.Add(item.dllName);
    92. }
    93. }
    94. return result.ToArray();
    95. }
    96. private void RefreshListData()
    97. {
    98. dataList.Clear();
    99. selectedDllList = MyGameTools.GetSelectedAssemblyDlls();
    100. foreach (var item in MyGameTools.GetProjectAssemblyDlls())
    101. {
    102. dataList.Add(new ItemData(IsInSelectedList(item), item));
    103. }
    104. }
    105. private bool IsInSelectedList(string dllName)
    106. {
    107. return ArrayUtility.Contains(selectedDllList, dllName);
    108. }
    109. }

  • 相关阅读:
    数据结构与算法复习:第三十五弹
    HTML静态网页作业——我的家乡安庆
    负载均衡的原理和算法
    【中秋国庆不断更】OpenHarmony多态样式stateStyles使用场景
    Springboot+vue的船舶监造系统(有报告)。Javaee项目,springboot vue前后端分离项目。
    JSR303校验(1)
    Redis学习 - 了解Redis(三)
    【Linux】(五)GateWay远程开发方式-实验室服务器使用GateWay远程开发
    如何使用DotNet-MetaData识别.NET恶意软件源码文件元数据
    从头开始进行CUDA编程:流和事件
  • 原文地址:https://blog.csdn.net/final5788/article/details/126451377