iOS App 闪退监测

article/2025/9/14 14:55:30

为保障线上 App 的用户体验,我们一般都会对线上 App 的 crash 率做实时监控,一旦检测到 spike,可以即刻调查原因,但这一切的前提是 crash 日志能够准确上报。

crash 日志上报有两个难点:

  • crash handler 安装之前的代码要绝对稳定
    如果日志采集器还没成功启动就 crash 了,自然什么日志也无法采集到。这一点并没有太多技巧可言,只能严格限制 handler 启动之前可以执行的代码。

  • App 无限循环 crash 时上报
    crash 日志上报时,会发送网络请求,如果请求成功之前 App 又发生 crash 该如何处理?用户甚至会陷入无限循环的 crash 中。

这篇文章介绍下出现第二种情况时,如何准确上报 crash 日志。

首先我们需要一种比较可靠的方式,可以在 app 启动时判断上次是否发生了启动 crash。介绍一个可行的思路。

如何检测连续闪退

连续闪退包含两个元素,闪退和连续。只有这两个元素同时具备时,才会影响我们的日志上传。闪退的定义可以简单为

app crash 时间 -  app 启动时间 <= 5s (或者其他 threshold)

连续的定义为,至少接连出现两次或者以上。一般 2 次就够了,很多时候用户连续经历两次闪退,就会放弃尝试。

我们可以通过记录若干个特殊的时间点 timestamp 来试图还原 App crash 场景下的生命周期。

  • App 启动 timestamp,定义为 launchTs
    App 每次启动时,记录当前时间,写入时间数组。

  • App crash timestamp,定义为 crashTs
    App 每次启动时,通过 crash 采集库,获取上次 crash report 的时间戳,写入时间数组。

  • App 正常退出 timestamp,定义为 terminateTs
    App 在接收到 UIApplicationWillTerminateNotification 通知时,记录当前时间戳,写入时间数组。注意,还有很多种 App 退出行为的时间戳是无法被准确记录的。

之所以要记录 terminateTs,是为了排除一种特殊情况,即用户启动 App 之后立即手动 kill app。如果我们正确记录了上面三个时间戳,那么我们可以得到一个与 App crash 行为相关的时间线。比如:

launchTs => crashTs => launchTs => terminateTs

或者

launchTs => launchTs => launchTs

或者

launchTs => crashTs => launchTs => crashTs => launchTs

请自行脑洞上面三种时间线的行为特征。很明显,第三种时间线看上去是连续 crash 了两次。我们只需要加上时间间隔判断,就能得知是否为连续两次闪退了。注意,如果两个 crashTs 之间如果存在 terminateTs,则不能被认为是连续闪退。检测代码比较简单,我就不贴了。

这个时间线只是记录与 crash 相关的 App 启动和退出行为,还有很多特殊的时间点没有记录,比如 App 在 前台发生 out of memory(FOOM),App 在前台 main thread 卡住被系统 Watch Dog 杀掉,iOS 系统升级时 App 被强杀,App 从 AppStore 升级时被强杀等等,这些特殊的时间点都没有记录,不过这些并不影响我们的 App 连续闪退检测,所以可以忽略。

这里指的注意的是,因为启动时要从 disk 读取时间线记录,涉及磁盘读写,会对 App 的启动时间产生影响,一个优化点是,在每次写入时间点移除掉较老的 timestamp,比如只记录最近 5 个时间戳。或者在没有读取到 crash 日志时,甚至不用启动连续闪退检测的整个流程。

接下来,我们看假设检测到连续闪退,我们如何继续上传日志。

同步等待 Crash 日志上传

最直白的方式,在 App 的代码继续执行之前,先等待日志上传成功。

把网络请求改成同步的?这会卡住 UI 线程,网络差的场景下会被系统 watch dog 强杀,显然不可取。

我们可以依旧保持异步网络请求,但是,暂时中断 UI 线程的流程,让整个 App 处于 UI 线程的 runloop 等待中,一旦网络请求成功,则跳回到 UI 线程的原有代码流程。

看着简单的实现,有几个细节需要注意。首先我们需要增加一个 App 交互,一旦进入 runloop 等待,展示一个 loading 界面,告知用户耐心等待。其次,这个等待时间不能过长,我个人建议不超过 5s,一旦超过 5s,无论 crash 日志上传的 request 是否成功,都恢复 App 原有代码流程。5s 内日志都无法上传成功的情况应该比较小,除非日志文件过大。

这种做法缺陷也很明显,一是改动比较大(修改了原有代码流程),二是需要增加新的 UI 交互,三是延长了用户的等待时间。

我们来看另一种取巧的做法。

启用后台进程上传 Crash 日志

其实最理想的日志上传,是将上传的 request 放到另一个不同的进程,那么即使 App 又发生闪退,也不会影响到另一个进程代码的执行。

问题是,iOS app 都处于 sandbox 环境下,系统不允许代码 fork 一个新进程。

幸运的是,从 iOS 8 开始,系统对 NSURLSession 新增了一个 background session 特性。这个特性允许 NSURLSession 将网络请求放入到一个单独的进程中执行。我个人感觉,这个特性设计,原本是为了增强某些 App 后台下载音视频等资源的体验。我实际测试下来,发现不管下载或者是上传,我们都可以将网络请求放入另一个进程。代码也很简单,比如我写一段如下的测试代码:

NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.mrpeak.background.crashupload"];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue new]];
NSURL *url = [NSURL URLWithString:@"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 = [session downloadTaskWithURL:url];
[task resume];

exit(0);

执行之后,我们可以在 console 中看到如下日志:

可以清楚的看到 nsurlsessiond 进程如何替我们完成网络请求,并试图唤醒已经异常退出的 App。

当然这种最理想的方式,也有一些细节需要处理。比如如何告知 App 某个 crash 日志上传成功,并从本地移除。由于连续闪退的 App 处于极度不稳定的状态,所以任何代码逻辑都无法确保顺利完成。

我个人感觉一种比较理想的方式是,给后台进程上报的日志加上某个特殊的 flag,然后在后台通过 client request ID 和这个 flag 来做去重和整理。

线上 App 连续闪退是一种极其恶劣和可怕的故障,可怕之处在于,发生大面积连续闪退且无法被监控时,你正哼着小曲敲着代码,老板突然发现自己手机上 App 启动不了了,一打开 AppStore,发现一星差评潮水般涌来,如果是主流 App 甚至还会上科技新闻,不难预料一口黑漆漆的大锅正在成形。下次 App 的升级介绍里一定会出现 "fire peter" 了。


http://chatgpt.dhexx.cn/article/CS4i4YyP.shtml

相关文章

苹果上传闪退 php,苹果手机app频繁闪退原因以及解决方法

就算再流畅的ios系统,再好的iPhone手机,用久了都会遇到一个这样子的问题,就是闪退的问题,而且APP闪退占据多数。那该怎么修复呢?闪电修告诉你原因和搞定的步骤。 闪电修上门维修手机 我们先了解下iPhone闪退的原因: 1.插件影响程序的正常运行,而导致APP闪退 2.下载的软件…

vc++6.0打开文件闪退_ipa企业签名app闪退原因

闪退跟签名一般来说没什么直接关系&#xff0c;如果掉签了&#xff0c;那根本打不开应用。闪退一般是程序或者设备问题。 掉签的原因一般有&#xff1a; 1、 企业证书的装机量的问题&#xff1a;苹果公司创建企业开发者账号最初的目的是为了方便一些大型企业内部员工测试用的&a…

app常见的 闪退及闪退的原因

背景&#xff1a; 最近一直在休假&#xff0c;把自己在工作中梳理的点点滴滴汇总整理&#xff0c;这样既能及时地让自己巩固各个要点&#xff0c;也希望通过自己整理的东西帮助其他的同行少走弯路&#xff0c;避开我之前踩过的大大小小&#xff0c;深深浅浅的坑。 此问题经常…

记一次使用android studio分析app闪退原因的过程

闪退演示 首页和问题反馈重复切换两次就闪退 &#xff08;因为是公司内部app&#xff0c;原有视频不做展示&#xff09; app架构 app是原生android studio开发的&#xff0c;部分页面是h5开发的&#xff0c;通过WebView和addJavascriptInterface接口实现js与java的交互 页面…

app闪退分析

一、网络异常引起的 1.网络异常引起的&#xff0c;服务端响应不及时&#xff0c;可能导致闪退&#xff0c;检查网络配置情况 二、版本过低 1.应用版本过低&#xff0c;app的sdk和手机的系统不兼容&#xff0c;造成闪退 2.有些api在老版本中有&#xff0c;在新版本中没有&am…

线程池的组成及种类

文章目录 一、 线程池的组成结构二、常见的线程池种类三、线程池的工作流程四、线程池的好处五、小结 我们知道一个进程可以把任务分成多个部分交给线程执行&#xff0c;多线程技术减少了CPU闲置时间&#xff0c;增加了程序并发性。 假设创建线程的时间为t1,执行任务的时间为t2…

Java 中几种常用的线程池

概述&#xff1a; 在java内置API中操作线程所用到的类为Thread。创建线程一般有两种方式&#xff0c; 继承Thread方式实现Runnable方式&#xff0c;并以runnable作为target创建Thread 在Android中的耗时任务一般都需要另开线程来执行&#xff0c;常常需要用线程池来管理这些…

面试官:线程池有哪几种创建方式,能详细的说下么?

根据摩尔定律所说&#xff1a;集成电路上可容纳的晶体管数量每 18 个月翻一番&#xff0c;因此 CPU 上的晶体管数量会越来越多。 但随着时间的推移&#xff0c;集成电路上可容纳的晶体管数量已趋向饱和&#xff0c;摩尔定律也渐渐失效&#xff0c;因此多核 CPU 逐渐变为主流&a…

JAVA常用的几种线程池

1. 为什么使用线程池 诸如 Web 服务器、数据库服务器、文件服务器或邮件服务器之类的许多服务器应用程序都面向处理来自某些远程来源的大量短小的任务。请求以某种方式到达服务器&#xff0c;这种方式可能是通过网络协议&#xff08;例如 HTTP、FTP 或 POP&#xff09;、通过 …

java线程池详解及五种线程池方法详解

基础知识 Executors创建线程池 Java中创建线程池很简单&#xff0c;只需要调用Executors中相应的便捷方法即可&#xff0c;比如Executors.newFixedThreadPool(int nThreads)&#xff0c;但是便捷不仅隐藏了复杂性&#xff0c;也为我们埋下了潜在的隐患&#xff08;OOM&#x…

Java常见的线程池有哪些?

1、什么是线程池 java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池 多线程技术主要解决处理器单元内多个线程执行的问题&#xff0c;它可以显著减少处理器单元的闲置时间&#xff0c;增加处理器单元的吞吐能力。 假设一个服务…

线程池的使用(7种创建方法)

目录 1. 固定数量的线程池 a. 线程池返回结果 b. ⾃定义线程池名称或优先级 2. 带缓存的线程池 3. 执⾏定时任务 a. 延迟执⾏(⼀次) b. 固定频率执⾏ c. scheduleAtFixedRate VS scheduleWithFixedDelay 4. 定时任务单线程 5. 单线程线程池 6. 根据当前CPU⽣成线程池 7. Threa…

线程池原理常用四大线程池及七大参数

目录 前言常用的四种线程池newCachedThreadPool——可缓存线程池newFixedThreadPool————指定线程数量newSingleThreadExecutor————单线程的ExecutornewScheduleThreadPool——定时线程池 线程池七大参数corePoolSize——核心线程最大数maximumPoolSize——线程池最大线…

创建线程池的七种方式

在 Java 语言中&#xff0c;并发编程往往都是通过床架线程池来实现的&#xff0c;而线程池的创建方式也有很多种&#xff0c;每种线程池的创建方式都对应了不同的使用场景。总结来说线程池的创建可以分为两大类&#xff1a; 通过 Executors 创建 通过 ThreadPoolExecutor 创建…

Java中常用的四种线程池

在Java中使用线程池&#xff0c;可以用ThreadPoolExecutor的构造函数直接创建出线程池实例&#xff0c;在Executors类中&#xff0c;为我们提供了常用线程池的创建方法。 ​ 接下来我们就来了解常用的四种&#xff1a; newFixedThreadPool 首先&#xff0c;看一下这种线程池的…

5种常用的线程池

目录 0 概述1 newCachedThreadPool&#xff08;可缓存的线程池&#xff09;2 newFixedThreadPool&#xff08;固定大小的线程池&#xff09;3 newScheduledThreadPool&#xff08;可做任务调度的线程池&#xff09;4 newSingleThreadPool&#xff08;单个线程的线程池&#xff…

java中的线程池有哪些,分别有什么作用?

阅读完本篇文章会知道如下三点&#xff1a; 1.进程-线程简单介绍 2.java的线程池是什么&#xff0c;有哪些类型&#xff0c;作用分别是什么 3.使用线程池的优点 1.进程-线程的简单介绍 进程 什么是进程呢&#xff1f; 进程是计算机中的程序关于某数据集合的一次运行活动&…

线程池有几种创建方式?

总体来说线程池的创建可以分为以下两类&#xff1a; 通过 ThreadPoolExecutor 手动创建线程池通过 Executors 执行器自动创建线程池。 而以上两类创建线程池的方式&#xff0c;又有 7 种具体实现方法&#xff0c;这 7 种实现方法分别是&#xff1a; Executors.newFixedThre…

创建线程池有哪几种方式

通常开发者都是利用Executors提供的通用线程池创建方法&#xff0c;去创建不同配置的线程池&#xff0c;主要区别在于不同的 Executors目前提供了5种不同的线程池创建配置&#xff1a; 1、newCachedThreadPool&#xff08;&#xff09;&#xff0c;它是用来处理大量短时间工作…

Java常见的5种线程池

在开发过程中我们常常需要使用到多线程来提高我们代码处理某些任务的效率&#xff0c;最基本的两种创建多线程的方式分别是继承Thread类和实现Runnable接口。但是创建线程和销毁线程的系统开销比较大&#xff0c;而且过多的线程会占用过多的内存等资源。在《阿里巴巴Java开发手…