iOS 归档方法野指针崩溃修复记录 archiveRootObject: toFile:

最近线上遇到一个归档[NSKeyedArchiver archiveRootObject: toFile:] 崩溃的问题, 修复历程颇为曲折, 直到最后也没找到具体的问题, 在此记录下具体的排查方向吧.

崩溃几率大概千万分之一级别, 崩溃堆栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

SIGSEGV:SEGV_ACCERR

0 libobjc.A.dylib _objc_release_x0 + 8

1 CoreFoundation _cow_cleanup + 164

2 CoreFoundation -[__NSDictionaryM dealloc] + 148

3 CoreFoundation ___RELEASE_OBJECTS_IN_THE_ARRAY__ + 116

4 CoreFoundation -[__NSArrayM dealloc] + 148

5 Foundation -[NSKeyedArchiver dealloc] + 268

6 Foundation +[NSKeyedArchiver archivedDataWithRootObject:requiringSecureCoding:error:] + 120

7 业务代码

崩溃代码的目的是想把一个字典存到磁盘上做缓存, 内容如下:

1
2
3
4
5
6
7
8
9
{
"key1" : {
"item1" : {"property1" : "value1", "property2" : "value2", ... "property9" : "value9",},
"item2" : {"property1" : "value1", "property2" : "value2", ... "property9" : "value9",},
...
},
"key2" : "seq",
"key3" : ["url1", "url2", ...]
}

第一开始以为只是一个简单的野指针问题, item 在多线程环境下产生的野指针, 就简单做了一个深拷贝, 然后再从深拷贝之后的 item 转化成 json :{"property1" : "value1", "property2" : "value2", ... "property9" : "value9",}, 用新的 json 插入到大字典中, 最后存储, 然后上线后, 崩溃数据一点都没有变.

此时意识到问题不简单, 又继续做了如下修改

  1. 检查了字典中的每一个对象, 确认涉及对象创建和释放的部分使用都是发生在一个串行队列中
  2. [NSKeyedArchiver archiveRootObject: toFile:] 被标注为弃用, 换成新的[NSKeyedArchiver archivedDataWithRootObject:requiringSecureCoding:error:], 需要注意的是, 新老 api 的产物有小概率不兼容
  3. 使用 @try @catch 包住出问题代码
  4. 发现存在多次连续保存的调用, 限制频次为 10 秒触发一次
  5. 只有数据改变时触发归档
  6. 使用 atomic 修饰涉及的属性
  7. 归档前, 打印字典

以上操作 只有第5/6两个修改大概减少了 70% 的野指针崩溃数量, 第 8 点可以确定传入的数据没问题, 但是仍然崩溃, 说明是 NSKeyedArchiver 内部存在问题.

后续跟进思路, 打算弃用NSKeyedArchiver, 继续预研其他持久化工具

  • MMKV: 微信团队基于 mmap 内存映射的 key-value 组件, 但是也存在一些文件损坏的情况, 需要做好文件备份以及恢复
  • Sqlite : 存在数据库损坏, 且无法恢复
  • YYDiskCache : 在实践中发现用户主动杀死 app 时, YYDiskCache 监听退出通知, 然后释放缓存时候超时, 触发 MetricKit 上报问题, 并且它的磁盘缓存还是基于 NSKeyedArchiver 以及 Sqlite 实现的.
-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道