iOS OC语言copy修饰符实现原理
前言
在经过频繁的业务开发后,我们一般对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
优化性能.