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 实现的.

二次更新

1.使用 MMKV 存储磁盘缓存
2.数据改动后, 从全量写入, 改为按需写入

改为 MKMV 后仍存在崩溃, 量级比原本的野指针崩溃甚至更高一点, 崩溃点是 遍历 MMKV 所有的 key, 然后从 MMKV 获取 Object 时候崩溃, 遍历 key 以及 获取 Object 两处均存在崩溃.
然后增加修改

  1. 启动后遍历一次 MMKV 读取数据到内存, 后续遍历直接从内存遍历,
  2. 删除 MMKV 数据时候, 也删除内存缓存
  3. 写入 MMKV 的字符串做一次深拷贝.

改动上线后, 一两百次的野指针崩溃基本消失, 但是 MertricKit 仍捕获一例问题, 先引导用户删除缓存再试试

-------------本文结束感谢您的阅读-------------

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