# 一、UGUI

  1. # 超链接

    已解决,超链接点击返回剔除富文本信息

    详细可见另一篇文档:处理 Unity2019 的 UGUI 顶点数据更新后的图文混排问题

  2. # 图文混排

    已解决,计算表情绘制下标时剔除表情标签、剔除富文本信息

    详细可见另一篇文档:处理 Unity2019 的 UGUI 顶点数据更新后的图文混排问题

  3. # 竖行排版

    由于 Unity 本身提供的 UILineInfo 包含了换行符信息、而顶点数据并不包括导致。

    已解决,自行通过『\n』 分割文本,并剔除富文本信息再作计算。

    详细可见另一篇文档:处理 Unity2019 的 UGUI 顶点数据更新后的图文混排问题

  4. # AorTextIncSprites 缺少最后一位内容

    查看代码,并经过调试,最后发现是因为绘制时手动减掉了最后 4 位顶点,关键代码如下:

    //Last 4 verts are always a new line...
    int vertCount = verts.Count-4;

    这里根据注释,就可以明白了,最后一位以前是当换行符去掉了,新版顶点并无这些信息,不再减去 4 个顶点即可。

  5. # 行军线绘制

    使用 UGUI + 自定义 Shader 实现

    由于自定义 UGUI 顶点绘制,新版 Unity 无法正常工作,通过禁用 raycastTarget 可以避免报错,走上正常逻辑,但不可见。

    • 2022.7.22 打出来真机包可见,此时暂时可排除顶点绘制函数本身逻辑不兼容嫌疑
    • 后进行调试,发现新新军线绘制代码 OnPopulateMesh 无调用
    • 后查询 UGUI Graphic 源码,得知更新条件需要 CanvasRender
    • 并经调试查询,发现内部有缺失 CanvasRender 报错
    • 由此可知 CanvasRender 是一个关键点
    • 尝试运行时手动添加,无效
    • 尝试按照源码规则调用,发现获取失败
    • 且修改调用方式 gameObject.GetComponent<CanvasRenderer>() 可以获取
    • 该类直接放置于另一个类脚本中,按理说挂载的脚本都是单独一个类,且类名需与代码文件名一致,怀疑是否因此问题导致 (尝试后表明于此无关)
    • 试图添加此绘制组件前,先添加 CanvasRender,cull 置为 false
    obj.AddComponent<CanvasRenderer>().cull = false

    已解决

    注:其实 UGUI 源代码中,对于 CanvasRenderer 的获取,也是做了额外判断的,若获取空则会添加,不清楚是什么条件造成了添加代码无效的。

  6. # AorUIStage# 自适应子节点长宽为 NaN

    可确定由 AorUiRuntimeUtility 动态创建 AorUIStage# 后,再动态创建可自适应大小的 MainLayer 子容器导致

    //AorUIStage
    GameObject stage = CreatePrefab_UIBase(canv.transform, 0, 0, 512f, 512f, 0.5f, 0.5f, 0.5f, 0.5f);
    stage.name = "AorUIStage#";
    //AorUIStage.mainLayer
    GameObject ml = CreatePrefab_UIBase(stage.transform);
    ml.name = "MainLayer";
    GameObject tl = CreatePrefab_UIBase(stage.transform);
    tl.name = "TopLayer";

    修改 AorUIManager updateStageSize 方法,增加重置计算

    private void updateStageSize()
    {
    	if (Application.isEditor && !Application.isPlaying)
    	{
    		if (!isAwakeInit || !isInit)
    		{
    			return;
    		}
    	}
    	switch (_scaleMode)
    	{
    		case ScaleModeType.widthBase:
    			set2WidthBaseMode();
    			break;
    		case ScaleModeType.heightBase:
    			set2HeightBaseMode();
    			break;
    		case ScaleModeType.fitScreen:
    			set2FitScreenMode();
    			break;
    		case ScaleModeType.envelopeScreen:
    			set2EnvelopeScreenMode();
    			break;
    		default:
    			break;
    	}
    	// 升级 2019 后,自适应子节点会出现问题,这里强制重设一次
    	for (int i = 0; i < Stage.childCount; i++)
    	{
    		RectTransform rt = Stage.GetChild(i) as RectTransform;
    		rt.anchoredPosition = Vector2.zero;
    		rt.sizeDelta = Vector2.zero;
    	}
    }

    已解决
    注:解决方式应当不止这种。

# 二、Socket 连接

在编辑器 / 真机包 返回登录界面时触发。

查询是由于调用 socket.Disconnect(true); 导致,估计是以前想复用 Socket 实例,不过查了下代码,每次 open 实际上都是重新创建的一个,将 Disconnect 的调用替换为如下代码解决:

try
{
    socket.Shutdown(SocketShutdown.Both);
}
catch (Exception)
{
    //Do Nothing
}
socket.Close();
socket = null;

# 三、自定义 DLL 的库文件引用

已解决

DLL 框架版本设置为 4.0 以上 (我是 4.5)

并重新分散引用 Unity 内部 DLL,路径位于对应 Unity 安装目录

Unity 2021.3.0f1\Editor\Data\Managed\UnityEngine

注:重新引用后注意修改 DLL 复制本地 为 false

# 四、打 Assetbundle 包出现资源反复 Import 问题

表现为初始化环境时,还没轮到正式打 Assetbundle,每个 prefab 皆出现 Import 提示

经过调试,发现新版 Unity 在每次打包之前重设标签时,都会导致重新导入一次资源,增加大量额外时间消耗。

例如:

已解决,重设标签时,判断旧标签是否与新标签一致。

代码位于 AssetBundleTool AddLabel

public static void AddLabel(Object obj, string label)
{
    //ClearLabel(obj);
    UnityEngine.Object _object = obj;
    if (null != _object)
    {
        List<string> _labelList = AssetDatabase.GetLabels(obj).ToList();
        // 对象目前已有标签与设置标签不一致,或者对象拥有多个标签 (不合法),则重设标签
        if (!_labelList.Contains(label) || _labelList.Count > 1)
        {
            ClearLabel(obj);
            AssetDatabase.SetLabels(obj, new string[] { label });
        }
        // 如果这个物体本来就有指定的 label,就不需要重复处理了
        //AssetDatabase.SetLabels(obj, _labelList.ToArray());
    }
}

注:方法开口有调用 ClearLabel,可知一个对象只会存在一个标签,因此此处直接判断设置标签是否相等即可。

# 五、打 Assetbundle 包崩溃

查询日志得出为栈溢出:

Couldn't extract exception string from exception of type StackOverflowException (another exception of class 'StackOverflowException' was thrown while processing the stack trace)

StackOverflowException: The requested operation caused a stack overflow.
  at (wrapper managed-to-native) System.String.FastAllocateString(int)
  at System.String.CreateStringFromEncoding (System.Byte* bytes, System.Int32 byteLength, System.Text.Encoding encoding) [0x00013] in <6073cf49ed704e958b8a66d540dea948>:0 
  at System.Text.Encoding.GetString (System.Byte* bytes, System.Int32 byteCount) [0x00033] in <6073cf49ed704e958b8a66d540dea948>:0 
  at System.Text.Encoding.GetString (System.ReadOnlySpan`1[T] bytes) [0x00013] in <6073cf49ed704e958b8a66d540dea948>:0 
  at System.String.Ctor (System.SByte* value, System.Int32 startIndex, System.Int32 length, System.Text.Encoding enc) [0x0006d] in <6073cf49ed704e958b8a66d540dea948>:0 
  at System.String.CreateString (System.SByte* value, System.Int32 startIndex, System.Int32 length, System.Text.Encoding enc) [0x00000] in <6073cf49ed704e958b8a66d540dea948>:0 
  at (wrapper managed-to-managed) System.String..ctor(sbyte*,int,int,System.Text.Encoding)
  at UnityEngine.StackTraceUtility.ExtractStackTrace () [0x0002c] in <1332c534a8a44623b2e5cc943a0b790e>:0 
  at (wrapper managed-to-native) UnityEngine.DebugLogHandler.Internal_Log(UnityEngine.LogType,UnityEngine.LogOption,string,UnityEngine.Object)
  at UnityEngine.DebugLogHandler.LogFormat (UnityEngine.LogType logType, UnityEngine.Object context, System.String format, System.Object[] args) [0x0000b] in <1332c534a8a44623b2e5cc943a0b790e>:0 
  at UnityEngine.Logger.Log (UnityEngine.LogType logType, System.Object message) [0x00027] in <1332c534a8a44623b2e5cc943a0b790e>:0 
  at UnityEngine.Debug.Log (System.Object message) [0x00006] in <1332c534a8a44623b2e5cc943a0b790e>:0 
  at EditorAssetData.caculateTop () [0x00123] in D:\[Works]\sanguo_release\Unity\Assets\Scripts\Editor\AssetBundleTool\EditorAssetData.cs:184 
  at EditorAssetData.caculateTop () [0x0013a] in D:\[Works]\sanguo_release\Unity\Assets\Scripts\Editor\AssetBundleTool\EditorAssetData.cs:185 
  at EditorAssetData.caculateTop () [0x0013a] in D:\[Works]\sanguo_release\Unity\Assets\Scripts\Editor\AssetBundleTool\EditorAssetData.cs:185 
//余下省略========================================================
//余下省略========================================================
//余下省略========================================================

查询代码可知为递归获取上级引用问题。

经过调试可发现出现问题者,皆为名称与本身不一致的对象。

如图所示:

出现问题的原因,多为美术 (特效),原因想来很简单,美术直接单纯复制对象,未做正确操作。

由此可以得出结论,老版本 Unity 可能直接有一套兼容机制处理,新版 Unity 直接崩掉了。

已解决,通过检测递归层数,人为限制,数量超出上限则打印错误信息,以便修复:

private int _stackOverflowCheck = 0;
    /// <summary>
    /// 一直向上递归,直到上级资源的上级资源数为 0 或上级资源为 BuildSingle
    /// </summary>
    public List<EditorAssetData> caculateTop()
    {
        _stackOverflowCheck++;
        if (_stackOverflowCheck > 50)
        {
            Debug.LogError(LoadPath+ " 超出递归限制上限,可能已经出现了问题!");
            return TopUpDependenceList;
        }

效果:

# 六、报 load IL2CPP 失败

通过 ADB 拉取的日志查看,一堆 Could not load symbol XXX 的错误

经过对 Unity 导出资源的目录的对比,发现新版资源路径都已经不一样了,多了为两个目录 『Il2CppOutputProject』、『jniStaticLibs』, 且 jniLibs 目录中少了 libil2cpp.so


经过查询信息表示,新版 Unity 导出的 AndroidStudio 工程,不再编译 libil2cpp.so ,而是直接提供源码,得自己处理编译的功能。

打开导出的资源 unityLibrary 目录下的 build.gradle,其中就有编译的代码:

// 省略

将其复制到我们的 SDK 下 build.gradle 中对应位置,并依照我们项目作出部分修改:

android {
	//已有代码
	//省略
	//省略

    aaptOptions {
        noCompress = ['.unity3d', '.ress', '.resource', '.obb', '.bundle', '.unityexp','.assetbundle']
    }

    packagingOptions {
        doNotStrip '*/armeabi-v7a/*.so'
        doNotStrip '*/arm64-v8a/*.so'
    }

    task BuildIl2CppTask {
        doLast {
            BuildIl2Cpp(project(':UnitySdk').projectDir.toString().replaceAll('\\\\', '/'), 'Release', 'armv7', 'armeabi-v7a', [  ] as String[]);
            BuildIl2Cpp(project(':UnitySdk').projectDir.toString().replaceAll('\\\\', '/'), 'Release', 'arm64', 'arm64-v8a', [  ] as String[]);
        }
    }
    afterEvaluate {
        if (project(':UnitySdk').tasks.findByName('mergeDebugJniLibFolders'))
            project(':UnitySdk').mergeDebugJniLibFolders.dependsOn BuildIl2CppTask
        if (project(':UnitySdk').tasks.findByName('mergeReleaseJniLibFolders'))
            project(':UnitySdk').mergeReleaseJniLibFolders.dependsOn BuildIl2CppTask
    }
    sourceSets {
        main {
            jni.srcDirs = ["src/main/Il2CppOutputProject"]
        }
    }
}

def getSdkDir() {
    Properties local = new Properties()
    local.load(new FileInputStream("${rootDir}/local.properties"))
    return local.getProperty('sdk.dir')
}

def BuildIl2Cpp(String workingDir, String configuration, String architecture, String abi, String[] staticLibraries) {
    def commandLineArgs = []
    commandLineArgs.add("--compile-cpp")
    commandLineArgs.add("--platform=Android")
    commandLineArgs.add("--architecture=" + architecture)
    commandLineArgs.add("--outputpath=" + workingDir + "/src/main/jniLibs/" + abi + "/libil2cpp.so")
    commandLineArgs.add("--libil2cpp-static")
    commandLineArgs.add("--baselib-directory=" + workingDir + "/src/main/jniStaticLibs/" + abi)
    commandLineArgs.add("--incremental-g-c-time-slice=3")
    commandLineArgs.add("--configuration=" + configuration)
    commandLineArgs.add("--dotnetprofile=unityaot-linux")
    commandLineArgs.add("--profiler-report")
    commandLineArgs.add("--profiler-output-file=" + workingDir + "/build/il2cpp_"+ abi + "_" + configuration + "/il2cpp_conv.traceevents")
    commandLineArgs.add("--print-command-line")
    commandLineArgs.add("--generatedcppdir=" + workingDir + "/src/main/Il2CppOutputProject/Source/il2cppOutput")
    commandLineArgs.add("--cachedirectory=" + workingDir + "/build/il2cpp_"+ abi + "_" + configuration + "/il2cpp_cache")
    commandLineArgs.add("--tool-chain-path=" + android.ndkDirectory)
    staticLibraries.eachWithIndex {fileName, i->
        commandLineArgs.add("--additional-libraries=" + workingDir + "/src/main/jniStaticLibs/" + abi + "/" + fileName)
    }
    def executableExtension = ""
    if (org.gradle.internal.os.OperatingSystem.current().isWindows())
        executableExtension = ".exe"
    exec {
        executable workingDir + "/src/main/Il2CppOutputProject/IL2CPP/build/deploy/il2cpp" + executableExtension
        args commandLineArgs
        environment "ANDROID_SDK_ROOT", getSdkDir()
    }
    delete workingDir + "/src/main/jniLibs/" + abi + "/libil2cpp.sym.so"
    ant.move(file: workingDir + "/src/main/jniLibs/" + abi + "/libil2cpp.dbg.so", tofile: workingDir + "/symbols/" + abi + "/libil2cpp.so")
}

重新执行 AS 打包操作 —— 发现 jniLibs 根本没有生成对应的 libil2cpp.so 文件,导致最终出来的包还是有问题。

经过与 SDK 同学一块检查,发现我这边是出包的时候根本没执行 BuildIl2CppTask 代码:

也就是说,就算如果增加了上述代码,要是按照正常流程打包,BuildIl2CppTask 会不执行,后边经过研究,只能手动执行 BuildIl2CppTask,手动执行输出也是正常的 (经过与 SDK 同学讨论,暂时也没发现为何没有自动执行的原因)。

这一步尝试成功后,就出了新包测试,确认无误后 —— 如此第一个能跑的包终于出来了!

# 无法正常调用 BuildIl2Cpp 编译 libil2cpp.so

上文提到了直接出包,AS 根本没有执行附加的 BuildIl2CppTask 这条任务

这一步操作肯定不可能手动执行的,而且有时候涉及到旧资源出包,就算能够成功执行,反复编译也是费时间的麻烦事

于是向 SDK 同学要了命令『./gradlew BuildIl2CppTask』,修改了打包工具,直接在向 AS 复制资源之前,执行一次 il2cpp 的检查及编译的操作,并将生成的 libil2cpp.so 复制回来作资源缓存:

//===== 编译 il2cpp======
string packil2CppSources = Path.Combine(packResRootPath, "Il2CppOutputProject");
string packjniStaticLibsPath = Path.Combine(packResRootPath, "jniStaticLibs");
string sdkil2CppSources = Path.Combine(sdkMainPath, "Il2CppOutputProject");
string sdkjniStaticLibsPath = Path.Combine(sdkMainPath, "jniStaticLibs");
//libil2cpp.so 文件
string packIl2cppv8Files = Path.Combine(packJniPath, "arm64-v8a/libil2cpp.so");
string packIl2cppv7Files = Path.Combine(packJniPath, "armeabi-v7a/libil2cpp.so");
string sdkIl2cppv8Files = Path.Combine(sdkJniPath, "arm64-v8a/libil2cpp.so");
string sdkIl2cppv7Files = Path.Combine(sdkJniPath, "armeabi-v7a/libil2cpp.so");
if (File.Exists(sdkIl2cppv8Files)) File.Delete(sdkIl2cppv8Files);
if (File.Exists(sdkIl2cppv7Files)) File.Delete(sdkIl2cppv7Files);
// 不存在,编译
if (!File.Exists(packIl2cppv8Files) || !File.Exists(packIl2cppv7Files))
{
    // 清理可能存在的旧文件
    if (Directory.Exists(sdkil2CppSources)) Directory.Delete(sdkil2CppSources, true);
    if (Directory.Exists(sdkjniStaticLibsPath)) Directory.Delete(sdkjniStaticLibsPath, true);
    // 复制 il2cpp 源码过去
    FileCompare.Copy(packil2CppSources, sdkil2CppSources);
    FileCompare.Copy(packjniStaticLibsPath, sdkjniStaticLibsPath);
    // 正式编译
    string rootSdkDir = Path.GetDirectoryName(Info.AndroidSDKPath);
    // 刷新
    DoCmd(rootSdkDir, "gradlew", "");
    Thread.Sleep(1000);
    // 编译
    DoCmd(rootSdkDir, "gradlew", "BuildIl2CppTask");
    Thread.Sleep(1000);
    // 复制回来,避免第二次复用时打包重复编译
    if (File.Exists(sdkIl2cppv8Files)) FileCompare.Copy(sdkIl2cppv8Files, packIl2cppv8Files);
    if (File.Exists(sdkIl2cppv7Files)) FileCompare.Copy(sdkIl2cppv7Files, packIl2cppv7Files);
}
//================

经测试新流程可以跑通。

注:此处调用的 AS 命令其实也是调用外部进程编译,所以不走 AS ,直接新开一个控制台命令也是可以的,这样还可以省下复制源码至 AS 的消耗,后边可以考虑优化。

# 七、安卓加载 StreamingAsset 目录资源失败

查询官方文档看到如下说明:

Unity 会将放置在 Unity 项目中名为 StreamingAssets__(区分大小写)的文件夹中的所有文件逐字复制到目标计算机上的特定文件夹。要获取此文件夹,请使用 Application.streamingAssetsPath 属性。在任何情况下,最好使用 Application.streamingAssetsPath 来获取 StreamingAssets__ 文件夹的位置,因为它总是指向运行应用程序的平台上的正确位置。

Application.streamingAssetsPath 返回的位置因平台而异:

  • 大多数平台(Unity Editor、Windows、Linux 播放器、PS4、Xbox One、Switch)使用 Application.dataPath + "/StreamingAssets"。
  • macOS 播放器使用 Application.dataPath + "/Resources/Data/StreamingAssets"。
  • iOS 使用 Application.dataPath + "/Raw"。
  • Android 使用经过压缩的 APK/JAR 文件中的文件:"jar:file://" + Application.dataPath + "!/assets"。

其中安卓的路径,与日志输出并不一致,怀疑是否此处引起了差异,于是查询我们项目内相关代码,发现对获取 StreamingAssets 有如下组合:

  • FResourceCommon GetStreamingAssetsPath 方法
  • LauncherLoading GetStreamingAssetsPath 方法

原初始化 streamingAssetsPath 路径的代码,判断为安卓没有直接取系统 Application.streamingAssetsPath,而且自己组合的路径,新版 Unity 可能不再对此作兼容性判断,导致无法再使用。

if (Application.platform == RuntimePlatform.Android)
{
    STREAMING_ASSET_PATH = Application.dataPath + "!assets";   // 安卓平台
}
else
{
    STREAMING_ASSET_PATH = Application.streamingAssetsPath;  // 其他平台
}

将其修改为直接取 Application.streamingAssetsPath 后,加载正常。

STREAMING_ASSET_PATH = Application.streamingAssetsPath;  // 新版本所有平台均使用 streamingAssetsPath

# 八、无法调试问题

将 DLL 设置中,高级生成设置 -> 调试信息 设置为可移植,并删除项目中 DLL 已有的 MDB 文件,重新编译。

# 九、Google 包黑屏问题

表现为闪屏后黑屏,且有 Debug 且存在部分 Unity 输出

增加打印并检查日志:

从日志中及对应代码推测,可以推测主要有两个问题:

  • # 打 Google 包 UseOBB 选项未生效

其中涉及代码如下:

Debug.Log("Launcher Awake + "+ UseOBB);
#if UNITY_ANDROID && !UNITY_EDITOR
        SDKManager.Inst.SendMsg(SDKManager.SEND_HAVE_CUTOUT);
        if(UseOBB)
        {
            Debug.Log("Launcher Awake + UseOBB");
            InitGoogleOBBResources(ShowUI);
            return;
        }                
#elif UNITY_IPHONE && !UNITY_EDITOR
        SDKManager.Inst.AddEvent(eSDKMngEvent.GetDynamicUpdateStateSuccess, OnGetDynamicUpdateStateSuccess);
        SDKManager.Inst.SendMsg(SDKManager.SEND_GET_DYNAMIC_UPDATE_NO);
        return;
#endif

在场景的 Launcher 上,就有 UseOBB 这个选项,用于控制我们游戏内是否能够正确识别为 Google 包

增加打印后,此处开始为 false(注:上述日志截图为解决后的表现,因此显示为 true

这个 UseOBB 选项,之前是在出 Google 包时,通过代码直接设置的。

但是经过个人反复测试,发现新版 Unity 在代码设置场景物体属性后,该项不会正常生效!

最简单的重现方式为:代码修改场景物体属性后,重启 Unity,物体属性被还原 (即使修改后调用保存场景及资源的接口,依然无效)。

解决方式为:将出包时自动设置 UseOBB 属性处修改为一个判断,若出包类型为 Google OBB 模式,且 UseOBB 未勾选,弹出提示需要手动勾选保存。

  • # OBB 无法被主应用正确识别

还是根据上述日志分析,看着像是没有正确识别 OBB,导致资源未能成功加载而报空。

在之前我们 Unity2017 出包的时候,导出的 AS 项目资源中,其中 AndroidManifest.xml 文件存在一个 build-id 的字段。

不过新版 Unity 导出资源中,AndroidManifest.xml 已经没有了,所以开始是直接打的包。

2021 升级文档: Upgrading to Unity 2021 LTS

其中有这么一个描述:

Changed how Unity checks to see if an obb is compatible with an apk
. Both the apk and obb now have unity_obb_guid file inside them and if the contents match between them, Unity treats them as being compatible.

意思是现在 Unity 通过 APK 及 OBB 包中的一个名为 unity_obb_guid 文件来确认 APK 与 OBB 是否一致。

也就是说,从 Unity2021 开始,OBB 识别机制不走 build-id ,而是走另外文件对比了。

解包 OBB 后,果然发现了这个东西:

同时检查打包出来的 APK,发现缺失,通过搜索 Unity 导出资源目录,可发现 APK 中该文件位于 unityLibrary\src\main\assets\unity_obb_guid

在之前我们的打包工具中,对 assets 目录的内容只复制其子目录,导致缺了文件,修改打包工具代码:

assetPaths = Directory.GetDirectories(packAssetsPath);
for (int i = 0; i < assetPaths.Length; i++)
{
    FileCompare.Copy(assetPaths[i], (sdkAssetsPath + assetPaths[i].Replace(packAssetsPath, "")));
}
// 复制 unity_obb_guid 文件
string packObbGuidFile = Path.Combine(packAssetsPath, "unity_obb_guid");
string sdkObbGuidFile = Path.Combine(sdkAssetsPath, "unity_obb_guid");
FileCompare.Copy(packObbGuidFile, sdkObbGuidFile);

这时候果然就可以了:

# 十、进出战斗崩溃

最初表现为打开引导时,概率发生于进入战斗时崩溃。

第一次查询的崩溃日志如下:

Received signal SIGSEGV
Obtained 50 stack frames
0x00007ff7b5c0aceb (Unity) GameObject::SendMessageAny
0x00007ff7b607e835 (Unity) Transform::BroadcastMessageAny
0x00007ff7b607e899 (Unity) Transform::BroadcastMessageAny
0x00007ff7b607e899 (Unity) Transform::BroadcastMessageAny
0x00007ff7b688d7f6 (Unity) UI::Canvas::WillDestroyComponent
0x00007ff7b5c0d3a1 (Unity) GameObject::WillDestroyGameObject
0x00007ff7b5f18901 (Unity) PreDestroyRecursive
0x00007ff7b5f16927 (Unity) DestroyObjectHighLevel_Internal
0x00007ff7b5f16564 (Unity) DestroyObjectHighLevel
0x00007ff7b626638f (Unity) Scripting::DestroyObjectFromScriptingImmediate
0x00007ff7b54dd663 (Unity) Object_CUSTOM_DestroyImmediate
0x0000018b373dcb35 (Mono JIT Code) (wrapper managed-to-native) UnityEngine.Object:DestroyImmediate (UnityEngine.Object,bool)
0x0000018b373dca53 (Mono JIT Code) UnityEngine.Object:DestroyImmediate (UnityEngine.Object)
0x0000018c1010bae3 (Mono JIT Code) YoukiaUnity.Resource.PoolManager:CleanPool (string)
0x0000018c1219f07b (Mono JIT Code) [BattleStage.cs:85] Demo.Stage.BattleStage:CleanPool () 
0x0000018c100fabab (Mono JIT Code) [BattleStage.cs:79] Demo.Stage.BattleStage:OnAsyncHandle () 
0x0000018c1219efbb (Mono JIT Code) [BattleStage.cs:116] Demo.Stage.BattleStage:<OnSceneLoaded>b__10_0 () 
0x0000018c1219ef76 (Mono JIT Code) [BattleManager.cs:1798] BattleManager:_onCreateFinish () 
0x0000018b3af83c05 (Mono JIT Code) YoukiaCore.AsyncCombiner:RefreshAsyncHandles ()
0x0000018b3af8d2c3 (Mono JIT Code) YoukiaCore.AsyncCombiner/AsyncHandle:Finish ()
0x0000018c1011fbdb (Mono JIT Code) [BattleManager.cs:2004] BattleManager/<>c__DisplayClass139_0:<_onRoleAdd>b__0 () 
0x0000018c1219dd26 (Mono JIT Code) [ModelChiefView.cs:147] Demo.ModelChiefView/<>c__DisplayClass10_0:<Create>b__2 () 
0x0000018c1219db21 (Mono JIT Code) [ModelChiefView.cs:193] Demo.ModelChiefView/<>c__DisplayClass13_0:<AddMountSkillClip>b__0 (object) 
0x0000018b3af41b1c (Mono JIT Code) FrameWork.FProcess:allDone ()
0x0000018b3af41573 (Mono JIT Code) FrameWork.FProcess:AssetLoaded (object[])
0x0000018b3af40fff (Mono JIT Code) FrameWork.FGameEvent:DispatchEvent (System.Enum,object[])
0x0000018b3af40e43 (Mono JIT Code) FrameWork.FEventManager:DispatchEvent (System.Enum,object[])
0x0000018b3af40d33 (Mono JIT Code) FrameWork.FProcess:ProcessComplete ()
0x0000018b3af3a9ab (Mono JIT Code) FrameWork.FProcess/<_load>d__18:MoveNext ()
0x0000018b3862cff0 (Mono JIT Code) UnityEngine.SetupCoroutine:InvokeMoveNext (System.Collections.IEnumerator,intptr)
0x0000018b3862d11f (Mono JIT Code) (wrapper runtime-invoke) <Module>:runtime_invoke_void_object_intptr (object,intptr,intptr,intptr)
0x00007ffdf5e6e4b4 (mono-2.0-bdwgc) [mini-runtime.c:3445] mono_jit_runtime_invoke 
0x00007ffdf5dae764 (mono-2.0-bdwgc) [object.c:3066] do_runtime_invoke 
0x00007ffdf5dae8fc (mono-2.0-bdwgc) [object.c:3113] mono_runtime_invoke 
0x00007ff7b626dac4 (Unity) scripting_method_invoke
0x00007ff7b6268644 (Unity) ScriptingInvocation::Invoke
0x00007ff7b621a1df (Unity) Coroutine::Run
0x00007ff7b6217c1c (Unity) Coroutine::ContinueCoroutine
0x00007ff7b5d35344 (Unity) DelayedCallManager::Update
0x00007ff7b5f45d29 (Unity) `InitPlayerLoopCallbacks'::`2'::UpdateScriptRunDelayedDynamicFrameRateRegistrator::Forward
0x00007ff7b5f2c89c (Unity) ExecutePlayerLoop
0x00007ff7b5f2c973 (Unity) ExecutePlayerLoop
0x00007ff7b5f325d9 (Unity) PlayerLoop
0x00007ff7b6e7993f (Unity) PlayerLoopController::UpdateScene
0x00007ff7b6e77bdf (Unity) Application::TickTimer
0x00007ff7b72c37aa (Unity) MainMessageLoop
0x00007ff7b72c805b (Unity) WinMain
0x00007ff7b864b42e (Unity) __scrt_common_main_seh
0x00007ffe3a797034 (KERNEL32) BaseThreadInitThunk
0x00007ffe3b5c2651 (ntdll) RtlUserThreadStart

对比代码发现,我们游戏在进入战斗时,会强制清理一次对象池,而清理方式则是直接调用 GameObject.DestroyImmediate

GameObject.DestroyImmediate 和 GameObject.Destroy 一直是有区别的,记得很早以前官方都推荐正式环境用 Destroy 而非 DestroyImmediate。

为了确认看了下文档:

立即销毁对象 /obj/。强烈建议您改用 Destroy。

该函数应只在编写 Editor 代码时使用,因为在编辑模式下, 永远不会调用延迟销毁。 在游戏代码中,您应该改用 Object.Destroy。Destroy 始终延迟进行 (但在同一帧内执行)。 使用该函数时要务必小心,因为它可以永久销毁资源! 另请注意,切勿循环访问数组并销毁正在迭代的元素。这会导致严重的问题(这是一条通用的编程实践,而不仅仅是在 Unity 中)。

说明 DestroyImmediate 肯定是有一定风险的,特别是 CleanPool 中的代码,还正好是一个循环调用的 DestroyImmediate。

于是怀疑是否这里的影响,尝试将其修改为 Destroy。

如此之后 ———— 还是崩溃了,日志变成如下:

Asset Pipeline Refresh: Total: 0.027 seconds - Initiated by RefreshV2(AllowForceSynchronousImport)
Received signal SIGSEGV
Obtained 22 stack frames
0x00007ff7b5c0aceb (Unity) GameObject::SendMessageAny
0x00007ff7b607e835 (Unity) Transform::BroadcastMessageAny
0x00007ff7b607e899 (Unity) Transform::BroadcastMessageAny
0x00007ff7b607e899 (Unity) Transform::BroadcastMessageAny
0x00007ff7b688d7f6 (Unity) UI::Canvas::WillDestroyComponent
0x00007ff7b5c0d3a1 (Unity) GameObject::WillDestroyGameObject
0x00007ff7b5f18901 (Unity) PreDestroyRecursive
0x00007ff7b5f16927 (Unity) DestroyObjectHighLevel_Internal
0x00007ff7b5f16564 (Unity) DestroyObjectHighLevel
0x00007ff7b5d3345b (Unity) DelayedDestroyCallback
0x00007ff7b5d35344 (Unity) DelayedCallManager::Update
0x00007ff7b5f457b9 (Unity) `InitPlayerLoopCallbacks'::`2'::PostLateUpdateScriptRunDelayedDynamicFrameRateRegistrator::Forward
0x00007ff7b5f2c89c (Unity) ExecutePlayerLoop
0x00007ff7b5f2c973 (Unity) ExecutePlayerLoop
0x00007ff7b5f325d9 (Unity) PlayerLoop
0x00007ff7b6e7993f (Unity) PlayerLoopController::UpdateScene
0x00007ff7b6e77bdf (Unity) Application::TickTimer
0x00007ff7b72c37aa (Unity) MainMessageLoop
0x00007ff7b72c805b (Unity) WinMain
0x00007ff7b864b42e (Unity) __scrt_common_main_seh
0x00007ffe3a797034 (KERNEL32) BaseThreadInitThunk
0x00007ffe3b5c2651 (ntdll) RtlUserThreadStart

由于这次属于延时销毁,所以不再有实际调用销毁的堆栈打印,不过最终走到的崩溃位置还是一样: GameObject::SendMessageAny

在此处增加对应销毁物体打印,只看到一个 TS_MainButtomView

<color=lightblue>1660365391</color> Debug:销毁:TS_MainButtomView 

造成崩溃对象可能就是这个 Prefab,在我们项目是一个 UI 组件。

并且后续经过尝试,若不销毁该对象,同样有可能崩溃,且会发生于退出战斗时也可能崩溃。

这时候基本上已经相当怀疑这个对象了,只是由于每次碰到的崩溃最终堆栈信息都还不尽相同,所以无法定性,常见有:

  • 销毁
  • 创建
  • 刷新精灵 Sprite

    tlsf_free
    DynamicHeapAllocator::Deallocate

正准备试试官方文档介绍的 Debug 方式 的时候,后续测试同学竟还找到了一个稳定复现之处,再经个人反复尝试和查询,最终得到如下指向比较清晰一点日志:

Received signal SIGSEGV
Obtained 51 stack frames
0x00007ff7e8a5f2a4 (Unity) UI::CanvasManager::AddCanvas
0x00007ff7e8a73633 (Unity) UI::Canvas::AddToManager
0x00007ff7e8a73c65 (Unity) UI::Canvas::AwakeFromLoad
0x00007ff7e8512fe7 (Unity) AwakeFromLoadQueue::InvokeAwakeFromLoad
0x00007ff7e850e003 (Unity) AwakeFromLoadQueue::AwakeFromLoadAllQueues
0x00007ff7e7df417a (Unity) GameObject::ActivateAwakeRecursively
0x00007ff7e7dfb719 (Unity) GameObject::SetSelfActive
0x00007ff7e767fd9b (Unity) GameObject_CUSTOM_SetActive
0x00000126e405c104 (Mono JIT Code) (wrapper managed-to-native) UnityEngine.GameObject:SetActive (UnityEngine.GameObject,bool)
0x00000126f84444fb (Mono JIT Code) [MainButtomView.cs:1442] MainButtomView/<>c__DisplayClass115_0:<OpenView>b__0 (object) 

崩溃结果指向,界面的子节点挂载的 Canvas,该对象 (TS_MainButtomView) 属于主界面 Aorpage 的一个 子节点 Prefab,且自身挂载了 Canvas。

这里经过尝试,表明该组件并非是必须的。

于是移除之后就真的好了..

# 十一、重打图集混乱

例如,更改图集大小,最明显的一个错误表现如下:

如果现在点击提交,发现 meta 文件都未被修改:

而且编辑器中也可以明显看出 Position 依然是旧的 —— 经过分析调试,在编辑器工具生成新的图集数据时,确实也是赋值了新的数据的:

于怀疑是不是 Unity 内部又出啥事了。

经过查询关键字 textureImporter.spritesheet,有找到一篇可能有所关联的博客 unity 2D Sprite 网格 Slice 工具

这篇文章描述了 Unity 的 textureImporter.spritesheet 大约存在一个 BUG,当图集中图片未被修改,仅发生位置表动时,将导致其数据无法正常更新。虽然这篇博文已经早是 2015 年的东西了,不过本着尝试一下的感觉,没成想真是这个问题!

末尾给加上

textureImporter.mipmapEnabled = !textureImporter.mipmapEnabled;
textureImporter.SaveAndReimport();
textureImporter.mipmapEnabled = false;
textureImporter.SaveAndReimport();

就正常了,关键是这个在之前 Unity2017 还能正常工作的。

# 十二、正式包

其它比较简单一点、或者一些功能上的的升级问题,这里就不作记录了,最终正式包如下: