前言
在经过频繁的业务开发后,我们一般对NSString的认知会固定在:NSString类型一定会使用copy修饰符来修饰,实际真的是这样吗?为什么要用copy来修饰呢?下面我们通过几个例子来说明这种情况,并通过oc底层源码来探究这种用法的具体实现。
copy与strong修饰符修饰属性的区别
下面通过几个例子来区别下copy与strong.这里我们就以NSString来举例:
首先这边声明两个属性
@interface ViewController () @property (nonatomic, copy) NSString *copyedStr; @property (nonatomic, strong) NSString *strongStr; @end
一个用copy来修饰,一个用strong来修饰,下面通过代码来检验这两个修饰符的区别:
NSMutableString *mut = [NSMutableString stringWithString:@"hik"]; self.copyedStr = mut; self.strongStr = mut; [mut appendString:@"vision"]; NSLog(@"copyedStr: %@", self.copyedStr); NSLog(@"strongStr: %@", self.strongStr);
看下打印的结果
2021-02-26 18:47:16.340037+0800 Test[6725:730911] copyedStr: hik 2021-02-26 18:47:16.340128+0800 Test[6725:730911] strongStr: hikvision
这里能看到使用copy修饰的属性没有被改变,而使用strong修饰的属性值已经被改变,我们实际开发过程中声明为NSString的为不可变类型,不希望某些原因导致值被改变引起其他问题.所以一般会将不可变类型(有可变类型子类的对象)用copy来修饰。但并不是说不能使用strong来修饰。
两者的区别
- copy:用该修饰符进行修饰的属性,在调用该属性的set方法时,会进行一次copy操作,新开辟一块内存区域,所以原对象指针指向的内存区域值被修改不会造成影响.因为做了一次深拷贝.
- strong:用该修饰符进行修饰的属性,引用计数+1,但仅仅拷贝了指针,实际跟原置针指向同一块内存区域,所以原置针指向的内存区域值被修改之后,新的对象值也会被修改.这里仅仅是浅拷贝.
copy内部做了什么处理
上面通过一个例子明白了copy与strong的区别,那么现在来看下到底copy到底是做了什么,才能保证值是不可变的.这里我们通过clang的编译命令将oc文件编译成底层的c++源码一探究竟.通过命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc xxx.m将对应的.m文件编译成c++文件xxx.cpp,由于编译后会产生很多系统的代码,这里我们只贴上我们自己写的代码:
static void _I_ViewController_testStringCopy(ViewController * self, SEL _cmd) { NSMutableString *mut = ((NSMutableString * _Nonnull (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"), sel_registerName("stringWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_50__967f2m57k5dkqm4fm300hwr0000gn_T_ViewController_574830_mi_3); ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setCopyedStr:"), (NSString *)mut); ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setStrongStr:"), (NSString *)mut); ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)mut, sel_registerName("appendString:"), (NSString *)&__NSConstantStringImpl__var_folders_50__967f2m57k5dkqm4fm300hwr0000gn_T_ViewController_574830_mi_4); NSLog((NSString *)&__NSConstantStringImpl__var_folders_50__967f2m57k5dkqm4fm300hwr0000gn_T_ViewController_574830_mi_5, ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("copyedStr"))); NSLog((NSString *)&__NSConstantStringImpl__var_folders_50__967f2m57k5dkqm4fm300hwr0000gn_T_ViewController_574830_mi_6, ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("strongStr"))); } static NSString * _I_ViewController_copyedStr(ViewController * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_ViewController$_copyedStr)); } extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool); static void _I_ViewController_setCopyedStr_(ViewController * self, SEL _cmd, NSString *copyedStr) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct ViewController, _copyedStr), (id)copyedStr, 0, 1); } static NSString * _I_ViewController_strongStr(ViewController * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_ViewController$_strongStr)); } static void _I_ViewController_setStrongStr_(ViewController * self, SEL _cmd, NSString *strongStr) { (*(NSString **)((char *)self + OBJC_IVAR_$_ViewController$_strongStr)) = strongStr; }
这里注意下这两个属性的set方法的实现
static void _I_ViewController_setCopyedStr_(ViewController * self, SEL _cmd, NSString *copyedStr) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct ViewController, _copyedStr), (id)copyedStr, 0, 1); } static void _I_ViewController_setStrongStr_(ViewController * self, SEL _cmd, NSString *strongStr) { (*(NSString **)((char *)self + OBJC_IVAR_$_ViewController$_strongStr)) = strongStr; }
这里可以看出使用copy修饰的属性跟strong修饰的属性底层的set实现是不同的,strong的实现就是根据地址偏移找到对应的实例变量,然后直接赋值,所以当对象的指针指向的内存区域被修改之后,属性值也会同步被修改。
而使用copy修饰的其set方法内部调用了extern “C” __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);这个函数,第一个参数为属性的实例对象,第二个参数为SEL方法,第三个参数offset,是相对于self的偏移,用来找对应的成员变量的,第四个是新值,第五个是是否为原子性atomic,最后一个为是否需要进行copy操作。
通过clang编译出来的源码目前只能看到这个程度,我们需要去苹果OC源码地址(将oc的源码下载下来,打开工程去查看对应的函数实现)去查看objc_setProperty这个函数的具体实现才能知道内部做了什么操作。
通过查看源码,objc_setProperty具体实现如下
void objc_setProperty(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); }
在上面编译出来的c++源码中能看到系统调用这个函数的时候, shouldCopy设置的是1(最后一个参数)
static void _I_ViewController_setCopyedStr_(ViewController * self, SEL _cmd, NSString *copyedStr) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct ViewController, _copyedStr), (id)copyedStr, 0, 1); }
所以这里告诉系统,这个对象需要进行拷贝。
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY); bool mutableCopy = (shouldCopy == MUTABLE_COPY);
这里是有两个copy标识的,当shouldCopy=1,并且不是mutable copy时需要copy操作,如果shouldCopy为mutable copy时,进行mutable copy操作(这里的MUTABLE_COPY就是个宏定义#define MUTABLE_COPY 2)。
接下来看实际内部真正实现的函数reallySetProperty:
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) { if (offset == 0) { object_setClass(self, newValue); return; } id oldValue; id *slot = (id*) ((char*)self + offset); if (copy) { newValue = [newValue copyWithZone:nil]; } else if (mutableCopy) { newValue = [newValue mutableCopyWithZone:nil]; } else { if (*slot == newValue) return; newValue = objc_retain(newValue); } if (!atomic) { oldValue = *slot; *slot = newValue; } else { spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); oldValue = *slot; *slot = newValue; slotlock.unlock(); } objc_release(oldValue); }
当copy或者mutableCopy为true时会调用copyWithZone/mutableCopyWithZone对属性进行一层深拷贝,这里就可以解释为什么用copy为什么原属性不会被修改,因为内存地址不是同一块了.而copy修饰的属性copy为true,mutableCopy为flase,所以只要是被copy修饰的属性,不管传入的值是否是可变的,最终赋值给属性后都是不可变的,因为这里调用了copyWithZone方法。
总结
所以具体NSString还有集合(NSArray、NSDictionary、NSSet)对象是否需要使用copy来修饰,完全可以根据情况来设置,如果这个值可以100%保证不会被修改的,那么使用strong即可,如果存在被修改的风险的则优先考虑使用copy来修饰。
使用copy修饰符会增加部分性能的消耗,从源码实现可以看出,copy修饰的对象底层实现的时候都需要进行判断,是否需要copy操作,如果项目过于庞大,copy修饰符使用过多也会造成性能消耗,所以如果这个值不存在被修改的风险,建议用strong优化性能。
Social Menu