Crash Report,这在大型软件开发领域是很常见的功能,就是能够当程序崩溃退出后,能够将崩溃时的信息,最好是携带dmp文件发送给服务器,这样开发人员既可以获得分发出去的客户端的崩溃率统计,也可以针对出现的错误进行及时的纠正,之前在PC的端游时代,这是很常见的做法,最近进行了在手游上的关于crash report的相关研究,并且为项目编写了一个相对完善的CrashReport模块。
这个模块的来源于手游项目正式上线,但是很多玩家反馈闪退,但是我们只能听到反馈闪退,却不能找到原因,只能凭脑袋去猜,是不是内存不够,机器配置太差,然后去尽可能优化性能,于是老大开始喊我们需要一个Crash Report, 于是就花2个星期完善了一个可以正式使用的Crash Repoter,项目基于Unity3D,在Android和Ios上做crash report 对我还是第一次,所以还是抱有了极大的兴趣。
1 Android 平台。
其实CrashReport也不应该是只有Crash了才Report,各种错误和潜在会导致Crash的问题也应该report上去。对于基于Unity3D的Android应用来说,自底到上可以分为三层:C++,Java和C#。 android是基于linux的系统,最底下的各种so a库就是C++的部分,android系统本身的相关逻辑则是java,U3D则使用了C#开发逻辑,所以我们采集问题也要从这三块分别着手。
C#
c#的错误很好处理,这层U3D完全封装好了,C#层会出现warning error和exception,在android下这几种情况都不会导致crash,都会被UNITY3d接住,但是我们需要知道并报告给服务器,U3D有接口Application.RegisterLogCallback(),可以让C#层发生上面的问题时被我知道,我们只要写这个callback,然后在里面给服务器就行了。
Java
java层的错误就是各种java exception,对于java,如果对于我们catch了的exception,不会导致crash,会按照我们的catch行为执行,对于那些我们没有catch的exception,是会crash的,还会在adblog上打印出来,我们需要获知这些exception,我们可以采用java中的接口Thread.setDefaultUncaughtExceptionHandler来重新设置这个对未catch的exeption的处理,在我们自己的handler中基本做的事情就是首先把这个exceptio报告给服务器,然后并不让程序退出,让程序尽可能活下去。
C++
C++中出现的问题通常就是很严重的了,这里也分两种,一种是普通的一些异常,这取决于你是否catch了,如果没有catch,默认就是abort的,也就是crash了,还有一些比如对内存的非法访问,就直接在linux中产生了一个结束信号,把进程结束了,也是crash。对于C++我们先后尝试了两种方案,第一种就是采用捕获linux的信号量,程序异常退出总是有信号的,可以使用linux 下的sigaction来设置对这些信号的捕获处理,比如我们捕获了SIGILL SIGABRT SIGFPE SIGSEGV SIGPIPE SIGBUS SIGSTKFLT,这样对于异常的程序退出我们是知道的,可以在下次进入游戏时告知服务器,但是这样做有一个明显的问题就是我们只是知道程序crash了,但是没有trace back,不知道在哪挂了,我们想要dump文件。于是后来采取的方法就是使用了google的breakpad框架,关于google breakpad,这是它的主页,https://chromium.googlesource.com/breakpad/breakpad/,关于他的基本原理,大家可以去看他的wiki和文档,很长,基本来说它是一个平台无关的C++的crash reporter,可以在crash后,生成dmp文件,然后利用它的一些工具获取堆栈的符号信息。
google breakpad在android的简单集成方法如下:
1.从http://google-breakpad.googlecode.com/svn/trunk拿到源码
2.建立你自己的jni工程
3.将google breakpad的android 和src两个文件夹放到你的工程里
4.配置你的Application.mk,里面要加入
APP_STL := stlport_static
APP_CPPFLAGS := -std=gnu++11 -D__STDC_LIMIT_MACROS
5.配置你的Android.mk,里面要加入以下的src文件
google_breakpad/src/client/linux/crash_generation/crash_generation_client.cc \
google_breakpad/src/client/linux/handler/exception_handler.cc \
google_breakpad/src/client/linux/handler/minidump_descriptor.cc \
google_breakpad/src/client/linux/log/log.cc \
google_breakpad/src/client/linux/dump_writer_common/thread_info.cc \
google_breakpad/src/client/linux/dump_writer_common/ucontext_reader.cc \
google_breakpad/src/client/linux/microdump_writer/microdump_writer.cc \
google_breakpad/src/client/linux/minidump_writer/linux_dumper.cc \
google_breakpad/src/client/linux/minidump_writer/linux_ptrace_dumper.cc \
google_breakpad/src/client/linux/minidump_writer/minidump_writer.cc \
google_breakpad/src/client/minidump_file_writer.cc \
google_breakpad/src/common/android/breakpad_getcontext.S \
google_breakpad/src/common/convert_UTF.c \
google_breakpad/src/common/md5.cc \
google_breakpad/src/common/string_conversion.cc \
google_breakpad/src/common/linux/elfutils.cc \
google_breakpad/src/common/linux/file_id.cc \
google_breakpad/src/common/linux/guid_creator.cc \
google_breakpad/src/common/linux/linux_libc_support.cc \
google_breakpad/src/common/linux/memory_mapped_file.cc \
google_breakpad/src/common/linux/safe_readlink.cc \
还要加入LOCAL_STATIC_LIBRARIES += breakpad_client
以及include google_breakpad/android/google_breakpad/Android.mk
6.在你的crashreport模块初始化中(当然通常也可以在JNI_OnLoad中)初始化google breakpad,
google_breakpad::MinidumpDescriptor descriptor(path);
handler = new google_breakpad::ExceptionHandler(descriptor, NULL, NULL, NULL, true, -1);
这里的path是你手机上存放dmp文件的文件夹,crash发生后,它会在这个文件夹内生成以UUID命名的dmp文件,当然前提你要保证这个文件夹真实存在。
7.最后编出你的so库,给程序使用。
这样我们就通过google breakpad实现了C++层的dump文件生成,当发生后,我们把dump文件传给服务器就行了。
最后关于这个dmp文件的解读,这里大家可以参考这份文档:https://www.chromium.org/developers/decoding-crash-dumps。这个解析要在linux环境下做,没有win的工具,基本就是两步骤,第一步是用工具将dmp的二进制文件转成可以看懂的文本格式,可以看到出错的地址,但是如果要想详细知道这些地址所代表的符号,还需要用里面的一个工具以及带有符号版本的so库才能知道,unity自己的库应该没有这种so库,但是也能大致看出问题处在哪了。
其他
似乎到这里我们能够堵到android上所有可能崩溃的地方,然而并不是。至少有两种情况是还不行的,一是watch dog超时,二是内存资源不足。
watch dog超时:当你的主线程超过一段时间没有相应,android系统会将你的程序退出,内存资源不足:当android系统认为这个程序使用的内存过高时,会选择将这个退出,以释放内存。
这两种情况都是android系统的管理器按照一定的策略调度的,虽然玩家看到闪退了,但是这两种情况逻辑上都不属于异常退出,和你自己退出android进程是一样的,只不过是系统帮你退出了,所以用google breakpad或者信号都不能知道,因为这其实是正常退出,但是对于我们程序设计来说,这是异常。所以从捕获异常退出来说没有办法(当然也许真的有,我不知道,那欢迎大家批评指正),所以对这两种情况我们退而求其次使用当感知到有潜在的退出危险时报告给服务器警告的策略。
对于watch dog 超时,我们在java层开一个新的线程,不断的去探测主线程,当较长时间发现主线程没回应,我们给一个警告给服务器,并带上现在的内存情况,告知服务器这台机器主线程卡住很久了,很可能一会就被系统退出了,但是也可能运气好一会又好了。这种探测的方法我们可以正好用unity的在native层的UnitySendMessage机制,因为这个就是异步的,我们用另一个线程不断的给主线程的Unity用这个发送心跳包,unity收到后回复,很久没回复就是主线程卡住了(原因多了,比如某个逻辑特别特别耗时。。。)
对于内存太高,我们会在程序里定期检测一下内存,当发现使用的内存明显高过我们的设计时发给服务器,比如说我们认为PSS内存超过600M,当然如果有机器每到这个就崩了,那也不是我们的目标机型。在android系统下动态得到系统的总内和当前可用内存可以用activityManager.getMemoryInfo(),获取当前的进程的使用内存的接口可以用activityManager.getProcessMemoryInfo(new int[]{Process.myPid()})
通过这两种策略我们可以预防式的得到这两种情况的一个crash统计。
IOS
IOS只有C++和C#两层,对于C#来说,和Android是一模一样的,不用多说。
对于C++这层,我们当然还是可以继续使用google breakpad,因为它是跨平台的,但是其实Unity(至少从4.6开始)为ios已经提供了一个crash report模块,他需要我们将工程生成好后,将Crashreport.h里面的ENABLE_CUSTOM_CRASH_REPORTER设置为1,或者你可以直接在unity安装路径下找到这个文件直接改。这样在c++ crash后,unity会为我们生存crash文件,等下次启动后,可以通过crashreport这个模块访问这些dmp文件,这些dmp文件都是ios上的标准dmp文件,可以使用ios的开发工具symbolicatecrash来查看。unity内置的crash report其实也是采用了第三方的库plcrashreport来实现的,这个库在ios上的应用很多。
另外对于ios,其实也提供了一个函数NSSetUncaughtExceptionHandler ,用来当那些未捕捉的异常发生时,进入这个处理,可以拦截一些东西,但是一些比如内存访问错误直接就退出了,不会进到这里,另外进到这里之后程序还是会退出,只是让我们可以记录一下,不过有了unity自带的crashreport 这个也就没啥用了。
还有在unity对ios的c#运行中,在player setting里面有个对代码的运行优化,选择slow but safe 还是fast but no exception,如果选择前者,所有c#的执行错误都会像脚本一样被catch住,会报c#的exception,如果后者就不会,就直接不catch了,会造成程序退出,但是运行效率是高的,我们通常选择slow but safe。这是mono架构的特性。
通过对ios 和android的崩溃的采集和报告,有助于了解我们程序在用户手中的稳定性和及时改进。