本文要将block的以下机制,并配合具体代码详细描述:
-
block 与 外部变量
-
block 的存储域:栈块、堆块、全局块
定义
块与函数类似,只不过是直接定义在另一个函数里,和定义它的那个函数共享同一个范围内的东西。
访问外部变量
堆块内部,栈是红灯区,堆是绿灯区。
根据块的存储位置,可将块分为全局块、栈块、堆块。这里先主要针对堆块讲解。
-
Block不允许修改外部变量的值。Apple这样设计,应该是考虑到了block的特殊性,block也属于“函数”的范畴,变量进入block,实际就是已经改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。又比如我想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。于是栈区变成了红灯区,堆区变成了绿灯区。
几种演算
-
block调用 基本数据类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
|
{
NSLog(@
"\n--------------------block调用 基本数据类型---------------------\n"
);
int
a =
10
;
NSLog(@
"block定义前a地址=%p"
, &a);
void
(^aBlock)() = ^(){
NSLog(@
"block定义内部a地址=%p"
, &a);
};
NSLog(@
"block定义后a地址=%p"
, &a);
aBlock();
}
/*
结果:
block定义前a地址=0x7fff5bdcea8c
block定义后a地址=0x7fff5bdcea8c
block定义内部a地址=0x7fa87150b850
*/
/*
流程:
1. block定义前:a在栈区
2. block定义内部:里面的a是根据外面的a拷贝到堆中的,不是一个a
3. block定义后:a在栈区
*/
{
NSLog(@
"\n--------------------block调用 __block修饰的基本数据类型---------------------\n"
);
__block
int
b =
10
;
NSLog(@
"block定义前b地址=%p"
, &b);
void
(^bBlock)() = ^(){
b =
20
;
NSLog(@
"block定义内部b地址=%p"
, &b);
};
NSLog(@
"block定义后b地址=%p"
, &b);
NSLog(@
"调用block前 b=%d"
, b);
bBlock();
NSLog(@
"调用block后 b=%d"
, b);
}
/*
结果:
block定义前b地址=0x7fff5bdcea50
block定义后b地址=0x7fa873b016d8
调用block前 b=10
block定义内部b地址=0x7fa873b016d8
调用block后 b=20
*/
/*
流程:
1. 声明 b 为 __block (__block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。)
2. block定义前:b在栈中。
3. block定义内部: 将外面的b拷贝到堆中,并且使外面的b和里面的b是一个。
4. block定义后:外面的b和里面的b是一个。
5. block调用前:b的值还未被修改。
6. block调用后:b的值在block内部被修改。
*/
{
NSLog(@
"\n--------------------block调用 指针---------------------\n"
);
NSString *c = @
"ccc"
;
NSLog(@
"block定义前:c=%@, c指向的地址=%p, c本身的地址=%p"
, c, c, &c);
void
(^cBlock)() = ^{
NSLog(@
"block定义内部:c=%@, c指向的地址=%p, c本身的地址=%p"
, c, c, &c);
};
NSLog(@
"block定义后:c=%@, c指向的地址=%p, c本身的地址=%p"
, c, c, &c);
cBlock();
NSLog(@
"block调用后:c=%@, c指向的地址=%p, c本身的地址=%p"
, c, c, &c);
}
/*
c指针本身在block定义中和外面不是一个,但是c指向的地址一直保持不变。
1. block定义前:c指向的地址在堆中, c指针本身的地址在栈中。
2. block定义内部:c指向的地址在堆中, c指针本身的地址在堆中(c指针本身和外面的不是一个,但是指向的地址和外面指向的地址是一样的)。
3. block定义后:c不变,c指向的地址在堆中, c指针本身的地址在栈中。
4. block调用后:c不变,c指向的地址在堆中, c指针本身的地址在栈中。
*/
{
NSLog(@
"\n--------------------block调用 指针并修改值---------------------\n"
);
NSMutableString *d = [NSMutableString stringWithFormat:@
"ddd"
];
NSLog(@
"block定义前:d=%@, d指向的地址=%p, d本身的地址=%p"
, d, d, &d);
void
(^dBlock)() = ^{
NSLog(@
"block定义内部:d=%@, d指向的地址=%p, d本身的地址=%p"
, d, d, &d);
d.string = @
"dddddd"
;
};
NSLog(@
"block定义后:d=%@, d指向的地址=%p, d本身的地址=%p"
, d, d, &d);
dBlock();
NSLog(@
"block调用后:d=%@, d指向的地址=%p, d本身的地址=%p"
, d, d, &d);
}
/*
d指针本身在block定义中和外面不是一个,但是d指向的地址一直保持不变。
在block调用后,d指向的堆中存储的值发生了变化。
*/
{
NSLog(@
"\n--------------------block调用 __block修饰的指针---------------------\n"
);
__block NSMutableString *e = [NSMutableString stringWithFormat:@
"eee"
];
NSLog(@
"block定义前:e=%@, e指向的地址=%p, e本身的地址=%p"
, e, e, &e);
void
(^eBlock)() = ^{
NSLog(@
"block定义内部:e=%@, e指向的地址=%p, e本身的地址=%p"
, e, e, &e);
e = [NSMutableString stringWithFormat:@
"new-eeeeee"
];
};
NSLog(@
"block定义后:e=%@, e指向的地址=%p, e本身的地址=%p"
, e, e, &e);
eBlock();
NSLog(@
"block调用后:e=%@, e指向的地址=%p, e本身的地址=%p"
, e, e, &e);
}
/*
从block定义内部使用__block修饰的e指针开始,e指针本身的地址由栈中改变到堆中,即使出了block,也在堆中。
在block调用后,e在block内部重新指向一个新对象,e指向的堆中的地址发生了变化。
*/
{
NSLog(@
"\n--------------------block调用 retain cycle---------------------\n"
);
View *v = [[View alloc] init];
v.tag =
1
;
v.frame = CGRectMake(
100
,
100
,
100
,
100
);
[self.view addSubview:v];
//self->view->v
void
(^block)() = ^{
v.backgroundColor = [UIColor orangeColor];
//定义内部:block->v
};
v.block = block;
//v->block
block();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(
3
* NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//预计3秒后释放v对象。
[v removeFromSuperview];
});
}
/*
结果:
不会输出 dealloc.
*/
/*
流程:
1. self->view->v
2. block定义内部:block->v 因为block定义里面调用了v
3. v->block
结论:
引起循环引用的是block->v->block,切断其中一个线即可解决循环引用,跟self->view->v这根线无关
*/
{
NSLog(@
"\n--------------------block调用self---------------------\n"
);
View *v = [[View alloc] init];
v.tag =
2
;
v.frame = CGRectMake(
100
,
220
,
100
,
100
);
[self.view addSubview:v];
//self->view->v
void
(^block)() = ^{
self.view.backgroundColor = [UIColor redColor];
//定义内部:block->self
_count ++;
//调用self的实例变量,也会让block强引用self。
};
v.block = block;
//v->block
block();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(
3
* NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//预计3秒后释放self这个对象。
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
appDelegate.window.rootViewController = nil;
});
}
/*
结果:
不会输出 dealloc.
*/
/*
流程:
1. self->view->v
2. v->block
3. block->self 因为block定义里面调用了self
结论:
在block内引用实例变量,该实例变量会被block强引用。
引起循环引用的是self->view->v->block->self,切断一个线即可解决循环引用。
*/
|
栈块、堆块、全局块
块本身也是对象,由isa指针、块对象正常运转所需的信息、捕获到的变量组成。
根据Block创建的位置不同,Block有三种类型,创建的Block对象分别会存储到栈、堆、全局数据区域。
block_storage.png
上面讲了块会把它所捕获的所有变量都拷贝一份,这些拷贝放在 descriptor 变量后面,捕获了多少个变量,就要占据多少内存空间。请注意,拷贝的并不是对象本身,而是指向这些对象的指针变量。
1. 在全局数据区的Block对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
{
NSLog(@
"\n--------------------block的存储域 全局块---------------------\n"
);
void
(^blk)(
void
) = ^{
NSLog(@
"Global Block"
);
};
blk();
NSLog(@
"%@"
, [blk
class
]);
}
/*
结果:输出 __NSGlobalBlock__
*/
/*
结论:
全局块:这种块不会捕捉任何状态(外部的变量),运行时也无须有状态来参与。块所使用的整个内存区域,在编译期就已经确定。
全局块一般声明在全局作用域中。但注意有种特殊情况,在函数栈上创建的block,如果没有捕捉外部变量,block的实例还是会被设置在程序的全局数据区,而非栈上。
*/
|
2. 在堆上创建的Block对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
{
NSLog(@
"\n--------------------block的存储域 堆块---------------------\n"
);
int
i =
1
;
void
(^blk)(
void
) = ^{
NSLog(@
"Malloc Block, %d"
, i);
};
blk();
NSLog(@
"%@"
, [blk
class
]);
}
/*
结果:输出 __NSMallocBlock__
*/
/*
结论:
堆块:解决块在栈上会被覆写的问题,可以给块对象发送copy消息将它拷贝到堆上。复制到堆上后,块就成了带引用计数的对象了。
在ARC中,以下几种情况栈上的Block会自动复制到堆上:
- 调用Block的copy方法
- 将Block作为函数返回值时(MRC时此条无效,需手动调用copy)
- 将Block赋值给__strong修饰的变量时(MRC时此条无效)
- 向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时
上述代码就是在ARC中,block赋值给__strong修饰的变量,并且捕获了外部变量,block就会自动复制到堆上。
*/
|
3. 在栈上创建的Block对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
{
NSLog(@
"\n--------------------block的存储域 栈块---------------------\n"
);
int
i =
1
;
__weak
void
(^blk)(
void
) = ^{
NSLog(@
"Stack Block, %d"
, i);
};
blk();
NSLog(@
"%@"
, [blk
class
]);
}
/*
结果:输出 __NSStackBlock__
*/
/*
结论:
栈块:块所占内存区域分配在栈中,编译器有可能把分配给块的内存覆写掉。
在ARC中,除了上面四种情况,并且不在global上,block是在栈中。
*/
|
内存泄漏
堆块访问外部变量时会拷贝一份指针到堆中,相当于强引用了指针所指的值。如果该对象又直接或间接引用了块,就出现了循环引用。
解决方法:要么在捕获时使用__weak解除引用,要么在执行完后置nil解除引用(使用后置nil的方式,如果未执行,则仍会内存泄漏)。
-
注意:使用__block并不能解决循环引用问题。
优缺点
优点:
-
捕获外部变量
-
降低代码分散程度
缺点:
-
循环引用引起内存泄露
总结
-
在block内部,栈是红灯区,堆是绿灯区。
-
在block内部使用的是将外部变量的拷贝到堆中的(基本数据类型直接拷贝一份到堆中,对象类型只将在栈中的指针拷贝到堆中并且指针所指向的地址不变。)
-
__block修饰符的作用:是将block中用到的变量,拷贝到堆中,并且外部的变量本身地址也改变到堆中。
-
循环引用:分析实际的引用关系,block中直接引用self也不一定会造成循环引用。
-
__block不能解决循环引用,需要在block执行尾部将变量设置成nil(但问题很多,比如block永远不执行,外面变量变了里面也变,里面变了外面也变等问题)
-
__weak可以解决循环引用,block在捕获weakObj时,会对weakObj指向的对象进行弱引用。
-
使用__weak时,可在block开始用局部__strong变量持有,以免block执行期间对象被释放。
-
块的存储域:全局块、栈块、堆块
-
全局块不引用外部变量,所以不用考虑。
-
堆块引用的外部变量,不是原始的外部变量,是拷贝到堆中的副本。
-
栈块本身就在栈中,引用外部变量不会拷贝到堆中。
参考