iOS block 机制

本文要将block的如下机制,并配合具体代码详细描述:ios

  • block 与 外部变量app

  • block 的存储域:栈块、堆块、全局块框架

定义函数

块与函数相似,只不过是直接定义在另外一个函数里,和定义它的那个函数共享同一个范围内的东西。spa

访问外部变量.net

堆块内部,栈是红灯区,堆是绿灯区。设计

根据块的存储位置,可将块分为全局块、栈块、堆块。这里先主要针对堆块讲解。指针

  • Block不容许修改外部变量的值。Apple这样设计,应该是考虑到了block的特殊性,block也属于“函数”的范畴,变量进入block,实际就是已经改变了做用域。在几个做用域之间进行切换时,若是不加上这样的限制,变量的可维护性将大大下降。又好比我想在block内声明了一个与外部同名的变量,此时是容许呢仍是不容许呢?只有加上了这样的限制,这样的情景才能实现。因而栈区变成了红灯区,堆区变成了绿灯区。code

几种演算orm

  • 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对象分别会存储到栈、堆、全局数据区域。

iOS 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执行期间对象被释放。

  • 块的存储域:全局块、栈块、堆块

  • 全局块不引用外部变量,因此不用考虑。

  • 堆块引用的外部变量,不是原始的外部变量,是拷贝到堆中的副本。

  • 栈块自己就在栈中,引用外部变量不会拷贝到堆中。

参考

相关文章
相关标签/搜索