IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 一次遍历导致的崩溃 -> 正文阅读

[移动开发]一次遍历导致的崩溃

b3a7f2e5832a908cc67f3d9312154b93.png

8a0adf50c2659ff7f2429e517988de94.gif?

本文字数:10920

预计阅读时间:28分钟

一次遍历导致的崩溃

题记:用最通俗的语言,描述最难懂的技术

?

本文是作者在对项目进行调试某静态库的功能进行单元测试发现的问题的记录,如果有哪些论述模糊或者不准确,请联系weiniu@sohu-inc.com

目录表

  • 故事背景

  • 问题定位

  • 解决方案

  • 原理

    • copy是什么

    • copy如何实现

    • copy底层实现

  • 延展之深浅拷贝

    • 集合类对象

    • 非集合类对象

  • 参考文档

  • 结束语

故事背景

环境及场景:

编译环境Xcode 12.5.1

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

操作流程

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

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

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

初步猜测

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

为了下一次的复现

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

  • 开启全局断点

最后定位

项目中引入SDK导致的崩溃

问题定位

问题原因

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

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

NSMutableSet?*mutableSet?=?[NSMutableSet?setWithObjects:@"1",@"2",@"3",?nil];
????
?for?(NSString?*item?in?mutableSet)?{
?????if?([item?integerValue]?<?3)?{
?????????[mutableSet?removeObject:item];
??????}
??}

控制台日志e69905f8545637f4c79a9aebcfdb5e11.png

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

解决方案

问题原因总结

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

解决问题

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

5e309fe6f399cf61108bf616c801b760.png

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

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

那问题来了

  • copy是什么?

  • copy在底层如何实现?

  • copy有哪些需要注意的?

?

带着这些疑问,我们继续下面的阅读,相信你读完肯定会柳暗花明又一村...

原理

copy是什么

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

copy如何实现

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

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

-?(id)copyWithZone:(NSZone?*)zone;

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

//?The?person.h?file
#import?<Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface?Person?:?NSObject<NSCopying>

-?(instancetype)initWithName:(NSString?*)name;

@property(nonatomic,?copy)?NSString?*name;


///?To?update?internal?mutabl?set?for?adding?a?person
///?@param?person?A?instance?of?person
-?(void)addPerson:(Person?*)person;


///?To?update?internal?mutbable?set?for?removing?a?person
///?@param?person?A?instance?of?person
-?(void)removePerson:(Person?*)person;


@end

NS_ASSUME_NONNULL_END
??
//?The?person.m?file
#import?"Person.h"

@interface?Person?()

@property(nonatomic,?strong)?NSMutableSet<Person?*>?*friends;

@end

@implementation?Person

#pragma?mark?-?Initalizaiton?Methods

-?(instancetype)initWithName:(NSString?*)name?{
????self?=?[super?init];
????if?(!self)?{
????????return?nil;
????}
???if(!name?||?name.length?<?1)?{
???????name?=?@"Augus";
????}
????_name?=?name;
????
????//?Warn:?Do?not?self.persons?way?to?init.?But?do?u?know?reason?
????_friends?=?[NSMutableSet?set];
????return?self;
}


#pragma?mark?-?Private?Methods

-?(void)addPerson:(Person?*)person?{
????
????//?Check?param?safe
????if?(!person)?{
????????return;
????}
????
????[self.friends?addObject:person];
????
????
}

-?(void)removePerson:(Person?*)person?{
????
????if?(!person)?{
????????return;
????}
????
????[self.friends?removeObject:person];
}


#pragma?mark?-?Copy?Methods

-?(id)copyWithZone:(NSZone?*)zone?{
????
????//?need?copy?object
????Person?*copy?=?[[Person?allocWithZone:zone]?initWithName:_name];
????
????return?copy;
}

-?(id)deepCopy?{
????Person?*copy?=?[[[self?class]?alloc]?initWithName:_name];
????copy->_persons?=?[[NSMutableSet?alloc]?initWithSet:_friends?copyItems:YES];
????return?copy;
}


#pragma?mark?-?Lazy?Load

-?(NSMutableSet?*)friends?{
????if?(!_friends)?{
????????_friends?=?[NSMutableSet?set];
????}
????return?_friends;
}

@end

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

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

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

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

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

copy底层实现

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

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

#import?"TestCopy.h"

@interface?TestCopy?()

@property(nonatomic,?copy)?NSString?*augusCopy;

@end

@implementation?TestCopy

@end

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

接下来我们进行源码分析

//?augusCopy's?getter?function
static?NSString?*?_I_TestCopy_augusCopy(TestCopy?*?self,?SEL?_cmd)?{?return?(*(NSString?**)((char?*)self?+?OBJC_IVAR_$_TestCopy$_augusCopy));?}

//?augusCopy's?setter?function
static?void?_I_TestCopy_setAugusCopy_(TestCopy?*?self,?SEL?_cmd,?NSString?*augusCopy)?{?objc_setProperty?(self,?_cmd,?__OFFSETOFIVAR__(struct?TestCopy,?_augusCopy),?(id)augusCopy,?0,?1);?}

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

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

//?self:?The?current?instance
//?_cmd:?The?setter's?function?name
//?offset:?The?offset?for?self?that?find?the?instance?property
//?newValue:?The?new?value?that?outer?input
//?atomic:?Whether?atomic?or?nonatomic,it?is?nonatomic?here
//?shouldCopy:?Whether?should?copy?or?not
void?
objc_setProperty(id?self,?SEL?_cmd,?ptrdiff_t?offset,?id?newValue,?
?????????????????BOOL?atomic,?signed?char?shouldCopy)?
{
????objc_setProperty_non_gc(self,?_cmd,?offset,?newValue,?atomic,?shouldCopy);
}

void?objc_setProperty_non_gc(id?self,?SEL?_cmd,?ptrdiff_t?offset,?id?newValue,?BOOL?atomic,?signed?char?shouldCopy)?
{
????bool?copy?=?(shouldCopy?&&?shouldCopy?!=?MUTABLE_COPY);
????bool?mutableCopy?=?(shouldCopy?==?MUTABLE_COPY);
????reallySetProperty(self,?_cmd,?newValue,?offset,?atomic,?copy,?mutableCopy);
}

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

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

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

下面继续reallySetProperty的实现

static?inline?void?reallySetProperty(id?self,?SEL?_cmd,?id?newValue,?ptrdiff_t?offset,?bool?atomic,?bool?copy,?bool?mutableCopy)
{
????id?oldValue;
????id?*slot?=?(id*)?((char*)self?+?offset);

????if?(copy)?{
????????newValue?=?[newValue?copyWithZone:NULL];
????}?else?if?(mutableCopy)?{
????????newValue?=?[newValue?mutableCopyWithZone:NULL];
????}?else?{
????????if?(*slot?==?newValue)?return;
????????newValue?=?objc_retain(newValue);
????}

????if?(!atomic)?{
????????oldValue?=?*slot;
????????*slot?=?newValue;
????}?else?{
????????spin_lock_t?*slotlock?=?&PropertyLocks[GOODHASH(slot)];
????????_spin_lock(slotlock);
????????oldValue?=?*slot;
????????*slot?=?newValue;????????
????????_spin_unlock(slotlock);
????}

????objc_release(oldValue);
}

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

objc_retain的实现

id?objc_retain(id?obj)?{?return?[obj?retain];?}

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

延展之深浅拷贝

非集合类对象

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

类簇:Class Clusters

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

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

NSString

NSString?*str?=?@"augusStr";
NSString?*copyAugus?=?[str?copy];
NSString?*mutableCopyAugus?=?[str?mutableCopy];
????
NSLog(@"str:(%@<%p>:?%p):?%@",[str?class],&str,str,str);
NSLog(@"copyAugus?str:(%@<%p>:?%p):?%@",[copyAugus?class],&copyAugus,copyAugus,copyAugus);
NSLog(@"mutableCopyAugus?str:(%@<%p>:?%p):?%@",[mutableCopyAugus?class],&mutableCopyAugus,mutableCopyAugus,mutableCopyAugus);

//?控制台输出
2021-09-03?14:51:49.263571+0800?TestBlock[4573:178396]?augus?str(__NSCFConstantString<0x7ffee30a1008>:?0x10cb63198):?augusStr
2021-09-03?14:51:49.263697+0800?TestBlock[4573:178396]?copyAugus?str(__NSCFConstantString<0x7ffee30a1000>:?0x10cb63198):?augusStr
2021-09-03?14:51:49.263808+0800?TestBlock[4573:178396]?mutableCopyAugus?str(__NSCFString<0x7ffee30a0ff8>:?0x6000036bcfc0):?augusStr
?

__NSCFConstantString是字符串常量类,可看作NSString__NSCFString是字符串类,可看作NSMutableString

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

类名操作新对象拷贝类型元素拷贝新类名
NSStringcopyNO浅拷贝NONSString

mutableCopyYES深拷贝NONSMutableString

NSMutableString

NSMutableString?*str?=?[NSMutableString?stringWithString:@"augusMutableStr"];
NSMutableString?*copyStr?=?[str?copy];
NSMutableString?*mutableCopyStr?=?[str?mutableCopy];


NSLog(@"str:(%@<%p>:?%p):?%@",[str?class],&str,str,str);
NSLog(@"copyStr:?(%@<%p>:?%p):?%@",[copyStr?class],&copyStr,copyStr,copyStr);
NSLog(@"mutableCopyStr:?(%@<%p>:?%p):?%@",[mutableCopyStr?class],&mutableCopyStr,mutableCopyStr,mutableCopyStr);

//?控制台输出
2021-09-03?15:31:56.105642+0800?TestBlock[4778:198224]?str:(__NSCFString<0x7ffeeaa34008>:?0x600001a85fe0):?augusMutableStr
2021-09-03?15:31:56.105804+0800?TestBlock[4778:198224]?copyStr:?(__NSCFString<0x7ffeeaa34000>:?0x600001a86400):?augusMutableStr
2021-09-03?15:31:56.105901+0800?TestBlock[4778:198224]?mutableCopyStr:?(__NSCFString<0x7ffeeaa33ff8>:?0x600001a86070):?augusMutableStr

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

类名操作新对象拷贝类型元素拷贝新类名
NSMutableStringcopyYES深拷贝NONSMutableString

mutableCopyYES深拷贝NONSMutableString

集合类对象

?

因为本文对NSMutableSet展开讨论,所以只对该类进行测试,其余的NSArray&NSMutableArrayNSDictionary&NSMutableDictionary本质是一样的,请小伙伴自行参考测试就行

NSSet

Person?*p1?=?[[Person?alloc]?init];
Person?*p2?=?[[Person?alloc]?init];
Person?*p3?=?[[Person?alloc]?init];

NSSet?*set?=?[[NSSet?alloc]?initWithArray:@[p1,p2,p3]];
NSSet?*copySet?=?[set?copy];
NSSet?*mutableCopySet?=?[set?mutableCopy];

NSLog(@"set:(%@<%p>:?%p):?%@",[set?class],&set,set,set);
NSLog(@"copySet:?(%@<%p>:?%p):?%@",[copySet?class],&copySet,copySet,copySet);
NSLog(@"mutableCopySet:?(%@<%p>:?%p):?%@",[mutableCopySet?class],&mutableCopySet,mutableCopySet,mutableCopySet);
????
//?控制台输出
2021-09-03?16:11:36.590338+0800?TestBlock[4938:219837]?set:(__NSSetI<0x7ffeef3f7fd0>:?0x6000007322b0):?{(
????<Person:?0x600000931e00>,
????<Person:?0x600000931e20>,
????<Person:?0x600000932000>
)}
2021-09-03?16:11:36.590479+0800?TestBlock[4938:219837]?copySet:?(__NSSetI<0x7ffeef3f7fc8>:?0x6000007322b0):?{(
????<Person:?0x600000931e00>,
????<Person:?0x600000931e20>,
????<Person:?0x600000932000>
)}
2021-09-03?16:11:36.590614+0800?TestBlock[4938:219837]?mutableCopySet:?(__NSSetM<0x7ffeef3f7fc0>:?0x600000931fa0):?{(
????<Person:?0x600000931e00>,
????<Person:?0x600000932000>,
????<Person:?0x600000931e20>
)}
?

__NSSetI是不可变去重无序集合的子类,即NSSet,__NSSetM是可变去重无序集合的子类,即NSMutableSet

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

类名操作新对象拷贝类型元素拷贝新类名
NSSetcopyNO浅拷贝NONSSet

mutableCopyYES深拷贝NONSMutablSet

NSMutableSet

NSMutableSet?*set?=?[[NSMutableSet?alloc]?initWithArray:@[p1,p2,p3]];
NSMutableSet?*copySet?=?[set?copy];
NSMutableSet?*mutableCopySet?=?[set?mutableCopy];

NSLog(@"set:(%@<%p>:?%p):?%@",[set?class],&set,set,set);
NSLog(@"copySet:?(%@<%p>:?%p):?%@",[copySet?class],&copySet,copySet,copySet);
NSLog(@"mutableCopySet:?(%@<%p>:?%p):?%@",[mutableCopySet?class],&mutableCopySet,mutableCopySet,mutableCopySet);
?
?//?控制台输出
2021-09-03?16:33:35.573557+0800?TestBlock[5043:232294]?set:(__NSSetM<0x7ffeefb78fd0>:?0x600002b99640):?{(
????<Person:?0x600002b99620>,
????<Person:?0x600002b99600>,
????<Person:?0x600002b995e0>
)}
2021-09-03?16:33:35.573686+0800?TestBlock[5043:232294]?copySet:?(__NSSetI<0x7ffeefb78fc8>:?0x6000025e54a0):?{(
????<Person:?0x600002b99620>,
????<Person:?0x600002b99600>,
????<Person:?0x600002b995e0>
)}
2021-09-03?16:33:35.573778+0800?TestBlock[5043:232294]?mutableCopySet:?(__NSSetM<0x7ffeefb78fc0>:?0x600002b99680):?{(
????<Person:?0x600002b99620>,
????<Person:?0x600002b99600>,
????<Person:?0x600002b995e0>
)}

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

类名操作新对象拷贝类型元素拷贝新类名
NSMutableSetcopyYES深拷贝NONSSet

mutableCopyYES深拷贝NONSMutablSet

结论分析

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

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

参考文档

  • 文档0:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Collections/Articles/Copying.html#//apple_ref/doc/uid/TP40010162-SW8

  • 文档1:https://gist.github.com/Catfish-Man/bc4a9987d4d7219043afdf8ee536beb2

  • 文档2:https://opensource.apple.com/source/objc4/objc4-723/runtime/objc-accessors.mm.auto.html

  • Apple源码:https://opensource.apple.com/source/objc4/objc4-723/runtime/objc-accessors.mm.auto.html

  • Class Clusters 文档:https://gist.github.com/Catfish-Man/bc4a9987d4d7219043afdf8ee536beb2

结束语

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

bdbae4fc7786ee3d57043931641fb4c6.gif

e7a36eb408c93877f2dc2f7d3673bee4.png

也许你还想看

(▼点击文章标题或封面查看)

小小的宏 大大的世界

2021-12-09

f0411bcb43bfaff7d4e909051a03cc1b.png

iOS下的闭包上篇-Block

2021-11-04

ffeaad85bc033e921d10839c26b02b38.png

你真的了解符号化么?

2021-09-16

a086f0738bff3dab940496eeac06408d.png

干货:探秘WKWebView

2021-10-21

e4962d44ad9496a70a95cf49b71e92ce.png

前端工程化-打造企业通用脚手架

2022-01-13

58bc6b6d65b4b0964a3aa440b4261a2f.png
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-02-05 21:51:20  更:2022-02-05 21:52:50 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 14:00:55-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码