OC归纳总结 -- (2)OC对象@property的修饰符

对于@property常规的两种声明copy和strong修饰:

@interface LGPerson()
@property (copy, nonatomic) NSString *nickName; // copy 修饰
@property (strong, nonatomic) NSString *name;   // strong 修饰
@end
复制代码

在通过clang重写成c++以后, 会生成如下相关的代码:

...
// copy 修饰 -> 底层使  objc_setProperty
static void _I_LGPerson_setNickname_(LGPerson * self, SEL _cmd, NSString *nickName) { 
	objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGPerson, _nickName), (id)nickName, 0, 1); 
}

// getter 方法, 直接通过地址偏移获取
static NSString * _I_LGPerson_name(LGPerson * self, SEL _cmd) {
    return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_name)); 
}

// strong 修饰 -> 使用指针的地址偏移, 找到内存地址, 直接赋值
static void _I_LGPerson_setName_(LGPerson * self, SEL _cmd, NSString *name) { 
	(*(NSString *__strong *)((char *)self + OBJC_IVAR_$_LGPerson&_name)) = name;
}
复制代码

这里分析拆分成两个部分:

  1. copy修饰, 底层会走objc_getPropertyobjc_setProperty
  2. strong修饰底层会走其他的方法

@Property中的copy修饰符的底层方法

使用copy修饰会走底层LLVM中的objc_getPropertyobjc_setProperty方法

对于 getter方法, LLVM中的源码如下

//property相关的时 atomic 修饰
// 获取属性信息直接使用 _ivar 的指针地址偏移
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) {
        return object_getClass(self);
    }

    id *slot = (id*) ((char*)self + offset); // 根据 _ivar的偏移找 slot 
    // 这里涉及到 @property的 atomic属性, 如果非原子方法, 直接返回对象
    if (!atomic) return *slot;
    
    // 全局的属性锁!!!
    spinlock_t& slotlock = PropertyLocks[slot]; 
    slotlock.lock();//加锁
    id value = objc_retain(*slot); //获取到的对象引用计数+1
    slotlock.unlock();//解锁
    
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    //将获取到的对象注册到自动释放池中,保证值能一定获取到,所以是线程安全的
    return objc_autoreleaseReturnValue(value);
}
复制代码

对于atomic状态下使用的全局锁的一点相关信息如下:

  1. PropertyLocks是一个StripedMap<spinlock_t>类型的全局变量, 并且根据名称应该是一个HashMap
  2. StripedMap是一个用数组来实现的HashMapkey是slot指针,value是类型是spinlock_t对象.

简单来说, atomic修饰的getter方法在底层会根据slot地址作为key, 然后获取一个全局的HashMap中维护的一组锁, 然后在后续关键过程中加锁!!!

最后返回对象时, 对象的引用计数+1, 然后被添加到AutoReleasePool

这里能看出atomic的加锁方式性能很差, 会公用全局的锁!!!

这个锁与对象_ivar的地址相关, 可能内存多个不同类型的对象公用一个全局锁!

另外对于 objc_setProperty在LLVM源码中有相关资料:

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);
}

/**
self:隐含参数,对象消息接收者
_cmd:隐含参数,setter对应函数
newValue:需要赋值的传入
offset:属性所在指针的偏移量
atomic:是否是原子操作
copy:是否是浅拷贝
mutableCopy:是否是深拷贝
*/
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) {
    if (offset == 0) { // 指针便宜是0, 表示 self 本身
        object_setClass(self, newValue); 
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset); // 成员变量对象指针

    if (copy) {
        //如果是浅拷贝,则将传入的新对象调用copyWithZone方法浅拷贝一份,并且赋值给newValue变量
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        //如果是深拷贝,则将传入的新对象调用mutableCopyWithZone方法深拷贝一份,并且赋值给newValue变量
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        //非copy, 赋值时候需要判断 newValue 与 oldValue 的地址是否一致!!! c++ 经常这样做
        if (*slot == newValue)
            return;
        // 赋新值!
        
        // newValue 对象引用计数+1,并且将返回值赋值给newValue变量
        newValue = objc_retain(newValue);
    }
    
    // 后面是真赋值给slot, 然后 release oldvalue
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        // 全局锁! 前面讲过
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;     
        slotlock.unlock();
    }
    objc_release(oldValue);
}
复制代码

从上面的代码逻辑可以十分清洗, 这里仔细看一下copy相关的内容:

  1. 如果是 copy 方法,则调用对象的 copyWithZone方法

  2. 如果是mutablecopy,则调用对象的mutableCopyWithZone 方法

  3. 如果 copy = 0,mutablecopy = 0,那么最终会调用objc_retain方法

总之, copy只会在setter时候有效, 在getter时,无效!!!

实际在ARC中这两个方法在很多场景下不会触发. 而是走了另外的逻辑. 例如上面的strong或者assign, setter会直接通过地址偏移量直接进行赋值, 而不会走objc_setProperty方法. 于此同时, 两个方法中根本没有体现strong weak assign字段的逻辑.

OC对象属性的直接获取成员变量以及赋值逻辑分析

我们知道:

  1. getter的本质是获取对象属性中生成的成员变量的信息!

  2. setter的本质就是给对象属性生成的成员变量进行赋值!!!

@property 会生成 _ivar getter 和setter

下面是runtime的相关函数:

id object_getIvar(id obj, Ivar ivar) {
    if (!obj  ||  !ivar  ||  obj->isTaggedPointer()) return nil;
    ptrdiff_t offset;
    //成员变量的内存管理方式: 可能是ARC: strong, weak, Unretained 也可能是MRC 
    objc_ivar_memory_management_t memoryManagement;
    _class_lookUpIvar(obj->ISA(), ivar, offset, memoryManagement);
    id *location = (id *)((char *)obj + offset);
   
    if (memoryManagement == objc_ivar_memoryWeak) {
        return objc_loadWeak(location); // weak 修饰, 去弱引用表查询
    } else {
        return *location; //非弱引用, strong, unretain 直接取值
    }
}

void _object_setIvar(id obj, Ivar ivar, id value, bool assumeStrong) {
    if (!obj  ||  !ivar  ||  obj->isTaggedPointer()) return;

    ptrdiff_t offset;
    objc_ivar_memory_management_t memoryManagement;
    _class_lookUpIvar(obj->ISA(), ivar, offset, memoryManagement);

    // 成员变量的内存管理方式 未知, 是否假设成 strong or unretain
    if (memoryManagement == objc_ivar_memoryUnknown) {
        if (assumeStrong) memoryManagement = objc_ivar_memoryStrong;
        else memoryManagement = objc_ivar_memoryUnretained;
    }
    
    id *location = (id *)((char *)obj + offset);

    switch (memoryManagement) {
    case objc_ivar_memoryWeak:       
            objc_storeWeak(location, value); // weak修饰符, 粗糙农户到weak_table
            break;
    case objc_ivar_memoryStrong:     
            objc_storeStrong(location, value); // strong 修饰符
            break;
    case objc_ivar_memoryUnretained:  // unretain 的内存管理方式! 直接赋值操作!!! 没有内存操作
            *location = value; 
            break;
    case objc_ivar_memoryUnknown:    
            _objc_fatal("impossible");
    }
}
复制代码

从上面能简单总结:

  1. weak修饰下, 关联方法是objc_loadWeakobjc_storeWeak
  2. strong修饰下, getter方法直接取值!!!, 而setter方法是objc_storeStrong
  3. unretain修饰下, getter 和 setter直接对成员变量*location取值赋值操作, 没有任何内存管理的操作

strong修饰符

对于strong修饰符的关联方法objc_storeStrong, 代码非常清洗, 就是内存管理的一些方法:

void objc_storeStrong(id *location, id obj) {
    id prev = *location; // 缓存旧值
    // 判断是否重复赋值
    if (obj == prev) {
        return;
    }
	// 新值内存管理
    objc_retain(obj);
    //将对象指针指向新值
    *location = obj;
    // 释放旧值
    objc_release(prev);
}
复制代码

weak 修饰符

与weak修饰符有关的是objc_loadWeakobjc_storeWeak, 这里主要看一下storeWeak

static id storeWeak(id *location, objc_object *newObj) {
    assert(haveOld  ||  haveNew);
    if (!haveNew) 
    	assert(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;
 retry:
    if (haveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    
    if (haveNew) {
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }
    
    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
    //如果haveOld为真,而且location指针指向的对象并不是oldObj
    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }

    // Prevent a deadlock between the weak reference machinery
    // and the +initialize machinery by ensuring that no 
    // weakly-referenced object has an un-+initialized isa.
    if (haveNew && newObj) {
        Class cls = newObj->getIsa();
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized())  {
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            _class_initialize(_class_getNonMetaClass(cls, (id)newObj));
            previouslyInitializedClass = cls;
            goto retry;
        }
    }

    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    if (haveNew) {
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating);
        if (newObj && !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }
        *location = (id)newObj;
    } 
    
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
    return (id)newObj;
}
复制代码

weak的赋值操作比strong复杂很多, strong修饰的成员变量只是增加所持有对象的引用计数,而weak修饰的成员变量,需要处理weak_table. 这里关于weak的更多的内容在后续的内存管理中专门来写文章说明。

关于copy修饰在iOS中的理解

这里有一篇关于copy的总结, 我直接贴出来了:

就 iOS 开发而言,关于 copy 的几个概念:

1. 拷贝:即复制,目的是产生副本,让原对象和副本相互独立,互不影响;
2. 不可变拷贝:即 copy 方法,无论原对象是否可变,都产生不可变副本;
3. 可变拷贝:即 mutableCopy 方法,无论原对象是否可变,都产生可变副本;
4. 深拷贝:内容拷贝,产生新的对象;
5. 浅拷贝:指针拷贝,不产生新的对象;

由上可知,copy 和深拷贝是两个概念,两者并不一定相等,先给结果:

- 源对象不可变时,copy 方法就是浅拷贝;
- 源对象可变时,copy 方法就是深拷贝;
- mutableCopy 方法无论何种情况都是深拷贝;

另外的一些总结:

1. copy 的目的是创建一个互不干扰,相互独立的副本;
2. copy 无论是直接调用还是修饰属性,其本质是调用copyWithZone和mutableCopyWithZone方法;
3. 深浅复制的区别在于返回值是否为新创建的对象,和调用 copy 的哪个方法无关;
4. 使用 copy 修饰属性的关键目的是告诉使用者,这个不要直接修改属性所指向内存中的值;
5. 修饰可变类型的对象,比如可变数组,严禁使用 copy 修饰;
6. copy 的本质是调用 copy 协议中的两个方法,只是系统对字符串、数组、字典、NSNumber 和 Block 实现了该协议的两个方法,其中两个方法所实现的逻辑大同小异;
7. copy 修饰属性的本质是自动调用新值的 copy 方法以获取一个不可变对象,属性无 mutableCopy 修饰,因为没有必要;
8. copy 修饰 Block 属性的本质仍然是调用 copy 方法,只是其内部实现是将存放在栈上的 block 转移到堆上,否则栈中的 Block 被销毁后会访问指向该 Block 的指针会产生坏内存访问问题


作者:康小曹
链接:https://juejin.cn/post/6844904033019232264
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
复制代码

补充:几个与@property修饰相关的问题

1、为什么给NSString类型属性使用copy修饰,改为strong可以吗?

NSString,NSArray,NSDictionary等等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary.

常规情况下copystrong的含义一致, 但是特殊情况下:

例如 NSString 属性可能被赋值成 NSMutableString 实例, 此时可变类型改变会破坏原有@property的封装性. 例如:

@interface Person:NSObject
@property(nonatomic, strong) NSSring *name;
@end

int main(){
	Person *person = [Person new];
	NSMutableString *testName = [NSMutableString stringWithString:@"hello"];
	person.name = testName;
	NSLog("%@", person.name); // 打印:  hello
	[testName appendString:@"world"];
	NSLog("%@", person.name); // 打印   helloworld
	return 0;
}
复制代码

这里很明显, 给 strong 修饰的NSString 属性, 会持有 NSMutableString的对象, 而该对象改变时, 会破坏NSString的封装性!!!

简单来说就是: 为了防止在把一个可变字符串在未使用copy方法时赋值给这个字符串对象时,修改原字符串时,本字符串也会被动进行修改的情况发生

2. @property (nonatomic, copy) NSMutableArray *array; 这种写法有什么问题?

使用copy修饰NSMutableArray成员, 因为copy 会产生一个不可变对象NSArray, 在后续调用增删改NSMutableArray的方法时, 实际的消息发送给NSArray, 会因为找不到到方法而崩溃.

参考

www.jianshu.com/p/4259ea33c…

blog.51cto.com/u_12801393/…

juejin.cn/post/684490…

猜你喜欢

转载自juejin.im/post/6996537874711576583