一. 引用计数
1. 引用计数存储在哪
我们都知道,调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1,那么引用计数存储在哪里呢?
其实在isa存储信息分析中已经讲过了,在64bit中,引用计数可以直接存储在优化过的isa指针中,也可能存储在SideTable结构体中。
- 如果引用计数不大(不大于19),会存储在isa的extra_rc中,其中rc是retainCount的意思。
- 如果引用计数过大,isa中的has_sidetable_rc就为1,引用计数就存储在一个叫SideTable的结构体的refcnts成员中,refcnts是个散列表。
isa_t共用体结构如下:
union isa_t
{
Class cls;
uintptr_t bits;
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
//0,代表普通的指针,存储着Class、Meta-Class对象的内存地址
//1,代表优化过,使用位域存储更多的信息
uintptr_t nonpointer : 1;
//是否有设置过关联对象,如果没有,释放时会更快
uintptr_t has_assoc : 1;
//是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
uintptr_t has_cxx_dtor : 1;
//存储着Class、Meta-Class对象的内存地址信息
uintptr_t shiftcls : 33;
//用于在调试时分辨对象是否未完成初始化
uintptr_t magic : 6;
//是否有被弱引用指向过,如果没有,释放时会更快
uintptr_t weakly_referenced : 1;
//对象是否正在释放
uintptr_t deallocating : 1;
//引用计数是否过大无法存储在isa中
//如果为1,那么引用计数会存储在一个叫SideTable的结构体的refcnts成员中,refcnts是个散列表
uintptr_t has_sidetable_rc : 1;
//里面存储的值是引用计数减1
uintptr_t extra_rc : 19;
};
};
2. 查看retainCount源码
打开objc4源码,找到NSObject.mm文件,找到 retainCount 方法:
- (NSUInteger)retainCount {
return ((id)self)->rootRetainCount();
}
objc_object::rootRetainCount()
{
//如果是TaggedPointer就意味着不是OC对象,就返回它自己
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits); //拿到isa本身
ClearExclusive(&isa.bits);
if (bits.nonpointer) { //如果是非指针类型(就是优化过的isa)
uintptr_t rc = 1 + bits.extra_rc; //直接将extra_rc加1,然后返回的就是引用计数
if (bits.has_sidetable_rc) { //如果有SideTable
rc += sidetable_getExtraRC_nolock(); //就从SideTable拿到引用计数
}
sidetable_unlock();
return rc; //然后返回
}
sidetable_unlock();
return sidetable_retainCount();
}
从上面代码可以看出,如果是优化过的isa,extra_rc+1就是引用计数,如果有SideTable,就从SideTable拿到引用计数,从SideTable拿到的引用计数加上extra_rc+1就是总的引用计数。
再进入sidetable_getExtraRC_nolock()函数,看看是如何在SideTable中拿到引用计数的,如下:
objc_object::sidetable_getExtraRC_nolock()
{
assert(isa.nonpointer);
//获取SideTable
SideTable& table = SideTables()[this];
//拿到table里的的refcnts(就是个散列表),然后通过find获取一个遍历器
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) return 0;
//取得遍历器里的second,然后进行一次位运算,之后就把这个值返回了
else return it->second >> SIDE_TABLE_RC_SHIFT;
}
进入SideTable:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts; //引用计数表(散列表)
weak_table_t weak_table; //弱引用表(散列表)
}
根据上面两段代码可以知道,先获取SideTable,再拿到SideTable里的refcnts(就是个散列表),引用计数就存储在这个refcnts散列表中,在refcnts散列表中拿到引用计数,再做一次位运算就获取到引用计数的值了。
3. 查看release源码
再看看 release 方法,从NSObject.mm -> release -> rootRelease -> sidetable_release,进入sidetable_release函数:
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
//获取SideTable
SideTable& table = SideTables()[this];
bool do_dealloc = false;
table.lock();
//拿到table里的的refcnts(就是个散列表),然后通过find获取一个遍历器
RefcountMap::iterator it = table.refcnts.find(this);
//用遍历器做各种事情
if (it == table.refcnts.end()) {
do_dealloc = true;
table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) {
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
do_dealloc = true;
it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
//引用计数减去SIDE_TABLE_RC_ONE这个值
it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
//如果需要调用dealloc,就给自己发送dealloc消息
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}
上面代码可以看注释。
- 先看倒数第二个注释:“引用计数减去SIDE_TABLE_RC_ONE这个值”,说明release方法的确有做引用计数减一的操作。
- 再看最后一个注释:“如果需要调用dealloc,就给自己发送dealloc消息”,以前我们说过,如果release之后引用计数为0,对象就会被销毁,这里也从源代码中证明了我们以前说的话是对的。
4. 查看retain源码
再看看 retain 方法,从NSObject.mm -> retain -> rootRetain -> sidetable_retain,进入sidetable_retain函数:
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
table.lock();
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
//引用计数加上SIDE_TABLE_RC_ONE这个值
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
上面代码也可以看出retain方法的确有做引用计数加一的操作。
二. __weak的原理
1. __strong、__weak、__unsafe_unretained的区别
① __strong
运行如下代码:
- (void)viewDidLoad {
[super viewDidLoad];
__strong MJPerson *person1;
// __weak MJPerson *person1;
// __unsafe_unretained MJPerson *person1;
NSLog(@"111");
{
//什么都不加,默认强指针,就相当于加一个__strong
MJPerson *person = [[MJPerson alloc] init];
//引用计数:1 __strong
//引用计数:1 __weak
//引用计数:1 __unsafe_unretained
person1 = person;
//引用计数:2 __strong
//引用计数:1 __weak
//引用计数:1 __unsafe_unretained
} //引用计数:1 __strong
//引用计数:0 __weak
//引用计数:0 __unsafe_unretained
NSLog(@"222");
NSLog(@"%p",person1);
NSLog(@"%@",person1);
} //引用计数:0 __strong 这行代码之后,对象才被释放
打印:
111
222
0x6000001ed290
<MJPerson: 0x6000001ed290>
-[MJPerson dealloc]
可以发现,当使用强指针指着,viewDidLoad代码执行完,对象才被释放,你也可以看上面引用计数分析的注释,这也和我们预期的结果一样。
② __weak
打开注释,将person1改成__weak类型,重新运行代码,打印:
111
-[MJPerson dealloc]
222
0x0
(null)
dealloc方法在111和222之间打印,说明在{}执行完,对象就被释放了,这时候打印指针的值为0x0,指针指向的内容为null。
这是因为用__weak修饰,引用计数器不会加一,在{}执行完之后,对象的引用计数为0,对象被释放,指针被清空(person1 = nil)。
③ __unsafe_unretained
打开注释,将person1改成__unsafe_unretained类型,重新运行代码后崩溃,提示坏内存访问:
Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
打印:
111
-[MJPerson dealloc]
222
0x6000005382f0
(lldb)
上面打印,前面三行和__weak没什么区别,第四行打印的地址有值,说明指针没被清空,最后一行崩溃了,提示坏内存访问,说明指针指向的地址已经被系统回收了。
这是因为使用__unsafe_unretained修饰,引用计数器不会加一,在{}执行完之后,对象的引用计数为0,对象被释放,对象的内存被回收,指针不会被清空,所以这时候通过指针访问指向的对象就会报坏内存访问错误。
2. 源码分析__weak原理
当一个对象要释放时,会自动调用dealloc,从NSObject.mm ->dealloc ->_objc_rootDealloc ->rootDealloc,进入rootDealloc函数:
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer && //是优化过的isa
!isa.weakly_referenced && //没有被弱引用指向过
!isa.has_assoc && //没有设置过关联对象
!isa.has_cxx_dtor && //没有C++的析构函数
!isa.has_sidetable_rc)) //没有SlideTable
{
assert(!sidetable_present());
free(this); //释放自己
}
else {
object_dispose((id)this); //如果上面条件不满足,就处理
}
}
上面代码很容易理解,如果是优化过的isa,并且没有被弱引用指向过、没有设置过关联对象、没有C++的析构函数、没有SlideTable就会直接调用free(this)提前释放掉自己。
如果条件不满足,会进入object_dispose函数:
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj); //处理对象的其他事情
free(obj); //然后释放自己
return nil;
}
这个函数可以看出,如果条件不满足,会先做一些事情,然后释放掉自己。
进入objc_destructInstance函数:
void *objc_destructInstance(id obj)
{
if (obj) {
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
if (cxx) object_cxxDestruct(obj); //清除成员变量
if (assoc) _object_remove_assocations(obj);//移除关联对象
obj->clearDeallocating(); //将指向当前对象的弱指针置为nil
}
return obj;
}
看最后一行注释,clearDeallocating函数将指向当前对象的弱指针置为nil。
进入clearDeallocating函数:
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) { //普通isa指针
sidetable_clearDeallocating();
}
//优化过的isa指针
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
clearDeallocating_slow();
}
assert(!sidetable_present());
}
进入clearDeallocating_slow函数:
objc_object::clearDeallocating_slow()
{
assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
//获取SideTable
SideTable& table = SideTables()[this];
table.lock();
if (isa.weakly_referenced) {
//取出SideTable的弱引用表,将当前对象传进去,把当前对象的弱引用清除
weak_clear_no_lock(&table.weak_table, (id)this);
}
//如果还有引用计数再把引用计数清除
if (isa.has_sidetable_rc) {
table.refcnts.erase(this);
}
table.unlock();
}
weak_clear_no_lock函数就是清空当前对象的弱引用,后面就不往下跟了。
可以发现,当一个对象要销毁,会自动调用dealloc,就会取出对象的SideTable中的弱引用表(散列表),然后将弱引用表里面存储的弱引用都给清除掉。
SideTable结构如下:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts; //引用计数表(散列表)
weak_table_t weak_table; //弱引用表(散列表)
}
三. 面试题
1. __weak指针的实现原理?
当一个对象要销毁,会自动调用dealloc,就会取出对象的SideTable中的弱引用表(散列表),然后将弱引用表里面存储的弱引用都给清除掉。
2. ARC都帮我们做了什么?
① ARC是LLVM编译器和Runtime系统相互协作的一个结果。
② ARC利用LLVM编译器自动帮我们生成retain、release这些代码。
③ __weak弱引用这样的存在是需要Runtime的支持的,是在程序运行过程中监控到对象要销毁的时候就会把这个对象对应的弱引用都给清除掉,这个我们从Runtime源码也能看出来。
Demo地址:__weak
3. 解释一下,什么时候用weak,什么时候用assgin?
- ARC之后才有weak,weak是弱指针,当使用weak关键字修饰成员变量的时候,成员变量内部是用__weak修饰的,不会让引用计数器+1,如果指向对象被销毁,指针会自动清空,就不会报坏内存访问了。
- 当使用assgin修饰的时候,内部是用__unsafe_unretained修饰的,不会让引用计数器+1,如果指向对象被销毁,指针不会清空,如果这时候访问对象的指针,就会有坏内存访问错误,如下图;
示例代码如下:
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, assign 或 weak ) UIView *redView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIView *view = [[UIView alloc] init];
view.backgroundColor = [UIColor redColor];
_redView = view;
// 如果注释掉下面的代码,点击屏幕的时候是不会有view出现的,因为view是局部变量,{}之后就被销毁了
[self.view addSubview:view];
}
// 如果使用weak修饰,点击屏幕不会报错,因为view被销毁之后,指针也会自动清空,所以不会报坏内存访问错误
// 如果是assign修饰,view被销毁之后,指针不会被清空,点击屏幕,执行代码_redView.frame = CGRectMake(50, 50, 200, 200);通过指针访问对象,但是这时候对象已经被销毁了,所以就会报坏内存访问错误
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
_redView.frame = CGRectMake(50, 50, 200, 200);
}
@end
4. weak内部实现原理?
Runtime维护了一个weak表(SideTable中的弱引用表,是个散列表),用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。
- 初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
- 添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
- 释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
SideTable结构如下:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts; //引用计数表(散列表)
weak_table_t weak_table; //弱引用表(散列表)
}
追问:当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?
- 当一个对象要释放时,会自动调用dealloc
- 在dealloc中,调用了_objc_rootDealloc函数
- 在_objc_rootDealloc中,调用了object_dispose函数
- 调用objc_destructInstance函数
- 最后调用clearDeallocating函数,将指向当前对象的弱指针置为nil,详细过程如下:
a:从weak表中获取废弃对象的地址为键值的记录
b:将包含在记录中的所有附有weak修饰符变量的地址,赋值为nil
c:将weak表中该记录删除
d:从引用计数表中删除废弃对象的地址为键值的记录