copy
copy,引用计数会+1.然而设置新值并不会保留旧值,而是将其拷贝。
NSString对象为什么尽量用copy来修饰?
我们通过代码查看copy和strong修饰的区别
#import "ViewController.h"
@interface ViewController ()
// copy字符串
@property (nonatomic, copy) NSString *myCopyStr;
// 强引用str
@property (nonatomic, strong) NSString *strongStr;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableString *testStr = [[NSMutableString alloc] initWithString:@"测试"];
// NSString *testStr = @"测试";
self.myCopyStr = testStr;
self.strongStr = testStr;
NSLog(@"testStr : 指针地址 %p", testStr);
NSLog(@"myCopyStr : 指针地址 %p", _myCopyStr);
NSLog(@"strongStr : 指针地址 %p", _strongStr);
NSLog(@"----------------------------------------------------");
[testStr appendString:@"sss"];
NSLog(@"testStr : 指针地址 %p ,内容%@", testStr , testStr);
NSLog(@"myCopyStr : 指针地址 %p ,内容%@", _myCopyStr,_myCopyStr);
NSLog(@"strongStr : 指针地址 %p ,内容%@", _strongStr,_strongStr);
}
打印结果
2018-07-17 14:52:35.066351+0800 property[51796:5806435] testStr : 指针地址 0x600000253c20
2018-07-17 14:52:35.066535+0800 property[51796:5806435] myCopyStr : 指针地址 0x600000037d40
2018-07-17 14:52:35.066662+0800 property[51796:5806435] strongStr : 指针地址 0x600000253c20
2018-07-17 14:52:35.066793+0800 property[51796:5806435] ----------------------------------------------------
2018-07-17 14:52:35.067159+0800 property[51796:5806435] testStr : 指针地址 0x600000253c20 ,内容测试sss
2018-07-17 14:52:35.067315+0800 property[51796:5806435] myCopyStr : 指针地址 0x600000037d40 ,内容测试
2018-07-17 14:52:35.067464+0800 property[51796:5806435] strongStr : 指针地址 0x600000253c20 ,内容测试sss
结论:我们可以看出: 通过strong修饰的字符串,strongStr和testStr指向同一块内存地址,testStr修改对应的值,也会相对应修改了strong修饰的字符串strongStr。而copy是重新拷贝了一份,申请了一块独立的内存,无法被影响。
接下来我们看看源码
id
object_copy(id oldObj, size_t extraBytes)
{
return _object_copyFromZone(oldObj, extraBytes, malloc_default_zone());
}
static id
_object_copyFromZone(id oldObj, size_t extraBytes, void *zone)
{
if (oldObj->isTaggedPointerOrNil()) return oldObj;
// fixme this doesn't handle C++ ivars correctly (#4619414)
Class cls = oldObj->ISA(/*authenticated*/true);
size_t size;
id obj = _class_createInstanceFromZone(cls, extraBytes, zone,
OBJECT_CONSTRUCT_NONE, false, &size);
if (!obj) return nil;
// Copy everything except the isa, which was already set above.
uint8_t *copyDst = (uint8_t *)obj + sizeof(Class);
uint8_t *copySrc = (uint8_t *)oldObj + sizeof(Class);
size_t copySize = size - sizeof(Class);
memmove(copyDst, copySrc, copySize); // 拷贝对象的内存数据
fixupCopiedIvars(obj, oldObj); // 处理对象的ARC
return obj;
}
最终调用的源码
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和strong修饰的属性在底层编译的不一致,主要还是llvm中对其进行了不同的处理的结果。copy的赋值是通过objc_setProperty,而strong的赋值时通过self + 内存平移(即将指针通过平移移至name所在的位置,然后赋值),然后还原成 strong类型
strong & copy 在底层调用objc_storeStrong,本质是新值retain,旧值release
strong
strong: 强引用,会使引用计数+1.setter方法赋值时,会保留新值,并释放旧值,然后在将新值设置。
weak
弱引用,引用计数不增加。setter方法赋值时,即不保留新值,也不释放旧值。当对象被销毁时,属性值会自动置nil。
assign
用于基本数据类型。CGFloat,NSInteger等
unsafe_unretained
作用于OC对象,引用计数不增加。当对象被销毁时,属性值不会清空,正如字面上的意思,不安全。
retain
1.判断是否为nonpointer ->散列表
2.操作引用计数
a: 如果不是 nonpointer -> 散列表
spinlock_t slock; 开解锁
RefcountMap refcnts; 引用计数表
weak_table_t weak_table; 弱应用表
b: 是否正在释放 如果正在释放就不需要操作引用计数了
c: extra_rc + 1 满了 - 散列表
d: carry 满 extra_rc 满/2 -> extra_rc 满/2 -> 散列表 (开锁关锁)
retain源码
ALWAYS_INLINE id
objc_object::rootRetain()
{
return rootRetain(false, RRVariant::Fast);
}
ALWAYS_INLINE bool
objc_object::rootTryRetain()
{
return rootRetain(true, RRVariant::Fast) ? true : false;
}
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
if (slowpath(isTaggedPointer())) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
oldisa = LoadExclusive(&isa.bits);
if (variant == RRVariant::FastOrMsgSend) {
// These checks are only meaningful for objc_retain()
// They are here so that we avoid a re-load of the isa.
if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
ClearExclusive(&isa.bits);
if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
return swiftRetain.load(memory_order_relaxed)((id)this);
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
}
}
if (slowpath(!oldisa.nonpointer)) {
// a Class is a Class forever, so we can perform this check once
// outside of the CAS loop
if (oldisa.getDecodedClass(false)->isMetaClass()) {
ClearExclusive(&isa.bits);
return (id)this;
}
}
do {
transcribeToSideTable = false;
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain(sideTableLocked);
}
// don't check newisa.fast_rr; we already called any RR overrides
if (slowpath(newisa.isDeallocating())) {
ClearExclusive(&isa.bits);
if (sideTableLocked) {
ASSERT(variant == RRVariant::Full);
sidetable_unlock();
}
if (slowpath(tryRetain)) {
return nil;
} else {
return (id)this;
}
}
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
// extra_rc++
//苹果为什么要这么设计???
//因为存储到散列表中需要开锁解锁操作,所以这里只放一半并且苹果也解释了 slowpath是很小的可能性
// x 很可能不为 0,希望编译器进行优化
#define fastpath(x) (__builtin_expect(bool(x), 1))
// x 很可能为 0,希望编译器进行优化
#define slowpath(x) (__builtin_expect(bool(x), 0))
if (slowpath(carry)) { //操作散列表 extra_rc 如果满了 放一半到散列表里面 newisa.extra_rc = RC_HALF;
// newisa.extra_rc++ overflowed
if (variant != RRVariant::Full) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
if (variant == RRVariant::Full) {
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
} else {
ASSERT(!transcribeToSideTable);
ASSERT(!sideTableLocked);
}
return (id)this;
}
NONPOINTER_ISA
苹果将 isa 设计成了联合体,在 isa 中存储了与该对象相关的一些内存的信息,原因也如上面所说,并不需要 64 个二进制位全部都用来存储指针。
来看一下 isa 的结构:
// x86_64 架构
struct {
uintptr_t nonpointer : 1; // 0:普通指针,1:优化过,使用位域存储更多信息
uintptr_t has_assoc : 1; // 对象是否含有或曾经含有关联引用
uintptr_t has_cxx_dtor : 1; // 表示是否有C++析构函数或OC的dealloc
uintptr_t shiftcls : 44; // 存放着 Class、Meta-Class 对象的内存地址信息
uintptr_t magic : 6; // 用于在调试时分辨对象是否未完成初始化
uintptr_t weakly_referenced : 1; // 是否被弱引用指向
uintptr_t deallocating : 1; // 对象是否正在释放
uintptr_t has_sidetable_rc : 1; // 是否需要使用 sidetable 来存储引用计数
uintptr_t extra_rc : 8; // 引用计数能够用 8 个二进制位存储时,直接存储在这里
};
// arm64 架构
struct {
uintptr_t nonpointer : 1; // 0:普通指针,1:优化过,使用位域存储更多信息
uintptr_t has_assoc : 1; // 对象是否含有或曾经含有关联引用
uintptr_t has_cxx_dtor : 1; // 表示是否有C++析构函数或OC的dealloc
uintptr_t shiftcls : 33; // 存放着 Class、Meta-Class 对象的内存地址信息
uintptr_t magic : 6; // 用于在调试时分辨对象是否未完成初始化
uintptr_t weakly_referenced : 1; // 是否被弱引用指向
uintptr_t deallocating : 1; // 对象是否正在释放
uintptr_t has_sidetable_rc : 1; // 是否需要使用 sidetable 来存储引用计数
uintptr_t extra_rc : 19; // 引用计数能够用 19 个二进制位存储时,直接存储在这里
}
注意这里的 has_sidetable_rc 和 extra_rc,has_sidetable_rc 表明该指针是否引用了 sidetable 散列表,之所以有这个选项,是因为少量的引用计数是不会直接存放在 SideTables 表中的,对象的引用计数会先存放在 extra_rc 中,当其被存满时,才会存入相应的 SideTables 散列表中,SideTables 中有很多张 SideTable,每个 SideTable 也都是一个散列表,而引用计数表就包含在 SideTable 之中。
SideTables
引用计数要么存放在 isa 的 extra_rc 中,要么存放在引用计数表中,而引用计数表包含在一个叫 SideTable 的结构中,它是一个散列表,也就是哈希表。而 SideTable 又包含在一个全局的 StripeMap 的哈希映射表中,这个表的名字叫 SideTables。
散列表(Hash table,也叫哈希表),是根据建(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过一个关于键值得函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称作散列函数,存放记录的数组称作散列表
// SideTables
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
// SideTable
struct SideTable {
spinlock_t slock; // 自旋锁
RefcountMap refcnts; // 引用计数表
weak_table_t weak_table; // 弱引用表
// other code ...
};
weak_table_t weak_table; 弱应用表
散列表 在内存里面有多张 + 最多能够多少张???
回答:
一个 SideTables 包含众多 SideTable,每个 SideTable 中又包含了三个元素,spinlock_t 自旋锁、RefcountMap 引用计数表、weak_table_t 弱引用表。所以既然 SideTables 是一个哈希映射的表,为什么不用 SideTables 直接包含自旋锁,引用计数表和弱引用表呢?这是因为在众多线程同时访问这个 SideTable 表的时候,为了保证数据安全,需要给其加上自旋锁,如果只有一张 SideTable 的表,那么所有数据访问都会出一个进一个,单线程进行,非常影响效率,虽然自旋锁已经是效率非常高的锁,这会带来非常不好的用户体验。针对这种情况,将一张 SideTable 分为多张表的 SideTables,再各自加锁保证数据的安全,这样就增加了并发量,提高了数据访问的效率,这就是为什么一个 SideTables 下涵盖众多 SideTable 表的原因。
retain总结
首先判断是否非nonpointer指针,直接操作散列表,进行引用计数+1的操作。
如果是nonpointer,操作extra_rc进行常规的引用计数+1操作,当然这里还有一个判断。如果属性值正在进行释放,也不需要进行引用计数的操作,同时这里还有一个细节要注意,苹果设计了一个算法是比较到位的,在真机上只有8位,如果说8位满了,需要额外借助散列表来存储,它会把extra_rc满状态的一半存储2的7次方到散列表中,剩下的2的7次方还是存在nonpointer的extra_rc中,来进行正常的引用计数操作。