Awesome
Unity异常处理与分析
引言
对于程序开发者来说,异常可以理解为坏消息,为了避免异常我们需要良好的结构设计,完备的逻辑编写以及规范的代码格式。但是异常总会不和事宜的出现,针对可能异常的处理和异常的收集也至关重要。
此教程就针对异常处理,统计和分析进行展开讲解, 因为异常的内容知识非常庞大,特别是针对Android和iOS平台本身的Native异常,对于Native异常两个平台有大量的资料可供查阅学习,本教程给出了扩展链接,可以按需阅读。
核心要点
- Unity exception的介绍和异常处理
- Native crash的介绍
- 上报平台Unity/Appcenter/Firebase/Bugly对比分析
- 处理异常的一般原则和注意事项
- 上报事所必要的信息
Unity Excepction
默认情况下C#代码下Exception并不会造成进程杀死,这样的情况如果异常产生后会中断当前执行的代码逻辑,可能会造成一系列意外的结果。iOS平台上Unity提供了Fast but no Exceptions
选项保证所有Exception触发Crash。对于Android平台,Unity并没有这样的选项,但是我们可以通过全局异常捕获接口获得Exception,然后强制终止/重启程序或者弹出异常弹窗让用户选择终止或重。下面我们会从种类、构建、处理三个方面介绍异常。
###异常种类
Unity中Excepction都是派生于 System.Exception
,下面是我们开发过程常见的Exception
具体参阅:
Handling and throwing exceptions in .NET
构建异常
通过 throw
关键字可以抛出自定义Exception,三个构造方法可选择:
public Exception();
public Exception(string message);
public Exception(string message, Exception innerException);
建议:
尽可能详尽的附加message
信息
innerException
可以附加捕获的Exception,可用于保留异常第一现场的调用栈
异常处理
局部处理
使用try-catch-finally 语句。
如果使用了Task
处理异步任务 ,还可以使用 ContinueWith
获得Task运行期间的Exception,运行线程和Task相同。
Task.Run(() => { throw new CustomException("task1 faulted.");})
.ContinueWith( t => { Console.WriteLine("{0}: {1}",
t.Exception.InnerException.GetType().Name,
t.Exception.InnerException.Message);
}, TaskContinuationOptions.OnlyOnFaulted);
全局处理
Unity 本身提供了LogCallback
监听程序Log和异常
/// <summary>
/// <para>Use this delegate type with Application.logMessageReceived or Application.logMessageReceivedThreaded to monitor what gets logged.</para>
/// </summary>
public delegate void LogCallback(string condition, string stackTrace, LogType type);
其中 LogType
是
public enum LogType
{
Error,
Assert,
Warning,
Log,
Exception,
}
看到LogType
中有 Exception
,然后我们通过Application.logMessageReceivedThreaded
注册接口,就可以获得C#代码中抛出的所有Exception了。
Native Crash
前面我们介绍了Unity Exception,除了Unity托管代码本身产生的异常之外,运行平台也会有对应的异常,平台托管代码中产生的异常我们是Native Crash。
当然Native Crash也是跟随平台特性的不同也各有差异,具体到Android平台可以分为 Java Exception
和 Android native crash
两类,还有iOS平台的 iOS native crash
Android Java Exception:
发生在Android 虚拟机层面的异常,我们通过Java全局的异常捕获类Thread.UncaughtExceptionHandler
拿到各个线程抛出的Exception
例1-Java Exception
java.lang.ArithmeticException: divide by zero
at com.lbj.mvpflower.mvp.ui.activity.UserActivity.onUser(UserActivity.java:36)
at java.lang.reflect.Method.invoke(Native Method)
at android.view.View$DeclaredOnClickListener.onClick(View.java:4702)
at android.view.View.performClick(View.java:5619)
at android.view.View$PerformClick.run(View.java:22298)
at android.os.Handler.handleCallback(Handler.java:754)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:165)
at android.app.ActivityThread.main(ActivityThread.java:6365)
更多详情:
Android Native Crash:
简单理解Android的native crash就是发生在so库中异常,所以这些异常实际上就是Linux系统 常见导致Native Crash的原因有以下几种:
- so库内部代码数组越界、缓冲区溢出、空指针、野指针等;
- Android ART发现或出现异常;
- 其他framework、Kernel或硬件bug;
例2-Android Crash
E/CRASH (32251): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 745a8008
E/CRASH (32251): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
E/CRASH (32251): Build type 'Release', Scripting Backend 'il2cpp', CPU 'armeabi-v7a'
E/CRASH (32251): Build fingerprint: 'Lenovo/A7600-F/A7600-F:4.4.2/KOT49H/A7600F_A442_000_027_141207_ROW:user/release-keys'
E/CRASH (32251): Revision: '0'
E/CRASH (32251): pid: 32251, tid: 10190, name: Flurry #82 >>> my.package.demo <<<
E/CRASH (32251): r0 77878008 r1 745a8008 r2 0000ac50 r3 00320057
E/CRASH (32251): r4 00310035 r5 00470062 r6 00730077 r7 006d0049
E/CRASH (32251): r8 00300068 r9 00000000 sl 6999eb88 fp 0002bc70
E/CRASH (32251): ip 00480064 sp 793f7a80 lr 007a0042 pc 400bd480 cpsr 793f7790
E/CRASH (32251):
E/CRASH (32251): backtrace:
E/CRASH (32251): #00 pc 00026480 /system/lib/libc.so (__memcpy_base_aligned+52)
E/CRASH (32251): #01 pc 00020bf9 /system/lib/libbinder.so (android::Parcel::appendFrom(android::Parcel const*, unsigned int, unsigned int)+136)
E/CRASH (32251): #02 pc 0006cb9b /system/lib/libandroid_runtime.so
E/CRASH (32251): #03 pc 0001e90c /system/lib/libdvm.so (dvmPlatformInvoke+112)
E/CRASH (32251): #04 pc 0004fbbd /system/lib/libdvm.so (dvmCallJNIMethod(unsigned int const*, JValue*, Method const*, Thread*)+484)
E/CRASH (32251): #05 pc 00027ce8 /system/lib/libdvm.so
E/CRASH (32251): #06 pc 0002f2f0 /system/lib/libdvm.so (dvmMterpStd(Thread*)+76)
E/CRASH (32251): #07 pc 0002c7d4 /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*)+188)
E/CRASH (32251): #08 pc 00062ef9 /system/lib/libdvm.so (dvmCallMethodV(Thread*, Method const*, Object*, bool, JValue*, std::__va_list)+340)
E/CRASH (32251): #09 pc 00062f1d /system/lib/libdvm.so (dvmCallMethod(Thread*, Method const*, Object*, JValue*, ...)+20)
E/CRASH (32251): #10 pc 000575c5 /system/lib/libdvm.so
- 信号名:signal 11 (SIGSEGV)
- 信号产生的原因:code 1 (SEGV_MAPERR)
- 异常的内存地址:fault addr 745a8008(如果是SIGSEGV/SIGBUS等信号,一般都会有异常内存地址显示)
- 编译类型:il2cpp
- ABI信息:armeabi-v7a
- 设备型号:Lenovo
- Android系统版本:4.4.2
- 系统的build号:A7600FA442000027141207_ROW
- 系统的类型:user(对应有user/eng/userdebug/optional等,user表示是release版本,eng是调试版本)
- Crash的进程号:32251
- Crash的线程号:10190
- Crash的线程名称:Flurry #82(名称可能被裁减导致不全)
- Crash的进程名称:my.package.demo
- 中止消息:无(如果是SIGABRT,可能有类似javavmext.cc:504] JNI DETECTED ERROR IN APPLICATION: java_array == null这样的消息)
- 下面是 寄存器指向的内存地址:
r0 77878008 r1 745a8008 r2 0000ac50 r3 00320057 ...
- 最后Native调用栈(栈帧序号-pc地址值-虚拟内存映射区域名称-(对应符号-符号偏移量):
#00 pc 00026480 /system/lib/libc.so (__memcpy_base_aligned+52) ...
更多详情:
iOS Native Crash
例3-iOS Crash
Incident Identifier: D9857968-7AF7-40C0-BA05-192EF275F380
Hardware Model: iPhone11,8
Process: demo [4801]
Path: /private/var/containers/Bundle/Application/03D4FE4F-26BC-47E5-A1D9-B8FF1D379B4A/demo.app/demo
Identifier: my.bundleid.demo
Version: 2 (1.0)
AppStoreTools: 11E608a
AppVariant: 1:iPhone11,8:13
Beta: YES
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: my.bundleid.demo [1515]
Date/Time: 2020-07-13 08:28:25.9201 +0700
Launch Time: 2020-07-13 08:27:57.4775 +0700
OS Version: iPhone OS 13.3 (17C54)
Release Type: User
Baseband Version: 2.03.07
Report Version: 104
Exception Type: EXC_BAD_ACCESS (SIGBUS)
Exception Subtype: KERN_PROTECTION_FAILURE at 0x000000011b747ff0
VM Region Info: 0x11b747ff0 is in 0x11b734000-0x11b748000; bytes after start: 81904 bytes before end: 15
REGION TYPE START - END [ VSIZE] PRT/MAX SHRMOD REGION DETAIL
MALLOC_LARGE 000000011b730000-000000011b734000 [ 16K] rw-/rwx SM=PRV
---> CG raster data 000000011b734000-000000011b748000 [ 80K] r--/r-- SM=PRV
MALLOC_LARGE 000000011b748000-000000011b74c000 [ 16K] rw-/rwx SM=PRV
Termination Signal: Bus error: 10
Termination Reason: Namespace SIGNAL, Code 0xa
Terminating Process: exc handler [4801]
Triggered by Thread: 38
Thread 0 name:
Thread 0:
0 libsystem_kernel.dylib 0x00000001b92c8c04 mach_msg_trap + 8
1 libsystem_kernel.dylib 0x00000001b92c8020 mach_msg + 76 (mach_msg.c:103)
2 CoreFoundation 0x00000001b947b964 __CFRunLoopServiceMachPort + 220 (CFRunLoop.c:2575)
3 CoreFoundation 0x00000001b94767fc __CFRunLoopRun + 1428 (CFRunLoop.c:2931)
4 CoreFoundation 0x00000001b9475f40 CFRunLoopRunSpecific + 480 (CFRunLoop.c:3192)
5 GraphicsServices 0x00000001c36f3534 GSEventRunModal + 108 (GSEvent.c:2246)
6 UIKitCore 0x00000001bd5eea60 UIApplicationMain + 1940 (UIApplication.m:4773)
7 UnityFramework 0x000000010281a330 -[UnityFramework runUIApplicationMainWithArgc:argv:] + 108 (main.mm:96)
8 demo 0x0000000100eb7e1c main + 68 (main.mm:26)
9 libdyld.dylib 0x00000001b92f4e18 start + 4
Thread 1:
0 libsystem_pthread.dylib 0x00000001b92119e0 start_wqthread + 0
Thread 2:
0 libsystem_pthread.dylib 0x00000001b92119e0 start_wqthread + 0
····
Thread 38 Crashed:
0 libsystem_platform.dylib 0x00000001b9206688 _platform_memmove + 408
1 UnityFramework 0x000000010334f840 il2cpp::icalls::mscorlib::System::Buffer::BlockCopyInternal(Il2CppArray*, int, Il2CppArray*, int, int) + 176
2 UnityFramework 0x0000000103aaee4c StreamWriter_Write_m8056BDE8A4AD4816F9D7DBDBCB80D03BE8F3ED14 + 192 (mscorlib9.cpp:21738)
3 UnityFramework 0x0000000103ab22f4 UnexceptionalStreamWriter_Write_mAC310C8D24F673608DC7F130666E572ADD388E07 + 92 (mscorlib9.cpp:24996)
4 UnityFramework 0x0000000103729968 Logger_Log_mC3BD9B8C40067382718BD9A54688A92599404E6B + 240 (SmartFox2X2.cpp:32959)
5 UnityFramework 0x00000001036f8548 UDPManager_OnUDPData_mCD9304C4A4BD03D6F85EFAD9512893D4B2D037DC + 396 (SmartFox2X1.cpp:19687)
6 UnityFramework 0x0000000103712ec0 OnDataDelegate_Invoke_mF1F76E2DCD9F8028B8956AE76035840B21615920 + 488 (SmartFox2X1.cpp:0)
7 UnityFramework 0x00000001037175fc ThreadManager_InThread_m70E3D0A1EBE6F3AF67DFE1E8D50F54463DA49D10 + 180 (SmartFox2X2.cpp:18993)
8 UnityFramework 0x00000001039fcb98 ThreadStart_Invoke_m11B6A66E82F02C74399A7314C14C7F52393CC4B4 + 284 (ClassInlines.h:0)
9 UnityFramework 0x00000001039d0940 ContextCallback_Invoke_m76E65E7A67AA99858554F451E0F78E22B6478998 + 440 (ClassInlines.h:0)
10 UnityFramework 0x00000001039d1bf8 ExecutionContext_RunInternal_m5BF955CE8B04D9A7C959B9011579CC0FABD5FC56 + 316 (mscorlib19.cpp:30738)
11 UnityFramework 0x00000001039fcb98 ThreadStart_Invoke_m11B6A66E82F02C74399A7314C14C7F52393CC4B4 + 284 (ClassInlines.h:0)
12 UnityFramework 0x000000010286b620 RuntimeInvoker_TrueVoid_t22962CB4C05B1D89B55A6E1139F0E87A90987017(void (*)(), MethodInfo const*, void*, void**) + 20 (Il2CppInvokerTable.cpp:18279)
13 UnityFramework 0x00000001033925c8 il2cpp::vm::Runtime::Invoke(MethodInfo const*, void*, void**, Il2CppException**) + 116
14 UnityFramework 0x000000010335c678 il2cpp::icalls::mscorlib::System::Threading::ThreadStart(void*) + 108
15 UnityFramework 0x0000000103365a7c il2cpp::os::Thread::RunWrapper(void*) + 88
16 UnityFramework 0x0000000103367d14 il2cpp::os::ThreadImpl::ThreadStartWrapper(void*) + 40
17 libsystem_pthread.dylib 0x00000001b9209840 _pthread_start + 168 (pthread.c:896)
18 libsystem_pthread.dylib 0x00000001b92119f4 thread_start + 8
····
Binary Images:
0x100eb0000 - 0x100eb7fff demo arm64 <14cde28b305d3bffb8abdfab5a30df25> /var/containers/Bundle/Application/03D4FE4F-26BC-47E5-A1D9-B8FF1D379B4A/demo.app/demo
0x100f80000 - 0x100f8bfff libobjc-trampolines.dylib arm64e <028df612175838e498958fd8c6a9a03c> /usr/lib/libobjc-trampolines.dylib
上面省略了其他线程的信息
- Incident Identifier : Crash唯一ID
- Hardware Model: 设备型号
- Process: 可执行文件名
- Identifier :Bundle ID
- Code Type : CPU架构
- OS Version: 系统版本号号
- Report Version :崩溃日志版本号
- Exception Type - Termination Reason: 是异常的类型和原因
- Triggered by Thread: 38 : 引发的线程
- Thread 0: 主线程
- 崩溃发生的堆栈 (位置, 地址, 函数符号+ 偏移量):
0 libsystem_kernel.dylib 0x00000001b92c8c04 mach_msg_trap + 8
- Binary Images: 二进制文件内存信息 ( 地址起始-地址结束 名称 CPU架构 <UIID> 路径)
0x100eb0000 - 0x100eb7fff demo arm64 <14cde28b305d3bffb8abdfab5a30df25> /var/containers/Bundle/Application/03D4FE4F-26BC-47E5-A1D9-B8FF1D379B4A/demo.app/demo
更多详情:
对于Debug环境中我们拿到的Crash日志通常都是带有符号的,也就是我们可以追踪到具体的crash方法和行号。如果Crash堆栈没有解析符号表,这种情况就需要我们手动解析符号表了,当然很多Crash平台都集成了解析符号表的功能,我们只需要自己上传即可。下面我们就介绍下符号表相关的知识。
解析符号表
为了能快速并准确地定位用户APP发生Crash的代码位置,我们使用符号表对APP发生Crash的程序堆栈进行解析和还原, 比如一个iOS的crash堆栈:
符号表
简单来说,符号表是内存地址与函数名,文件名,行号的映射表
<起始地址> <结束地址> <函数> [<文件名:行号>]
-
对于Android工程,一般情况下我们采用 il2cpp 方式构建,unity-2018.4会把符号表自动打包成一个压缩包文件。后缀
.symbols.zip
, 存放位置和.apk
文件在同一目录。 -
iOS系统的下的符号表就是dSYM文件,文件名通常为:xxx.app.dSYM。我们使用xcode编译完项目后,会生产dSYM文件,和app文件在同一个目录下
符号表相关的知识参考: Bugly官方手册 - Android符号表,iOS符号表
解析符号表
拿到符号表,并且确认UIID一致的情况下,我们就可以进行工具进行堆栈符号解析了。
- Android平台下可以使用NDK库中
add2line
ndk-stack
工具,ndk-stack
可以对Android的Crash-Log进行提取和解析,非常方便在拿到Crashlog的情况下分析。 我们通过以下命名可以获取实时的Crash信息
adb shell logcat | ndk-stack -sym $PROJECT_PATH/obj/local/armeabi
更多内容请翻阅: Android Native crash日志分析
-
iOS平台下两种工具
symbolicatecrash
和atos
,symbolicatecrash
其实就是对atos
的封装 更多内容请翻阅: OS Crash分析必备:符号化系统库方法
Unity上报平台的选择
主流上报平台的对比分析
Unity平台的异常上报通常有四家较为主流的平台,
这里对各个平台的特点梳理了一个表格,集成后可以参阅(标绿的部分是明显优势项,表格中仅拿Android项目进行了测试统计):
根据咱们公司风控要求,Bugly限制国内平台使用,Firebase限制全平台使用,所以产品的集成建议以下组合方式:
国内产品推荐:Bugly, Unity
海外产品推荐:Appcenter, Unity
搭建自己的上报平台
前面介绍了第三方Crash或者Unity平台,假如我们需要上报到自己的平台,需要分几个方面进行处理,这些可以单独进行集成:
- Exception 捕获
- Native Crash, 包括Android和iOS平台
全局异常处理
前面已经讲过 Application.logMessageReceivedThreaded
注册接口可以获取全局的Exception,所以Unity托段代码的异常获得还是非常简单的,实现对应的接口方法即可监听。
iOS 异常
iOS平台提供了捕获异常的接口:
/* Set the uncaught exception handler */
NSSetUncaughtExceptionHandler(&uncaught_exception_handler);
如果同时使用了注册了第三方的Crash平台, 需要先保存第三方的 ExceptionHandler,然后在设置自己处理 exception 的 ExceptionHandler,在自己的 ExceptionHandler 处理完异常之后,再将异常交给第三方处理。
如果需要集成服务端解析符号表,请参考: 有赞 crash 平台符号化实践
Android Java 异常
Android平台也提供了处理Java异常的全局接口 Thread.UncaughtExceptionHandler
, 我们实现此接口通过
Thread.setDefaultUncaughtExceptionHandler
注册给系统即可。同样iOS平台类似,应为此接口值接受一个Handler,需要先通过 Thread.getDefaultUncaughtExceptionHandler
获取之前的Handler,再根据需要将捕获的Exception进行转交。
Android Native 异常
Android 平台可以使用 google breakpad 捕获native crash(TODO: 集成方案:Sentry), Appcenter平台就是采用的该方案。
异常处理和上报
当我们了解了所有的异常类型,也选择了合适的上报平台,最后就可以进行异常上报了。通常第三方都会主动收集异常信息,AppCenter
这样的平台甚至会主动上报Error的log, Bugly
的Android平台还是收集 logcat日志信息。 除了平台做的这些事情我们还需要注意两件非常重要的事情,来快速定位Crash:
- 恰当的处理异常:异常的信息能够发现具体的异常原因 和 异常发生位置
- 上报异常之外的其他信息,进行辅助判断,因为很多的native异常位置指向性并不能定位原因。
恰当的处理异常
正确使用try-catch-finially语句处理异常。
try
的作用域尽量单一,方便错误定位。catch
中做恢复资源,如果不能正确处理请上报事件或debug下输出错误,最后或者是重新throw。finally
中清理资源,可以不catch
Exception,仅使用try-finally做资源闭环处理。
另外更多使用规则请参阅 Best practices for exceptions
throw 关键字使用
throw [e];
抛出一个异常,调用栈是当前调用方法。
注意: 当try-catch中 抛出一个新的异常,捕获的异常的调用栈并不是真正错误来源。如果需要保留catch的Exception,需要使用下面的构建方法:
public Exception(string message, Exception innerException);
一个常见的用法:
public class ProcessFile
{
public static void Main()
{
FileStream fs;
try
{
fs = new FileStream("data.txt", FileMode.Open);
var sr = new StreamReader(fs);
// ....
}
catch(FileNotFoundException e)
{
Console.WriteLine($"[Data File Missing] {e}");
throw new FileNotFoundException(@"[data.txt not in c:\temp directory]", e);
}
finally
{
if (fs != null)
fs.Close();
}
}
}
throw;
重新抛出try-catch的异常 ,通常可作为代码异常的监听,但不做拦截
方便确定异常发生的位置
il2cpp目前并不记录异常代码的行号,这对定位产生了一定的困难。以下几个点可以让我们更加方便确定异常的位置:
-
编写职责单一的方法,方法行数不易过多。
-
异常捕获时提供额外信息。
-
准确记录堆栈路径,通常是自己构建Exception时注意保留Catch的Exception堆栈。
上报关键信息
我们可以通过反馈入口或者异常捕获拿拿到线上用户的现场信息,为了方便定位Crash,应当尽量提供一下现场信息
- 设备信息: 包括设备基本类型,系统版本,系统语言,定义的UIID, 内存情况,存储情况(需要单独实现Native接口)等
SystemInfo.deviceModel,
SystemInfo.deviceName,
SystemInfo.deviceType,
SystemInfo.deviceUniqueIdentifier,
SystemInfo.operatingSystem,
Localization.language,
SystemInfo.systemMemorySize,
SystemInfo.processorCount,
SystemInfo.processorType,
Screen.currentResolution.width,
Screen.currentResolution.height,
Screen.dpi,
Screen.fullScreen,
SystemInfo.graphicsDeviceName,
SystemInfo.graphicsDeviceVendor,
SystemInfo.graphicsMemorySize,
SystemInfo.graphicsPixelFillrate,
SystemInfo.maxTextureSize,
- 应用信息:运行时间,异常时所在界面,用户操作的动作链等
- Trace事件:为了方便业务分析,我们在开发过程会上报事件用户埋点分析,除此之外我们可以提供Trace事件类型,主要来统计模块加载和错误输出,发生异常是可以根据场景附加上多个事件集合。
- 日志文件:在可能的情况下,附加当前应用的日志文件,AppCenter提供了附加文件的接口,我们可以将日志文件一并上传。
- 反馈接口:用户提供额外的文字描述和截屏。