美文网首页
iOS OC语言copy修饰符实现原理

iOS OC语言copy修饰符实现原理

作者: 小峰书 | 来源:发表于2023-08-20 10:48 被阅读0次

    前言

      在经过频繁的业务开发后,我们一般对NSString的认知会固定在:NSString类型一定会使用copy修饰符来修饰,实际真的是这样吗?为什么要用copy来修饰呢?下面我们通过几个例子来说明这种情况,并通过oc底层源码来探究这种用法的具体实现.

    copy与strong修饰符修饰属性的区别

      下面通过几个例子来区别下copystrong.这里我们就以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内部做了什么处理

      上面通过一个例子明白了copystrong的区别,那么现在来看下到底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还有集合(NSArrayNSDictionaryNSSet)对象是否需要使用copy来修饰,完全可以根据情况来设置,如果这个值可以100%保证不会被修改的,那么使用strong即可,如果存在被修改的风险的则优先考虑使用copy来修饰.

    使用copy修饰符会增加部分性能的消耗,从源码实现可以看出,copy修饰的对象底层实现的时候都需要进行判断,是否需要copy操作,如果项目过于庞大,copy修饰符使用过多也会造成性能消耗,所以如果这个值不存在被修改的风险,建议用strong优化性能.

    相关文章

      网友评论

          本文标题:iOS OC语言copy修饰符实现原理

          本文链接:https://www.haomeiwen.com/subject/adesmdtx.html