【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配置了<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. 自动生成不能影响用户自定义部分。把自动生成部分用标识包起来,每次只变更被标识包出来的范围。

Strip Config Editor

 程序实现:

 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);
    }
}

猜你喜欢

转载自blog.csdn.net/final5788/article/details/126451377