概览
为提高代码质量、提升整体研发效率,这里汇总了客户端常见的异常情况,从写法、日志、异常表现、代码编写规范等几个方面进行分析,帮助大家提升调试能力和代码质量。
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. 序列化脚本建议不要在宏中定义变量(尤其是在prefab中赋值节点的)
- 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博客