当前位置: 首页 > 科技观察

iOSApp连续崩溃时如何上报crashlog

时间:2023-03-13 17:12:46 科技观察

为了保证在线App的用户体验,我们一般会实时监控在线App的崩溃率。一旦检测到尖峰,就可以立即排查原因,但是这一切的前提是能够准确上报crashlog。报告崩溃日志有两个难点:安装崩溃处理程序之前的代码必须绝对稳定。如果日志采集器还没有启动成功就崩溃了,自然就采集不到日志了。除了严格限制在处理程序启动之前可以执行的代码之外,这没有太多技巧。当应用程序崩溃时报告崩溃日志时,将发送网络请求。如果应用程序在请求成功之前崩溃了,我该怎么办?用户甚至可能陷入崩溃循环的崩溃中。本文介绍第二种情况发生时如何准确上报crashlog。首先,我们需要一种更可靠的方法来判断应用程序启动时是否发生了上次启动崩溃。介绍一个可行的想法。如何检测连续闪回连续闪回包含两个元素,闪回和延续。只有这两个元素同时存在,我们的日志上传才会受到影响。flashback的定义可以简单的appcrashtime-appstartuptime<=5s(orotherthreshold)continuous定义是至少连续出现两次或以上。一般2次就够了。很多时候,用户会在连续闪回两次后放弃尝试。我们可以通过记录几个特殊的时间点时间戳来尝试还原App崩溃场景下的生命周期。App启动时间戳,定义为launchTsApp每次启动,记录当前时间,写入时间数组。App崩溃时间戳,定义为App每次启动时的crashTs,通过崩溃收集库获取上次崩溃报告的时间戳,写入时间数组。App正常退出时间戳,定义为terminateTs当App收到UIApplicationWillTerminateNotification通知时,记录当前时间戳,写入时间数组。需要注意的是,还有很多种App退出行为的时间戳是无法准确记录的。之所以记录terminateTs是为了排除一种特殊情况,即用户在启动app后立即手动kill掉app。如果我们正确地记录了以上三个时间戳,那么我们就可以得到一个与App崩溃行为相关的时间线。例如:launchTs=>crashTs=>launchTs=>terminateTsorlaunchTs=>launchTs=>launchTsorlaunchTs=>crashTs=>launchTs=>crashTs=>launchTs请头脑风暴以上三个时间线的行为特征。显然,第三条时间线看起来像是连续崩溃了两次。我们只需要加上时间间隔判断就可以知道是否是连续两次闪回。注意如果两次crashT之间有terminateT,不能认为是连续crash。检测代码比较简单,就不贴了。这条时间线只记录了与crash相关的app启动和退出行为,还有很多特殊的时间点没有记录,比如app在前台内存不足(FOOM),app卡在后台前台主线程被系统WatchDog杀死,iOS系统升级时App被强杀,App从AppStore升级时被强杀等,这些特殊时间点不记录,但是这些并不影响我们的App持续闪退检测,所以可以忽略。这里的意思是因为时间线记录是在启动时从磁盘读取的,涉及到磁盘读写,所以会影响App的启动时间。一个优化点是在每个写入时间点删除旧的。例如timestamp,只记录最近的5个时间戳。或者在没有读取到crashlog的情况下,甚至不需要启动连续crashdetection的整个过程。接下来,让我们看看如何在检测到连续闪回的情况下继续上传日志。等待Crash日志同步上传最直接的方法就是等待日志上传成功,然后继续执行App代码。将网络请求改为同步?这样会阻塞UI线程,在网络不好的场景下会被系统看门狗杀死,显然不可取。我们仍然可以保持异步网络请求,只是暂时打断UI线程的流程,让整个app在UI线程的runloop中等待。一旦网络请求成功,就会跳回原来UI线程的代码流程。看看简单的实现,有几个细节需要注意。首先,我们需要添加一个应用交互。一旦进入runloop等待,会弹出加载界面提示用户耐心等待。其次,这个等待时间不宜过长。我个人建议不要超过5s。一旦超过5s,无论crashlog中上传的请求是否成功,都会恢复app原有的代码流程。日志不能在5s内上传成功的情况应该比较少,除非日志文件太大。这种方法的缺点也很明显。一是改动比较大(修改了原来的代码流程),二是需要增加新的UI交互,三是延长了用户的等待时间。让我们看看另一个技巧。启用后台进程上传Crash日志实际上,理想的日志上传是将上传的请求放到另一个不同的进程中,这样即使App再次崩溃,也不会影响另一个进程中代码的执行。问题是iOS应用程序都在沙箱环境中,系统不允许代码分叉新进程。幸运的是,从iOS8开始,系统为NSURLSession添加了后台会话功能。此功能允许NSURLSession在单独的进程中执行网络请求。个人感觉这个功能设计的初衷是为了提升部分App后台下载音视频等资源的体验。我实际测试了一下,发现无论是下载还是上传,我们都可以把网络请求放到另一个进程中。代码也很简单。比如我写了如下测试代码:NSURLSessionConfiguration*config=[NSURLSessionConfigurationbackgroundSessionConfigurationWithIdentifier:@"com.mrpeak.background.crashupload"];NSURLSession*session=[NSURLSessionsessionWithConfiguration:configdelegate:selfNSdelegateQueue:[NSOperationQueuenew]URL];*url=[NSURLURLWithString:@"https://images.unsplash.com/photo-1515816949419-7caf0a210607?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=f46b60857b4826e733da34993ec26a2f&auto=format&fit=crop&w=1534&q=80"];NSURLSessionDownloadTask*task=[sessiondownloadTaskWithURL:url];[taskresume];exit(0);执行后我们可以在控制台看到如下日志:我们可以清楚的看到nsurlsessiond进程是如何为我们完成网络请求,并尝试唤醒异常退出的App的。当然,在这种最理想的方式下,还有一些细节需要处理。例如,如何告诉应用程序已在本地成功上传和删除崩溃日志。由于连续崩溃的应用程序处于极不稳定的状态,因此无法保证任何代码逻辑都能顺利完成。我个人觉得一个比较理想的方式是在后台进程上报的日志中加入一个特殊的flag,然后在后台使用clientrequestID和这个flag来做去重和排序。在线App不断闪退是极其恶劣和可怕的故障。可怕的是当大面积连续闪退无法监控时,你正在哼着小曲敲代码,老大突然发现你手机上的App启动没了,一打开AppStore,我发现一星差评泛滥。如果是主流APP,甚至会上科技新闻。不难预料,一口漆黑的大锅正在形成。“火彼得”一定会出现在下一次的应用升级介绍中。全文结束。