前言

在经过频繁的业务开发后,我们一般对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优化性能。

Post Author: menglingfeng

发表评论

电子邮件地址不会被公开。 必填项已用*标注

You may also like

Label尺寸自适应与AutoLayout问题小结

水平布局与垂直布局L

iOS7之后的导航栏与控制器原点坐标问题

简单记录关于iOS7

iOS Extension App 问题总结

iOS extens

%d bloggers like this: