• 中文
    • English
  • 注册
  • 查看作者
  • 一次遍历导致的崩溃

    故事背景

    01 环境及场景

    编译环境Xcode 12.5.1

    2021年8月的某一天,Augus正在调试项目需求A,因为A要求需要接入一个SDK进行实现某些采集功能

    02 操作流程

    • 在程序启动的最开始地方,初始化SDK,并分配内存空间

    • 在某次的启动中就出现了以下错误:

    Trapped uncaught exception ‘NSGenericException’, reason: ‘*** Collection <__NSSetM: 0x2829f9740> was mutated while being enumerated.’

    03 初步猜测

    开始的时候,我先排除自己代码的原因(毕竟代码自己写的,还是求稳一些),因为调试模式下没有开全局断点,所以本次的崩溃就这么被错失机会定位

    为了下一次的复现

    • 首先进行了NSMutableSet某些方法的hook

    • 开启全局断点

    04 最后定位

    项目中引入SDK导致的崩溃

    问题定位

    01 问题原因

    被引入第三方的SDK在某个逻辑中使用的NSMutableSet遍历中对原可变集合进行同时读写的操作

    复现同样崩溃的场景,Let’s do it

    02 控制台日志

    一次遍历导致的崩溃

    很好,现在已经知道了问题的原因,那么接下来解决问题就很容易了,让我们继续

    解决方案

    01 问题原因总结

    不能在一个可变集合,包括NSMutableArray,NSMutableDictionary等类似对象遍历的同时又对该对象进行添加或者移除操作

    02 解决问题

    把遍历中的对象进行一次copy操作

    一次遍历导致的崩溃

    其实其中的道理很简单,我现在简而概括

    你在内存中已经初始化一块区域,而且分配了地址,那么系统在这次的遍历中会把这次遍历包装成原子操作,因为会可能会访问坏内存或者越界的问题,当然这也是出于安全原因,不同的系统下的实现方式不同,但是底层的原理是一致的,都是为了保护对象在操作过程中不受可变因素的更新

    03 那问题来了

    • copy是什么?

    • copy在底层如何实现?

    • copy有哪些需要注意的?

    原理

    01 copy是什么

    copy是Objective-C编程语言下的属性修饰关键词,比如修饰Block orNS*开头的对象

    02 copy如何实现

    对需要实现的类遵守NSCopying协议

    实现NSCopying协议,该协议只有一个方法

    举例说明,首先我们新建一个Perosn类进行说明,下面是示例代码

    类的功能很简单,初始化的时候需要外层传入name进行初始化,如果name非法则进行默认值的处理

    • 类内部维护了一个可变集合用来存放好友

    • 外部提供了新增和移除的两个方法

    • (id)copyWithZone:(NSZone *)zone;中的实现就是简单的一个copy功能

    • 而deepCopy是对可变集合的深层复制,至于原因,我们会在延展中举例说明,这里先搁置

    03 copy底层实现

    之前的文档中说过,想要看底层的实现那就用clang -rewrite-objc main.m看源码

    为了方便测试和查看,我们新建一个TestCopy的类继承NSObject,然后在TestCopy.m中只加如下代码

    然后在终端执行$ clang -rewrite-objc TestCopy.m命令

    接下来我们进行源码分析

    总结:copy的getter是根据地址偏移找到对应的实例变量进行返回,那么objc_setProperty又是怎么实现的呢?

    objc_setProperty在.cpp中没有找到,在[Apple源码](链接附文后)中找到了答案,我们来看下

    看到内部又调用了objc_setProperty_non_gc方法,这里主要看下这个方法内部的实现,前五个参数和开始的传入一致,最后的两个参数是由shouldCopy决定,shouldCopy在这里是0 or 1,我们现考虑当前的情况,

    • 如果shouldCopy=0,那么copy=NO,mutableCopy=NO

    • 如果shouldCopy=1,那么copy=YES,mutableCopy=NO

    下面继续reallySetProperty的实现

    基于本例子中的情况,copy=YES,最后还是调用了newValue = [newValue copyWithZone:NULL];,如果copy=NO and mutableCopy=NO,那么最后会调用newValue = objc_retain(newValue);

    objc_retain的实现

    总结:用copy修饰的属性,赋值的时候,不管本身是可变与不可变,赋值给属性之后的都是不可变的。

    延展之深浅拷贝

    01 非集合类对象

    在iOS下我们经常听到深拷贝(内容拷贝)或者浅拷贝(指针拷贝),对于这些操作,我们将针对集合类对象和非集合类对象进行copy和 mutableCopy实验。

    类簇:Class Clusters

    • an architecture that groups a number of private, concrete subclasses under a public, abstract superclass. (一个在共有的抽象超类下设置一组私有子类的架构);

    • Class cluster 是 Apple 对抽象工厂设计模式的称呼。使用抽象类初始化返回一个具体的子类的模式的好处就是让调用者只需要知道抽象类开放出来的API的作用,而不需要知道子类的背后复杂的逻辑。验证结论过程的类簇对应关系请看这篇 [Class Clusters 文档](链接附文后)。

    NSString

    结论:str和copyAugus打印出来的内存地址是一样的,都是0x10cb63198且类名相同都是__NSCFConstantString,表明都是浅拷贝,都是NSString;变量mutableCopyAugus打印出来的内存地址和类名都不一致,所以是生成了新的对象。

    一次遍历导致的崩溃

    NSMutableString

    结论:str和copyStr和mutableCopyStr打印出来的内存地址都不一样的,但是生成的类簇都是__NSCFString,也就是NSMutableString。

    一次遍历导致的崩溃

    02 集合类对象

    本文对NSMutableSet展开讨论,所以只对该类进行测试。

    NSSet

    结论:set和copySet打印出来的内存地址是一致的0x6000007322b0,类簇都是__NSSetI说明是浅拷贝,没有生成新对象,也都属于类 NSSet;mutableCopySet的内存地址和类簇都不同,所以是深拷贝,生成了新的对象,属于类NSMutablSet;集合里面的元素地址都是一样的。

    一次遍历导致的崩溃

    NSMutableSet

    结论:set和copySet和mutableCopySet的内存地址都不一样,说明操作都是深拷贝;集合里面的元素地址都是一样的

    一次遍历导致的崩溃

    03 结论分析

    • NSMutable*开头的类不要用copy属性去修饰,因为每次赋值操作拷贝出来的都是不可变集合类

    • 集合类的copy和mutableCopy操作,对象里面的元素不会发生拷贝,只会对容器层面拷贝,也称之为单层深拷贝

    一次崩溃定位,一次源码之旅,一系列拷贝操作,基本可以把文中提到的问题说清楚;遇到问题不要怕刨根问底,因为问底的尽头就是无尽的光明。

  • 0
  • 0
  • 0
  • 79
  • 请登录之后再进行评论

    登录
  • 任务
  • 实时动态
  • 发布
  • 单栏布局 侧栏位置: