概览

为提高代码质量、提升整体研发效率,这里汇总了客户端常见的异常情况,从写法、日志、异常表现、代码编写规范等几个方面进行分析,帮助大家提升调试能力和代码质量。

 

 

1. 空引用

NullReferenceException

UnassignedReferenceException

MissingReferenceException

1.1 访问空对象的属性或方法

NullReferenceException: Object reference not set to an instance of an object

eg:

        Debug.Log("test begin");

        UIPageBase page = null;

        page.Dispose();

        Debug.Log("test over");

 

报错后函数不再执行,“test over”未输出

 

1.2 访问空的序列化对象

编辑器下:UnassignedReferenceException: The variable Go_SearchPage of UIDebugOverseasPanel has not been assigned.

包内:NullReferenceException

eg:

        Debug.Log("test begin");

        Go_SearchPage.SetActive(true);

        Debug.Log("test over");

 

 

报错后函数中断,“test over”未输出

注:真机日志中往往没有行号!

 

1.3 访问已被销毁的节点

eg A:btnSkip30Days销毁前有赋值

编辑器下:MissingReferenceException: The object of type 'Button' has been destroyed but you are still trying to access it.

包内:NullReferenceException

 

        Debug.Log("test begin");

        Destroy(gameObject);

        await UniTask.Delay(1000);

        btnSkip30Days.gameObject.SetActive(true);

        Debug.Log("test over");

 

 

报错后函数不再执行,“test over”未输出

 

eg B:动态加载的节点被销毁后,表现同上

 

        Debug.Log("test begin");

        GameObject go = await LoadPrefabAndInstantiateAsync("xxx",transform);

        Debug.Log($"1st go == null {go == null}");

        Destroy(go);

        await UniTask.Delay(2000);

        Debug.Log($"2nd go == null {go == null}");

        go.SetActive(false);

        Debug.Log("test over");

 

2023-06-28 16:35:40.067 8489-8558/? I/Unity: 1st go == null False

2023-06-28 16:35:42.074 8489-8558/? I/Unity: 2nd go == null True

2023-06-28 16:35:42.093 8489-8558/? E/Unity: NullReferenceException: Object reference not set to an instance of an object.

 

1.4#节点销毁后mono回调

注:MonoBehaviour中重载了运算符==与!=

因此部分逻辑的表现比较特殊

参考文献:

After the underlying component is destroyed, the C# object for the MonoBehaviour remains in memory until garbage is collected. A MonoBehaviour in this state acts as if it is null. For example, it returns true for a "obj == null" check. However, this class doesn't support the null-conditional operator (?.) and the null-coalescing operator (??).

https://docs.unity3d.com/cn/current/ScriptReference/MonoBehaviour.html

Unity-自定义==运算符,我们应该保留它吗?Custom == operator, should we keep it?_unity 自定义运算符_Ryuu-64的博客-CSDN博客

eg A:

        Debug.Log("test begin");

        Destroy(gameObject);

        await UniTask.Delay(1000);

        Debug.Log($"this == null {this == null}");

        await LogOff();

        Debug.Log("test over");

 

编辑器、包内均可通过测试,无报错

this == null值为真,但其成员函数LogOff仍可执行

 

2023-06-28 16:33:06.291 8322-8391/? I/Unity: test begin

2023-06-28 16:33:07.313 8322-8391/? I/Unity: this == null True

2023-06-28 16:33:09.591 8322-8391/? I/Unity: test over

 

eg B:

编辑器下:MissingReferenceException: The object of type 'UIDebugOverseasPanel' has been destroyed but you are still trying to access it.

包内:NullReferenceException

 

        Debug.Log("test begin");

        Destroy(gameObject);

        await UniTask.Delay(1000);

        Debug.Log($"this == null {this == null}");

        Debug.Log($"this.gameObject == null {this.gameObject == null}");

        Debug.Log("test over");

 

 

报错后函数不再执行,“test over”未输出

总结:Mono代码判定其所属节点是否已被销毁,用this==null判定,不可用this.gameObject == null 或 gameObject == null

 

疑似有问题的代码:TODO

 

 

 

1.5 ?.语法相关

未继承Unity组件的一般对象可用?.语法做判空逻辑

GameObject/Component 不可使用?.语法*(情况复杂,尤其不建议使用!!)

 

eg A: 新建的GameObject被销毁后不可用?.      

        Debug.Log("test start");

        GameObject go = await LoadPrefabAndInstantiateAsync("xxx",transform);

        Debug.Log($"1st go == null {go == null}");

        Destroy(go);

        await UniTask.Delay(2000);

        Debug.Log($"2nd go == null {go == null}");

        go?.SetActive(false);

        Debug.Log("test over");

 

eg B: 序列化(未赋值)的GameObject不可用?.

        Debug.Log($"test start Go_SearchPage {Go_SearchPage == null}");

        Go_SearchPage?.SetActive(false);

        Debug.Log("test over");

 

eg C:一般对象(不继承Unity组件)可正确使用?.

        UIPageBase page = null;

        page?.Dispose();

eg D:编辑器、真机:序列化的组件(未赋值)在界面销毁前可以使用?.

        Debug.Log($"test start btnTrebleMode {btnTrebleMode == null}");

        btnTrebleMode?.gameObject?.SetActive(false);

        Debug.Log("test over");

eg E:编辑器、真机:序列化的组件(有赋值)在界面销毁后不可使用?.

        Destroy(gameObject);

        await UniTask.Delay(1000);

        Debug.Log($"test start 2157 {btnSkip30Days == null}");

        btnSkip30Days?.gameObject?.SetActive(false);

        Debug.Log("test over");

 

 

eg F:编辑器下:序列化的组件(未赋值)在界面销毁后可以使用?.

真机下:序列化的组件(未赋值)在界面销毁后不可以使用?.

        Destroy(gameObject);

        await UniTask.Delay(1000);

        Debug.Log($"test start 2158{btnTrebleMode == null}");

        btnTrebleMode?.gameObject?.SetActive(false);

        Debug.Log("test over");

 

 

总结:GameObject、Component等Unity相关类型不要用?.语法!!

 

1.6 代码编写规范

MonoBehaviour内判定自身节点是否已销毁:if (this == null)

Unity相关对象一律不使用?.语法

 

 

2. 数组越界

ArgumentOutOfRangeException

IndexOutOfRangeException

2.1 Array/List为空的情况

报空异常,不会报数组越界

        Debug.Log("start");

        string[] arr = null;

        string str = arr[1];

        Debug.Log("end");

 

        Debug.Log("start");

        List<string> arr = null;

        string str = arr[1];

        Debug.Log("end");

 

2.2 一般数组越界的情况

Array越界报IndexOutOfRangeException

List越界报ArgumentOutOfRangeException

        Debug.Log("start");

        string[] arr = { "A" };

        string str = arr[1];

        Debug.Log("end");

 

        Debug.Log("start");

        List<string> arr = new List<string>{ "A" };

        string str = arr[1];

        Debug.Log("end");

 

2.3 代码编写规范

写代码前(尤其关键逻辑代码)加上完善的判空保护,并且在异常逻辑内打印详细信息

        if (arr != null && arr.Length > index)// 正常逻辑

        {

            string str = arr[index];

        }

        else// 异常逻辑,完整打印

        {

            Log.Error($"UIDebugOverseasPanel.ResetGuestStatus

 arr?.Length = {arr?.Length} index = {index}");//猜猜看???

        }

进一步优化

        if (arr?.Length > index)

 

 

2.4 空对象的输出表现

空对象输出空字符串,空对象不可ToString

        string[] arr = null;

        Log.Error($"UIDebugOverseasPanel.ToggleTrebleMode arr = {null}");

        Log.Error($"UIDebugOverseasPanel.ToggleTrebleMode arr = {arr.ToString()}");

 

 

2.5 测试代码

        string[] arr0 = null;

        string[] arr1 = { "A" };

        string[] arr2 = { "A","B" };

        List<string> list0 = null;

        List<string> list1 = new List<string> { "A" };

        List<string> list2 = new List<string> { "A" ,"B" };

        bool bArr0Greater = arr0?.Length > 1;

        bool bArr0Less = arr0?.Length <= 1;

        bool bArr1Greater = arr1?.Length > 1;

        bool bArr1Less = arr1?.Length <= 1;

        bool bArr2Greater = arr2?.Length > 1;

        bool bArr2Less = arr2?.Length <= 1;

        bool bList0Greater = list0?.Count > 1;

        bool bList0Less = list0?.Count <= 1;

        bool bList1Greater = list1?.Count > 1;

        bool bList1Less = list1?.Count <= 1;

        bool bList2Greater = list2?.Count > 1;

        bool bList2Less = list2?.Count <= 1;

        Debug.Log($"bArr0Greater {bArr0Greater}");

        Debug.Log($"bArr0Less {bArr0Less}");

        Debug.Log($"bArr1Greater {bArr1Greater}");

        Debug.Log($"bArr1Less {bArr1Less}");

        Debug.Log($"bArr2Greater {bArr2Greater}");

        Debug.Log($"bArr2Less {bArr2Less}");

        Debug.Log($"bList0Greater {bList0Greater}");

        Debug.Log($"bList0Less {bList0Less}");

        Debug.Log($"bList1Greater {bList1Greater}");

        Debug.Log($"bList1Less {bList1Less}");

        Debug.Log($"bList2Greater {bList2Greater}");

        Debug.Log($"bList2Less {bList2Less}");

 

 

 

3. 字典越界

KeyNotFoundException

3.1 典型案例

2023-05-16 14:28:10.492 31566-31751/com.dgames.g65002002.onestore E/Unity: KeyNotFoundException: The given key 'month' was not present in the dictionary.

      at System.Collections.Generic.Dictionary`2[TKey,TValue].get_Item (TKey key) [0x00000] in <00000000000000000000000000000000>:0 

      at Script.Game.UI.Home.Shop.ShopScreenItem.LoadMonthlyPassData () [0x00000] in <00000000000000000000000000000000>:0 

 

        private async UniTask LoadMonthlyPassData()

        {

            var master = DataManager.Instance.Master.ShopProductMaster[masterData.shopProductMasterId];

            var maxIndex = DataManager.Instance.Master.GlobalConfig[TakashoUtil.GlobalConfigID.PlayerMonthlyPassLimit].data;

            var product = LCXHandler.Instance.mProducts[master.lcxProductId];

        }

 

函数中包含多个字典,难以第一时间判断,哪个字典越界

 

3.2 一般情况

        Debug.Log("start");

        Dictionary<string, string> dic = new Dictionary<string, string>();

        string key = "month";

        string rtn = dic[key];

        Debug.Log("end");

 

 

3.3 代码编写规范

ContainsKey

eg:

        if (memoryData != null && memoryData.ContainsKey(filename))

        {

            // 正常逻辑

        }

       else

{

//

}

 

TryGetValue

eg:

        Master TipCharacter = null;                      

        Master.TryGetValue(curTip.characterId, out TipCharacter);

 

 

 

4. AB加载异常

4.1 典型案例A

2022-10-27 17:19:22.057 4251-4356/? E/Unity: The file 'archive:/CAB-04009564394466edb019f0d0069c91ac/CAB-04009564394466edb019f0d0069c91ac' is corrupted! Remove it and launch unity again!

    [Position out of bounds!]

 

4.1.1 排查方法

1.使用unity工具查出CAB-04009564394466edb019f0d0069c91ac对应的ab文件是哪一个

参考文档https://www.jianshu.com/p/b656ae521357

 

 

2.以上操作批处理找到问题ab

2.1: 批量解压*.unity3d文件

find /Users/haibo.du/Downloads/apks/cbt/Build-0.2.57-57-WW-LQA-BUILD_DEBUG-2022_10_25_16_01_22/assets/ab -name "*.unity3d" -type f -exec /Applications/Unity/Hub/Editor/2021.2.0f1/Unity.app/Contents/Tools/WebExtract {} \;

2.2: 找出对应文件所在的路径

find . -name 'CAB-04009564394466edb019f0d0069c91ac'

排查结论:

 

4.1.2 代码编写规范

以下代码,非常危险,仅手机包内才会暴露问题,严令禁止

#if UNITY_EDITOR

public GameObject textobj;

[SerializeField] private Text tips;

#endif

#if OVERSEAS

public GameObject textobj;

[SerializeField] private Text tips;

#endif

#if BUILD_DEBUG

public GameObject textobj;

[SerializeField] private Text tips;

#endif

 

 

4.2 典型案例B

The AssetBundle 'xxx' could not be loaded because t references scripts that are not compatible with the currently loaded ones. Rebuild the AssetBundle to fix this error.

包体热更时,若仅更新预制体,则预制体的序列化属性必须与出包时一致,包括顺序

 

 

 

4.3 典型案例C

E/Unity: Unable to open archive file: /data/app/com.dgames.g65002002.google-GNzeX7jNbzNr97YbQPJpDg==/base.apk!assets/ab/reslocalize/ja/message.unity3d

典型文件缺失的情况,可排查目录中是否存在该文件

 

4.4 结论

  1. 1.   序列化脚本建议不要在宏中定义变量(尤其是在prefab中赋值节点的)
  2. 2.   热更版本需要保持所有脚本序列化信息与出包时一致

 

 

 

5. 除零

5.1 除零错误表现:进程崩溃

 

 

信号

描述

SIGSEGV

内存引用无效。

SIGBUS

访问内存对象的未定义部分。

SIGFPE

算术运算错误,除以零。

SIGILL

非法指令,如执行垃圾或特权指令

SIGSYS

糟糕的系统调用

SIGXCPU

超过CPU时间限制。

SIGXFSZ

文件大小限制。

5.2 代码编写规范

做除法之前,检验除数是否等于0

 

 

 

6. 内存不足

6.1 典型案例(iOS)

内存不足引发进程崩溃,相关日志:

kernel    EXC_RESOURCE -> score[20659] exceeded mem limit: ActiveHard 2098 MB (fatal)

kernel    187669.572 memorystatus: killing_specific_process pid 20659 [score] (per-process-limit 100) 2150134KB - memorystatus_available_pages: 27037

崩溃前可能会触发引擎消息Application.lowMemory

 

 

 

7. 报错中断后的表现

7.1 一般逻辑异常

阻断后续逻辑

7.2 生命周期异常

Awake报错后,Start无法执行,Update无法执行,同一帧Awake报错不会先后影响

Start报错后,Update可正常执行,同一帧Start报错不会先后影响

Update报错后,下一帧仍可正常执行,且对其他脚本Update无影响

 

 

7.3 崩溃异常

除零、内存不足等异常,在设备上可能导致应用崩溃(闪退)

崩溃日志内包含fatal/FATAL字样

 

 

 

附录

 

参考文献:

Exceptions - C# language specification | Microsoft Learn

Unity-自定义==运算符,我们应该保留它吗?Custom == operator, should we keep it?_unity 自定义运算符_Ryuu-64的博客-CSDN博客