本文要将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(100100100100);
        [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(100220100100);
        [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执行期间对象被释放。

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

  • 全局块不引用外部变量,所以不用考虑。

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

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

参考

iOS block 机制的更多相关文章

  1. iOS block从零开始

    iOS block从零开始 在iOS4.0之后,block横空出世,它本身封装了一段代码并将这段代码当做变量,通过block()的方式进行回调. block的结构 先来一段简单的代码看看: void ...

  2. iOS 事件处理机制与图像渲染过程(转)

    iOS 事件处理机制与图像渲染过程 iOS RunLoop都干了什么 iOS 为什么必须在主线程中操作UI 事件响应 CALayer CADisplayLink 和 NSTimer iOS 渲染过程 ...

  3. iOS 事件处理机制与图像渲染过程

    Peter在开发公众号功能时触发了一个bug,导致群发错误.对此我们深表歉意,并果断开除了Peter.以下交回给正文时间: iOS 事件处理机制与图像渲染过程 iOS RunLoop都干了什么 iOS ...

  4. iOS Block的本质(二)

    iOS Block的本质(二) 1. 介绍引入block本质 通过上一篇文章Block的本质(一)已经基本对block的底层结构有了基本的认识,block的底层就是__main_block_impl_ ...

  5. IOS 消息机制(NSNotificationCenter)

    消息机制 NSNotificationCenter 一直都在频繁使用,但是却对其原理不是十分了解.今天就花些时间,把消息机制原理重头到尾好好过一遍. iOS 提供了一种 "同步的" ...

  6. iOS Block界面反向传值

    在上篇博客 <iOS Block简介> 中,侧重解析了 iOS Block的概念等,本文将侧重于它们在开发中的应用. Block是iOS4.0+ 和Mac OS X 10.6+ 引进的对C ...

  7. iOS事件机制(二)

    从上一篇的内容我们知道,在iOS中一个事件用一个UIEvent对象表示,UITouch用来表示一次对屏幕的操作动作,由多个UITouch对象构成了一个UIEvent对象.另外,UIResponder是 ...

  8. ios消息机制

    ios消息机制介绍 ios 调用每一个方法的时候其实是走的ios的消息机制 举例介绍一下 创建一个Pserson类 有一个eat 对象方法 那么下面的代码可以用消息机制实现  导入消息头文件    # ...

  9. iOS签名机制解析

    最近遇到一个签名的问题,借机把iOS签名相关知识点研究了一下.现总结如下:(研究过程中参考了这位仁兄的博客.很全面,本文也有部分借鉴) 非对称加密 这个是签名机制的算法基础.所谓非对称加密的是相对于对 ...

随机推荐

  1. Web前端新手经典学习路线

    做前端开发八九年的时间,到2015年的时候,Web前端因为HTML5技术的高速发展,成为IT行业中最受欢迎和未来需求人才发展最好的职业,各个行业都想转行做前端开发,包括一些在校的学生,土木工程,一些不 ...

  2. SourceTree/git解决pre-commit hook failed的问题

    一. git commit -m 'xxx' 出现问题 今天在上传项目的时候在commit阶段遇到一个问题,无论是在Sourcetree上传还是用命令git commit -m 'xxx'都报了一下错 ...

  3. java static方法不能被重写@Override

    重写方法的目的是为了多态,或者说:重写是实现多态的前提,即重写是发生在继承中且是针对非static方法的. 语法上子类允许出现和父类只有方法体不一样其他都一模一样的static方法,但是在父类引用指向 ...

  4. 对前台传过来的实体是否为空 进行为空校验的N种方法

    首先定义一个注解,如下 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import j ...

  5. django 聚合内容 RSS/Atom

    Django提供了一个高层次的聚合内容框架,让我们创建RSS/Atom变得简单,你需要做的只是编写一个简单的Python类. 一.范例 要创建一个feed,只需要编写一个Feed类,然后设置一条指向F ...

  6. MySQL学习(十四)

    utf8的bom问题 在xp下,用记事本创建utf8文件的时候,前面多了3个字节,这3个字节不用来显示,是用来辨识编码用的,EF BB BF告诉记事本,这是utf8编码. 存储引擎和事务简单介绍 引擎 ...

  7. lua --- __newindex 的使用规则

    1.如果 __newindex 是一个函数,在给 table 不存在的字段赋值的时候,会调用这个函数2.如果 __newindex 是一个table,在给 table 不存在的字段赋值的时候,会直接给 ...

  8. Python入门学习指南--内附学习框架

    https://blog.csdn.net/weixin_44558127/article/details/86527360

  9. (GoRails) 使用ActiveStorage给user添加上传头像功能。

    对activestorage的简单使用: 头像库:uifaces.co. 可以使用大量设置好的头像图片. 1.安装avatar rails active_storage:install 2.user ...

  10. p1467 Runaround Numbers

    直接搜就行. #include <iostream> #include <cstdio> #include <cmath> #include <algorit ...