对于@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;
}
复制代码
这里分析拆分成两个部分:
- copy修饰, 底层会走
objc_getProperty
和objc_setProperty
- strong修饰底层会走其他的方法
@Property中的copy修饰符的底层方法
使用copy修饰会走底层LLVM中的objc_getProperty
和objc_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状态下使用的全局锁的一点相关信息如下:
PropertyLocks
是一个StripedMap<spinlock_t>
类型的全局变量, 并且根据名称应该是一个HashMapStripedMap
是一个用数组来实现的HashMap
,key
是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
相关的内容:
-
如果是
copy
方法,则调用对象的copyWithZone
方法 -
如果是
mutablecopy
,则调用对象的mutableCopyWithZone
方法 -
如果
copy = 0
,mutablecopy = 0
,那么最终会调用objc_retain
方法
总之, copy只会在setter时候有效, 在getter时,无效!!!
实际在ARC中这两个方法在很多场景下不会触发. 而是走了另外的逻辑. 例如上面的strong
或者assign
, setter
会直接通过地址偏移量直接进行赋值, 而不会走objc_setProperty
方法. 于此同时, 两个方法中根本没有体现strong
weak
assign
字段的逻辑.
OC对象属性的直接获取成员变量以及赋值逻辑分析
我们知道:
-
getter的本质是获取对象属性中生成的成员变量的信息!
-
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");
}
}
复制代码
从上面能简单总结:
- weak修饰下, 关联方法是
objc_loadWeak
和objc_storeWeak
- strong修饰下, getter方法直接取值!!!, 而setter方法是
objc_storeStrong
- 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_loadWeak
和objc_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
.
常规情况下copy
和strong
的含义一致, 但是特殊情况下:
例如 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
, 会因为找不到到方法而崩溃.