iOS 关于block 、__weak、 __strong 对象
开篇寄语:
说到iOS下对象的释放一般都会想到引用计数这个概念,引用计数是否为 0 决定着对象是否要被回收。
声明一个临时变量,这个指针是存在栈区的,这个栈区的指针保存的内容是一个已在堆区开辟空间的对象地址。存在栈区的指针的创建与释放是由系统控制的,而堆区对象则需要手动创建及控制销毁。
问题一、__weak 、__strong 、__unsafe_unretained 修饰符对对象引用计数的影响
先上一段未加任何修饰符的代码,如下:
Person * p2;
{
Person * p = [[Person alloc] init];
Person * p1 = p;
p2 = p;
NSLog(@"p 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p)));
NSLog(@"p1 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p1)));
NSLog(@"p2 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p2)));
}
NSLog(@"p2 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p2)));
NSLog(@"p2 = %@",p2);
打印结果
如果你正在面试,或者正准备跳槽,不妨看看我精心总结的面试资料:https://gitee.com/Mcci7/i-oser 来获取一份详细的大厂面试资料 为你的跳槽加薪多一份保障
可以看到对于 p 的引用就是始终为 3,这里也没有任何异议,因为 3 个栈区的指针指向了同一块内存地址,引用计数就变为 3,这里有一个注意的地方,
代码在 p 对象 声明前后是加了一对 {} 这样的目的是 p 指针在出了 {} 后是会被系统销毁,那么,p、 p1 指针销毁后其指向的堆区内存对象引用计数就会相应 -1,注意看红框内打印的引用计数值,此时就只变成了 p2 在引用这块内存。
由于对象的引用计数未清 0 ,所以,先打印的是 p2 然后,在触发了对象销毁。
__strong 程序默认的变量修饰符就是 __strong ,所以,打印的结果与不加修饰符的一致。
__strong Person * p2;
{
Person * p = [[Person alloc] init];
Person * p1 = p;
p2 = p;
NSLog(@"p 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p)));
NSLog(@"p1 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p1)));
NSLog(@"p2 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p2)));
}
NSLog(@"p2 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p2)));
NSLog(@"p2 = %@",p2);
打印结果
__weak** 修饰
__weak Person * p2;
{
Person * p = [[Person alloc] init];
Person * p1 = p;
p2 = p;
NSLog(@"p 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p)));
NSLog(@"p1 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p1)));
NSLog(@"p2 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p2)));
}
NSLog(@"p2 = %@",p2);
打印如下
首先,这里有个小问题,用 __weak 修饰的 p2 引用计数为啥还是 3 ,这里可以这样理解,系统在使用 __weak 修饰的对象时会创建一个临时变量,这个临时变量会增加对象的引用计数,一旦代码执行完成,这个临时变量就会销毁,所以,这里打印的是 3 ,但它不会影响程序运行的最终结果,因为这个临时变量会及时销毁,可以联想一下 block 里 __strong 的作用。这里需要提出一点,一个对象的属性在用 . 语法的时候或者 set 方法的时候,引用计数也会加一,但这个加一并不会在代码执行完成后 -1 ,但该操作不会影响对象的释放。
其次,这里可以看到 Person 对象先进行销毁了,然后再打印的 p2 = null ,因为出来 {} 作用域 Person 对象就销毁了,所以先执行了销毁操作, __weak 修饰的 p2 被系统置为了 nil,对 nil 发送消息 iOS 下并不会崩溃。
__unsafe_unretained** 修饰
__unsafe_unretained Person * p2;
{
Person * p = [[Person alloc] init];
Person * p1 = p;
p2 = p;
NSLog(@"p 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p)));
NSLog(@"p1 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p1)));
NSLog(@"p2 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p2)));
}
NSLog(@"p2 = %@",p2);
打印如下
首先,这里的 p2 引用计数并没像 __weak 的那样会 +1,再次,这里在 Person 对象作用域外执行打印 p2 时候崩溃了,
由于 __unsafe_unretained 修饰的变量并不会在对象销毁后置为 nil,所以,程序访问了 野指针 崩溃了。
问题二、block 对引用计数的影响
Person * p = [Person alloc];
void(^block)(void) = ^{
NSLog(@"p---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p)));
};
block();
打印如下
这的 Person 的引用计数为 3,
两个疑问:1 为什么引用计数为 3;2、这里对象销毁了,是否不存在循环引用?
疑问一、这里来看看 block 里对 Person 对象都干了什么?打印一下地址信息
Person * p = [Person alloc];
NSLog(@"block 前 栈区 p 地址 = %p,p 堆区内存地址 = %p",&p,p);
void(^block)(void) = ^{
NSLog(@"block 后 栈区 p 地址 = %p,p 堆区内存地址 = %p",&p,p);
NSLog(@"p---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p)));
};
block();
打印如下
这里可以看到堆区的内存地址并没有变化,其实就是指向同一个对象,但是指针的地址变了,变到堆区了,也就是在 block 里面 copy 出来一个指向同一个堆区地址指针,这样就好理解了,block 的内部持有了外部变量引用计数 +1,但方法本身结束时候便会自动消减这次引用计数。
又 copy 出了一个指针,指针指向的内存地址还是 Person 对象,引用计数再次 +1,这样,执行一次 block,对象的引用计数 +2。
疑问二、为啥没有导致循环引用?
Person 对象并没有持有 block,二者并没有互相引用,block 执行完以后 Person 对象也就可以去释放了,所以,不存在循环引用的问题。
上面的 block 的类型为 NSMallocBlock ,用 __weak 修饰一下 block 变为 NSStackBlock 类型看看有什么变化。
这里有个警告,意思是这个 block 在赋值之后就会被销毁,但是 block 释放是在调用完成之后才会释放的,所以,这里有警告,当后面再用 __weak 修饰的 block 时候,它还可以再堆内存地址中找到这 block,这里不会在赋值后立即释放。
打印如下
首先,这里的 block 变为了 NSStackBlock 类型,block 里面 Person 对象的地址也由原来的 堆区地址 变为了 栈区地址, Person 对象的引用计数为 2。这里猜测,代码块内部并没有增加引用计数。
但是 NSStackBlock 类型的 block 很少用,因为调用一次就会释,但大多数业务场景是 block 要有随时处理逻辑的能力。
NSStackBlock 与 NSMallocBlock 类型的 block 本质都属于对象,内存地址都堆区。但是,不同的类型决定了 block 的释放时机。
本文仅仅是思考与总结,有不对地方欢迎指针,互相学习。