Home

Awesome

Unity异常处理与分析

引言

对于程序开发者来说,异常可以理解为坏消息,为了避免异常我们需要良好的结构设计,完备的逻辑编写以及规范的代码格式。但是异常总会不和事宜的出现,针对可能异常的处理和异常的收集也至关重要。

此教程就针对异常处理,统计和分析进行展开讲解, 因为异常的内容知识非常庞大,特别是针对Android和iOS平台本身的Native异常,对于Native异常两个平台有大量的资料可供查阅学习,本教程给出了扩展链接,可以按需阅读。

核心要点

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 ExceptionAndroid 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的原因有以下几种:

  1. so库内部代码数组越界、缓冲区溢出、空指针、野指针等;
  2. Android ART发现或出现异常;
  3. 其他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

r0 77878008 r1 745a8008 r2 0000ac50 r3 00320057 ...

#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

上面省略了其他线程的信息

0 libsystem_kernel.dylib 0x00000001b92c8c04 mach_msg_trap + 8

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堆栈:

符号表

简单来说,符号表是内存地址与函数名,文件名,行号的映射表

<起始地址> <结束地址> <函数> [<文件名:行号>]

符号表相关的知识参考: Bugly官方手册 - Android符号表iOS符号表

解析符号表

拿到符号表,并且确认UIID一致的情况下,我们就可以进行工具进行堆栈符号解析了。

adb shell logcat | ndk-stack -sym $PROJECT_PATH/obj/local/armeabi
更多内容请翻阅: Android Native crash日志分析

Unity上报平台的选择

主流上报平台的对比分析

Unity平台的异常上报通常有四家较为主流的平台,

这里对各个平台的特点梳理了一个表格,集成后可以参阅(标绿的部分是明显优势项,表格中仅拿Android项目进行了测试统计):

根据咱们公司风控要求,Bugly限制国内平台使用Firebase限制全平台使用,所以产品的集成建议以下组合方式:

国内产品推荐:Bugly, Unity

海外产品推荐:Appcenter, Unity

搭建自己的上报平台

前面介绍了第三方Crash或者Unity平台,假如我们需要上报到自己的平台,需要分几个方面进行处理,这些可以单独进行集成:

  1. Exception 捕获
  2. 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:

  1. 恰当的处理异常:异常的信息能够发现具体的异常原因异常发生位置
  2. 上报异常之外的其他信息,进行辅助判断,因为很多的native异常位置指向性并不能定位原因。

恰当的处理异常

正确使用try-catch-finially语句处理异常。

另外更多使用规则请参阅 Best practices for exceptions

throw 关键字使用

注意: 当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();
        }
    }
}
方便确定异常发生的位置

il2cpp目前并不记录异常代码的行号,这对定位产生了一定的困难。以下几个点可以让我们更加方便确定异常的位置:

上报关键信息

我们可以通过反馈入口或者异常捕获拿拿到线上用户的现场信息,为了方便定位Crash,应当尽量提供一下现场信息

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,