block本质探寻八之循环引用
说明:阅读本文,请参照之前的block文章加以理解;
一、循环引用的本质
//代码——ARC环境
void test1()
{
Person *per = [[Person alloc] init];
per.age = ;
per.block = ^{
NSLog(@"-------1");
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test1();
// test2();
}
NSLog(@"----");
return ;
}
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN typedef void(^MyBlock)(void); @interface Person : NSObject @property (nonatomic, assign) int age;
@property (nonatomic, copy) MyBlock block; @end NS_ASSUME_NONNULL_END #import "Person.h" @implementation Person - (void)dealloc
{
// [super dealloc];
NSLog(@"%s", __func__);
} @end
//打印
-- ::28.353740+ MJ_TEST[:] -[Person dealloc]
-- ::28.354013+ MJ_TEST[:] ----
Program ended with exit code:
分析:main函数日志输出之前,Person实例对象就被销毁了——因为在test1()方法中,强指针per持有[[Person alloc] init]对象会执行retain操作导致Person实例对象的retainCount值为2(此前alloc操作,其retainCount值就设置为1),当test1()方法结束时,per被存放在栈区也随之销毁,故per不会再持有Person实例对象即执行release操作导致该对象的retainCount指减1;当自动销毁池autoreleasepool结束时,会自动向池中的所有对象再次发送一条release消息,那么此时Person实例对象的retainCount值再次减1变成0,对象的引用计数一旦为0,其所占内存会被自动回收,因此Person实例对象就会销毁;
补充:我们知道blcok的内存管理模式为copy策略(原因就不分析了),因为在ARC环境下强指针持有block对象,系统会自动将block对象copy到堆区中,所以ARC模式下,系统会自动帮助我们对block进行copy的管理策略,我们写成strong的策略是没有任何问题的——但是,MRC模式下必须是copy策略,系统不会帮你管理内存,只能手动;这点请注意!
至此,以上Person实例对象销毁是正常的,那么什么情况下是不正常的?往下看:
//代码
void test2()
{
Person *per = [[Person alloc] init];
per.age = ;
per.block = ^{
NSLog(@"-------%d", per.age);
};
}
//打印
-- ::31.859710+ MJ_TEST[:] -----
Program ended with exit code:
//clang
main.cpp
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__strong per;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__strong _per, int flags=) : per(_per) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
Person.cpp
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
MyBlock _Nonnull _block;
};
struct NSObject_IMPL {
Class isa;
};
static void(* _I_Person_block(Person * self, SEL _cmd) )(){ return (*(MyBlock _Nonnull *)((char *)self + OBJC_IVAR_$_Person$_block)); }
static void _I_Person_setBlock_(Person * self, SEL _cmd, MyBlock _Nonnull block) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _block), (id)block, , ); }
分析:
<1>我们知道,oc对象编译成C++后的本质就是一个结构体Person_IMPL,该结构体的第一个成员变量就是isa指针,指向类对象本身;同时,@property修饰的属性,系统会自动生成一个结构体成员变量,还为之生成getter和setter方法——这些之前的文章已经说过,此处不再赘述!
<2>per实例对象结构体Person_IMPL中含有_block变量,通过setter法_I_Person_setBlock_将block对象(等号右边)赋值给该_block变量,因此_block指向block对象(强引用);
<3>在__main_block_impl_0结构体中,我们看到Person *__strong per,所以,block对象本身对Person实例对象也是强引用;
综上:block对象结构体__main_block_impl_0通过其内部成员指针变量Person *__strong per持有Person实例对象(强引用),而Person实例对象结构体Person_IMPL通过其内部成员指针变量_block持有block对象(强引用)——因此二者构成循环引用,当autoreleasepool大括号结束时,block对象和Person实例对象所占内存依然没有被系统回收,因为他们的引用计数依然大于0;
//图解——注:self是一个auto型的局部变量,指向的是[[Person alloc] init]实例对象

补充:所以block循环引用造成的直接后果是内存泄露(即程序结束而内存没有被回收——>根本原因是对象引用计数大于0(retain和release使用次数不对等)——>是因为强指针引用造成的);
引伸:当对象所占内存被回收时,指向对象的指针(强指针)应当被赋值于nil或者指向其他的合法内存,否则会导致野指针调用(乱指)程序崩溃——但是,用weak做内存管理策略(即修饰指针变量)时,为什么系统会自动将指针变量置为nil?这点后面文章会提到!
二、解决方案
思路:
据上分析,打破循环引用,只需要将其中一个强引用变成弱引用即可,那么要改变哪一个弱引用呢?Person实例对象内部拥有block属性,当该实例对象销毁时,其block属性也会随之销毁,所以我们只需要将block对象中的Person类型指针变成弱指针即可——通常都是这样做!
//图解

1)ARC环境下
方案一:weak修饰
//代码
void test3()
{
Person *per = [[Person alloc] init];
per.age = ;
__weak Person *weakPer = per;
per.block = ^{
NSLog(@"-------%d", weakPer.age);
};
}
//打印
-- ::17.451718+ MJ_TEST[:] -[Person dealloc]
-- ::17.452663+ MJ_TEST[:] ----
Program ended with exit code:
//clang
struct __test3_block_impl_0 {
struct __block_impl impl;
struct __test3_block_desc_0* Desc;
Person *__weak weakPer;
__test3_block_impl_0(void *fp, struct __test3_block_desc_0 *desc, Person *__weak _weakPer, int flags=) : weakPer(_weakPer) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
分析:
<1>block对象中,Person指针变量类型变成了__weak类型,打印前person对象销毁了;
<2>另外一种写法:__weak typeof(per) weakPer = per <=> __weak Person *weakPer = per;(前者写法居多)

方案二:__unsafe_unretained修饰

注:__weak修饰和__unsafe_unretained,二者有一个非常重要的区别:
经上分析,我们知道Person实例对象销毁后,其内部的block属性也会销毁,那么其也就不再指向block对象了,而此时一旦block对象没有任何强引用,作用域结束后,其也会被销毁,其成员变量Person指针也会被销毁,这点没问题!————但是,如果block对象还存在呢(被其他指针强引用),此时其内部成员变量Person指针也存在,但是依然会指向Person实例对象销毁前所占的内存区域,但是该内存区域已经被系统回收了,Person指针指向的是不合法的内存区域——如果是weak修饰,系统会自动将指针置为nil(指向合法的内存区域);如果是__unsafe_unretained修饰,什么也不会做,这样就会导致野指针调用!
方案三:__block修饰
//代码
void test4()
{
__block Person *per = [[Person alloc] init];
per.age = ;
per.block = ^{
NSLog(@"-------%d", per.age);
per = nil;
};
per.block();
}
//打印
-- ::06.697898+ MJ_TEST[:] -------
-- ::06.698250+ MJ_TEST[:] -[Person dealloc]
-- ::06.698300+ MJ_TEST[:] ----
Program ended with exit code:
//clang
struct __Block_byref_per_0 {
void *__isa;
__Block_byref_per_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
Person *__strong per;
};
struct __test4_block_impl_0 {
struct __block_impl impl;
struct __test4_block_desc_0* Desc;
__Block_byref_per_0 *per; // by ref
__test4_block_impl_0(void *fp, struct __test4_block_desc_0 *desc, __Block_byref_per_0 *_per, int flags=) : per(_per->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
分析:
<1>block对象持有__block对象,而__block对象又持有Person实例对象,而Person实例对象又持有block对象——如此,构成一个三角循环:

<2>通过block回调,Person实例对象指针被置为nil,而该指针本质是__block对象中的Person *__strong per指针,因此该指针不可能再指向Person实例对象了,所以,第2条持有就断开了,打破了三角循环;
说明:但是该方案看起来比较麻烦,一旦忘记将指针置为nil,就会造成内存泄露;
注:以上分析不理解,请参考前述block文章,此处不再赘述!
2)MRC环境
说明:该环境下不支持__weak修饰;
方案一:__unsafe_unretained修饰
//代码
void test5()
{
// __unsafe_unretained Person *per = [[Person alloc] init];
__block Person *per = [[Person alloc] init];
per.age = ;
per.block = [^{
NSLog(@"-------%d", per.age);
} copy];
[per release];
per = nil;
}
//打印
-- ::49.970441+ MJ_TEST[:] -[Person dealloc]
-- ::49.971587+ MJ_TEST[:] ----
Program ended with exit code:
分析:
<1>根据习惯,MRC环境下,我们通常会将block对象(等号右边)从栈区copy到堆区,以达到手动控制其内存销毁的目的;
<2>原理:调用alloc创建对象时,系统会自动将该实例对象引用计数置为1,而该对象又会随着block的copy而一起被copy到堆区,此时该对象的retainCount会加1(变成2),当对该对象发送release消息时,其retainCount自动减1(由2变成1),所以当程序结束时,Person实例对象retainCount为1(>0),其内存并不会被系统回收从而导致内存泄露;
那么,__unsafe_unretained修饰后,无论后面有多少次retain或者copy操作,Person实例对象的retainCount始终为1,所以程序结束前release时,其retainCount值变为0,此时内存被回收,而不会导致内存泄露的问题;
方案二:__block修饰

分析:为什么block可以?
<1>前述文章我们分析到,MRC环境下,__block修饰对象类型的auto局部变量,系统生成的__block对象并不会根据其内存成员变量Person指针变量(其实就是test5()方法中的per指针)是强指针类型而对Person实例对象([[Person alloc] init])进行retain操作(强引用);
<2>所以此时,__block的作用相当于__unsafe_unretained的作用,原理一样;
补充一个问题:在ARC环境下,弱指针不能通过"->"形式来访问对象的成员变量

原因:就是weakSelf很可能为为空(即有可能提前被释放了),所以必须使用强指针来访问
//代码
Person.m
- (void) test6
{
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"-------%d", strongSelf->_age);
};
}
main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
// test1();
// test2();
// test3();
// test4();
// test5();
Person *per = [[Person alloc] init];
[per test6];
}
NSLog(@"----");
return ;
}
//打印
-- ::41.010848+ MJ_TEST[:] -[Person dealloc]
-- ::41.011346+ MJ_TEST[:] ----
Program ended with exit code:
//clang
Person.m
struct __Person__test6_block_impl_0 {
struct __block_impl impl;
struct __Person__test6_block_desc_0* Desc;
Person *const __weak weakSelf;
__Person__test6_block_impl_0(void *fp, struct __Person__test6_block_desc_0 *desc, Person *const __weak _weakSelf, int flags=) : weakSelf(_weakSelf) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
分析:block对象的内部成员变量weakSelf依然是weak类型,并不受block代码块内部的__strong转化,该转化只是为了骗取编译器通过编译而已;
三、结论
1)原因:block对象与OC对象相互持有(强引用)——OC对象有block属性,block代码块中用到了该实例对象;
2)危害:程序结束时,相互强应用(对象的引用计数>0)导致实例对象所占内存不能及时被系统回收——即内存泄露;
3)解决:
<1>ARC:__weak修饰(常用)、__unsafe_unretained(会引起野指针调用,不推荐)、__block(过于繁琐,不推荐);
<2>MRC:__unsafe_unretained和__block;
block本质探寻八之循环引用的更多相关文章
- block本质探寻二之变量捕获
一.代码 说明:本文章须结合文章<block本质探寻一之内存结构>和<class和object_getClass方法区别>加以理解: //main.m #import < ...
- iOS 面试题(四):block 什么时候需要构造循环引用 --转自唐巧
问题 有没有这样一个需求场景,block 会产生循环引用,但是业务又需要你不能使用 weak self? 如果有,请举一个例子并且解释这种情况下如何解决循环引用问题. 答案 需要不使用 weak se ...
- block本质探寻一之内存结构
一.代码——命令行模式 //main.m #import <Foundation/Foundation.h> struct __block_impl { void *isa; int Fl ...
- block本质探寻七之内存管理
说明: <1>阅读本问,请参照block前述文章加以理解: <2>环境:ARC: <3>变量类型:基本数据类型或者对象类型的auto局部变量: 一.三种情形 //代 ...
- block本质探寻四之copy
说明: <1>阅读本文,最好阅读之前的block文章加以理解: <2>本文内容:三种block类型的copy情况(MRC).是否深拷贝.错误copy: 一.MRC模式下,三种b ...
- block本质探寻六之修改变量
说明: <1>阅读本文章,请参照前面的block文章加以理解: <2>本文的变量指的是auto类型的局部变量(包括实例对象): <3>ARC和MRC两种模式均适用: ...
- block本质探寻五之atuto类型局部实例对象
说明:阅读本文章,请参考之前的block文章加以理解: 一.栈区block分析 //代码 //ARC void test1() { { Person *per = [[Person alloc] in ...
- block本质探寻三之block类型
一.oc代码 提示:看本文章之前,最好按顺序来看: //代码 void test1() { ; void(^block1)(void) = ^{ NSLog(@"block1----&quo ...
- block中self会造成循环引用问题
将代码块中的 self换成unsafeSelf __unsafe_unretained 与 __weak 99%相同 __weak 当对象释放之后 会自动设置为nil 而__unsafe_unreta ...
随机推荐
- 网鼎杯 pwn 记录
题目位置 https://gitee.com/hac425/blog_data/tree/master/wdb babyheap 通过分配和释放构建 2 个 fastbin 链 利用 show 功能, ...
- linux 用户配置文件及其相关目录
用户配置文件及其相关目录: /etc/passwd 用户信息文件/etc/shadow 影子文件/etc/group 组信息文件/etc/gshadow 组密码文件邮箱目录模板目录 /etc/pass ...
- java 内存分析之方法返回值及匿名对象一
package Demo; public class Point { private double x, y, z; public Point(double _x, double _y, double ...
- spring多线程初探
6月14号 晴 最高温度37 今天很热的一天啊,开发的任务现在正在测试阶段,手头没有什么工作任务,忙里偷闲,丰富一下我的blog. 前两天有个需求:调用第三方接口,这个接口的响应时间有点长,需 ...
- for循环内嵌套finally使用的意外获得
在for循环中有continue和break,无论执行continue还是break finally的逻辑都会执行,原本以为是不执行的 格式 for (int i = 0; i < 3; i ...
- CSS 实例之文字的凸起与凹陷
一些页面会有一些凹凸文字效果,这个主要是设置背景+文字阴影来达成这个效果的.文字阴影使用方法如下: text-shadow: 水平位置 垂直位置 模糊距离 阴影颜色 具体代码如下: body { ba ...
- Windows Azure中WebSite 网站, Cloud Service 云服务,Virtual Machine 虚拟机的比较
在Windows Azure服务平台里,Web Site特点是: 在Windows Azure上构建高度可扩展的Web站点. 快速.轻松部署一个高度可扩展的云环境,并且可以从很小的规模开始. 使用您所 ...
- 【Redis】命令学习笔记——哈希(hash)(15个超全字典版)
本篇基于redis 4.0.11版本,学习哈希(hash)相关命令. hash 是一个string类型的field和value的映射表,特别适合用于存储对象. 序号 命令 描述 实例 返回 HSET ...
- [翻译] UIImageView-Letters
UIImageView-Letters https://github.com/bachonk/UIImageView-Letters An easy, helpful UIImageView cate ...
- 一、DAO设计模式 二、DAO设计模式的优化 三、JDBC中的事务,连接池的使用
一.DAO设计模式概述###<1>概念 DAO,Data Access Object ,用于访问数据库的对象. 位于业务逻辑和数据持久化层之间,实现对数据持久化层的访问 ...