iOS App 后台任务的坑

大多数 iOS App 在进入后台之后都会将一些关键任务封装到 Background Task 里,否则程序在若干秒之后就会被系统 Suspend。启动 Background Task 之后,可以获得 3 分钟继续执行代码的时间。最近在调查 Messenger 的 Background Crash 问题,最后都追踪到和 Background Task 相关,和大家分享下一些要点。

最近在调查 Messenger 的 Background Crash 问题,最后都追踪到和 Background Task 相关,和大家分享下一些要点。

Crash 信号

一般 App 都有自己的 crash 日志采集工具,这类工具一般有三个问题。第一是在工具启动之前的 crash 日志无法捕捉,第二是如果 App 启动闪退日志无法上传,第三是一些特殊场景的系统强杀无法捕捉 crash 信号。

解决第一个问题,只要将工具的执行时间尽可能提前,或者确保之前的代码及可能简单可靠。

解决第二个问题,可以采用我之前分享过的,使用 NSURLSession 的 background mode。

解决第三个问题,需要依赖于 Apple 自己的 crash 信号,这也是很多开发团队所忽视的一点。

Apple 也有自己的 crash 日志采集,不过基于用户隐私的考虑,这个 crash 日志并不可靠,主要存在以下几方面的缺陷:

用户需同意上传并分享数据,据闻,同意比例不足 20%,所以无法准确确定某个 crash 的实际影响面。

crash 日志工具简陋,通过 Xcode -> Organizer 打开,选中 App 就能从 Apple 后台下载某个版本的 crash 日志,无法通过某个条件做筛选,比如你不能过滤出所有 SIGKILL 的日志。

日志不全,Apple 按照自己的规则呈现 crash 样本,一个 App 实际线上的 crash 非常之多,但 Apple 列出的 crash 样本只有数十个,规则不明。

crash 日志只保存一周,一周刷新一次,所有比较明智的做法是写个脚本同步下来,上传到自己的后台。

Background Task 花式 crash

Background Task 的 API 及其简单,begin 和 end 之间的代码全部进入 Background Task 的范畴。但简单的代码隐藏着不小的风险,下面列出三个比较容易出现的 crash。而且这三个 crash 都是客户端自带的 crash 采集工具无法捕捉的,只能通过 Apple 的 crash 日志获得信号。原因很简单,这些 crash 发生的时候 app 一般处于 suspend 状态,根本没有机会执行任何代码,系统直接发送 SIGKILL 信号后就将 app 强杀,并生成一个系统日志,一个只能 Apple 访问的日志,还得用户先同意上传分享。

0xdead10cc

这个 crash 日志一般长这样:

Exception Type:  EXC_CRASH (SIGKILL)

Exception Codes: 0x0000000000000000, 0x0000000000000000

Exception Note:  EXC_CORPSE_NOTIFY

Termination Reason: Namespace SPRINGBOARD, Code 0xdead10cc

Termination Description: SPRINGBOARD, com.xxx.xxx was task-suspended with locked system file

原因我之前介绍过,当你的 App 有 Extension,而且 Extension 存在和 Host App 共享数据的需求,一般做法会将 db 文件放入 shared container 目录下,此时你的 App 就有大概率会发生这种 crash。

App 进入后台运行 Background Task,end 之后 App 被系统 suspend,如果 suspend 之后还存在任何访问 db 的操作,此时 App 会立马被系统强杀,这是 Apple 出于保护数据库文件的完整的考虑。

所以正确的做法是将所有有可能在 App 进入后台之后,还会发生的 db 操作统统封入 Background Task,以确保安全。这个代码写在 db layer 可能更加合适。

而且 Apple 推荐当你想启动 Background Task 的时候,其实并不需要考虑当前 App 是出于 foreground 还是 background,即使 App 在前台启动 Background Task,也并不会占用进入后台之后 3 分钟额度,所以放心大胆的把关键代码放进 Background Task 吧。

0xbada5e47

当你听从了上面的建议,大大方方的把尽可能多的关键代码封入 Background Task 后,那么你可能会遇到下面的 crash:

Exception Type:  EXC_CRASH (SIGKILL)

Exception Codes: 0x0000000000000000, 0x0000000000000000

Exception Note:  EXC_CORPSE_NOTIFY

Termination Reason: Namespace ASSERTIOND, Code 0xbada5e47

同理也是和 Background Task 相关,原因是 Apple 认为你启动了过多的 Background Task,所以要杀掉。多少算多呢?几十个不多,当前的 threshold 是 1000 个,超过 1000 个才会强杀。如果你的 Background Task 封装发生在 db layer,出现大量数据过来需要存储或读取的时候,还是有可能会 hit 这个 limit。

另一个 0xbada5e47 的可能原因是,Background Task 在超时之后会调用 expiry handler,无论你有多少个 Background Task,所有 expiry handler 执行的时间不能超过若干秒,一旦超过也会被枪杀。所以在 expiry handler 里面切忌有任何比如 disk io 的耗时操作。

0x8badf00d

说到 0x8badf00d,大家都很熟悉了,当你的主线程卡住的时间太长,系统的 Watchdog 会将你的 App 强杀,并生成一个带有 0x8badf00d 的 crash 日志。

Background Task 其实也可以 0x8badf00d 的,比如:

Exception Type:  EXC_CRASH (SIGKILL)

Exception Codes: 0x0000000000000000, 0x0000000000000000

Exception Note:  EXC_CORPSE_NOTIFY

Termination Reason: Namespace SPRINGBOARD, Code 0x8badf00d

当你的代码逻辑会产生 leaked Background Task 时,就会出现上面的系统强杀 crash 日志了,什么是 leaked Background Task 呢?看代码:

- (void)startBgTask

{

self.bgTaskID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{

NSLog(@"Expired: %lu", (unsigned long)self.bgTaskID);

[[UIApplication sharedApplication] endBackgroundTask:self.bgTaskID];

 }];

}

- (void)endBgTask

{

[[UIApplication sharedApplication] endBackgroundTask:self.bgTaskID];

}

上面的代码如果 startBgTask 执行两次,就一定会出现 leaked Background Task,因为 self.bgTaskID 第二次会被赋予一个新的 ID,之前的 task ID 就丢失了,无法正确调用 end。

那怎么判断 0x8badf00d 到底是主线程卡死,还是出现了 leaked Background Task ?很简单,看主线程的 stack,如果长这样:

Thread 0 Crashed:0  

libsystem_kernel.dylib         0x000000018472be08 0x18472b000 + 35921  

libsystem_kernel.dylib         0x000000018472bc80 0x18472b000 + 32002  

CoreFoundation                 0x0000000184c6ee40 0x184b81000 + 9744003  

CoreFoundation                 0x0000000184c6c908 0x184b81000 + 9648724  

CoreFoundation                 0x0000000184b8cda8 0x184b81000 + 485525  

GraphicsServices               0x0000000186b6f020 0x186b64000 + 450886  

UIKit                         0x000000018eb6d78c 0x18e850000 + 32664447  

Messenger                     0x0000000103015ee4 0x102ff8000 + 1225968   libdyld.dylib                 0x000000018461dfc0 0x18461d000 + 4032

这个 stack 很经典,经常会看到,不需要 symbolicate 也能知道是干啥,这是 UI 线程 runloop 处于 idle 状态的 stack,在等待 kernel 的 message。表示 UI 线程此时处于闲置状态,这种状态下的系统强杀大概率是由于 leaked Background Task 导致的。

善用设备本地的 crash 日志

当用户的手机遇到 crash,而你既无法重现又在后台找不到 crash 日志的时候,此时你最大的希望就是手机本地的 crash 日志了。

本地日志位于 Settings -> Privacy -> Analytics -> Analytics Data。打开看下,说不定你所开发的 App 的 crash 日志,我手机上微信和支付宝都有好些日志。

日志排序先是按照 App 的名称,再按日志发生的日期。

如果调查内存使用过多的 crash,可以查看 JetsamEvent-xxx 开头的日志。

如果想知道 App 发生 crash 前系统有哪些异常日志,需要首先在设备上安装一个 loggingiOS.mobileconfig 的文件,这个文件基本上就是让用户授权给你记录系统行为,用户在遇到 crash 的时候,同时按下两个音量键 + 电源键,松手震动之后,系统会将过去一段时间的关键日志记录下来,对于分析一些疑难杂症很有帮助,这种日志一般为 sysdiagnose_xxx 开头。

安装上述 loggingiOS.mobileconfig 文件之后,还有另外一个好处,Apple 会记录更多而且更详细的 crash 日志了,因为用户授权过,所以 Apple 可以大胆施为了。这类日志的文件名一般为:stacks + appName - date.ips。

如果用户的设备能重现你所调查的问题,还有另一个简单高效的办法,将手机 usb 连接 mac,然后启动 mac 上的 Console App,就能直观的看到所有系统关键日志了,比如网络异常日志可以查看 nsurlsessiond,定位异常日志查看 locationd,Background Task 异常日志可以查看 assertiond,也可以直接按照你 app 的进程名进行过滤,查看生命周期以及被强杀的原因。

总结

以上是最近调查 Background Task crash 的一些知识点分享,希望对大家有所帮助。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容