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

2023-08-20  本文已影响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内部做了什么处理

  上面通过一个例子明白了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优化性能.

上一篇下一篇

猜你喜欢

热点阅读