iOS block 机制

本文要将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执行期间对象被释放。
  • 块的存储域:全局块、栈块、堆块
  • 全局块不引用外部变量,所以不用考虑。
  • 堆块引用的外部变量,不是原始的外部变量,是拷贝到堆中的副本。
  • 栈块本身就在栈中,引用外部变量不会拷贝到堆中。

参考

时间: 2024-10-05 04:24:52

iOS block 机制的相关文章

iOS block从零开始

iOS block从零开始 在iOS4.0之后,block横空出世,它本身封装了一段代码并将这段代码当做变量,通过block()的方式进行回调. block的结构 先来一段简单的代码看看: void (^myBlock)(int a) = ^(int a){ NSLog(@"%zd",a); }; NSLog(@"旭宝爱吃鱼"); myBlock(999); 输出结果: 2016-05-03 11:27:18.571 block[5340:706252] 旭宝爱吃鱼

iOS block并发

iOS block并发 2012-06-13 09:31 1351人阅读 评论(0) 收藏 举报 iosuiviewnetwork任务threadimage 这篇文章转自 http://anxonli.iteye.com/blog/1097777,集中与iOS的多核编程和内存管理,大家完全可以使用苹果的多核编程框架来写出更加responsive的应用. 多核运算 在iOS中concurrency编程的框架就是GCD(Grand Central Dispatch), GCD的使用非常简单.它把任务

《转之微信移动团队微信公众号》iOS 事件处理机制与图像渲染过程

致歉声明: Peter在开发公众号功能时触发了一个bug,导致群发错误.对此我们深表歉意,并果断开除了Peter.以下交回给正文时间: iOS 事件处理机制与图像渲染过程 iOS RunLoop都干了什么 iOS 为什么必须在主线程中操作UI 事件响应 CALayer CADisplayLink 和 NSTimer iOS 渲染过程 渲染时机 CPU 和 GPU渲染 Core Animation Facebook Pop介绍 AsyncDisplay介绍 参考文章 iOS RunLoop都干了什

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

iOS 事件处理机制与图像渲染过程 iOS RunLoop都干了什么 iOS 为什么必须在主线程中操作UI 事件响应 CALayer CADisplayLink 和 NSTimer iOS 渲染过程 渲染时机 CPU 和 GPU渲染 Core Animation Facebook Pop介绍 AsyncDisplay介绍 参考文章 iOS RunLoop都干了什么 RunLoop是一个接收处理异步消息事件的循环,一个循环中:等待事件发生,然后将这个事件送到能处理它的地方. 如图1-1所示,描述了

深入浅出iOS事件机制

深入浅出iOS事件机制 2015年 04月 12日 本文章将讲解有关iOS事件的传递机制,如有错误或者不同的见解,欢迎留言指出.转载自:http://zhoon.github.io/ios/2015/04/12/ios-event.html iOS的事件有好几种:Touch Events(触摸事件).Motion Events(运动事件,比如重力感应和摇一摇等).Remote Events(远程事件,比如用耳机上得按键来控制手机),其中最常用的应该就是Touch Events了,基本存在于每个a

写给喜欢用Block的朋友(ios Block)

作者:fengsh998 原文地址:http://blog.csdn.net/fengsh998/article/details/38090205 转载请注明出处 如果觉得文章对你有所帮助,请通过留言或关注微信公众帐号fengsh998来支持我,谢谢! 本文不讲block如何声明及使用,只讲block在使用过程中暂时遇到及带来的隐性危险. 主要基于两点进行演示: 1.block 的循环引用(retain cycle) 2.去除block产生的告警时,需注意问题. 有一次,朋友问我当一个对象中的b

IOS通知机制初解

消通知机制: 3个步骤: 1.通知的发布 2.通知的监听 3.通知的移除 需要了解的要点 1.通知中心:(NSNotificationCenter) 每一个应用程序都有一个通知中心(NSNotificationCenter)实例,专门负责协助不同对象之间的消息通信 任何一个对象都可以向通知中心发布通知(NSNotification),描述自己在做什么.其他感兴趣的对象(Observer)可以申请在某个特定通知发布时(或在某个特定的对象发布通知时)收到这个通知 2.通知:(NSNotificati

iOS事件机制(一)

iOS事件机制(一) DEC 7TH, 2013 运用的前提是掌握掌握的本质是理解 本篇内容将围绕iOS中事件及其传递机制进行学习和分析.在iOS中,事件分为三类: 触控事件(单点.多点触控以及各种手势操作) 传感器事件(重力.加速度传感器等) 远程控制事件(远程遥控iOS设备多媒体播放等) 这三类事件共同构成了iOS设备丰富的操作方式和使用体验,本次就首先来针对第一类事件:触控事件,进行学习和分析. Gesture Recognizers Gesture Recognizers是一类手势识别器

iOS事件机制(二)

iOS事件机制(二) DEC 29TH, 2013 本篇内容接上一篇iOS事件机制(一),本次主要介绍iOS事件中的多点触控事件和手势事件. 从上一篇的内容我们知道,在iOS中一个事件用一个UIEvent对象表示,UITouch用来表示一次对屏幕的操作动作,由多个UITouch对象构成了一个UIEvent对象.另外,UIResponder是所有响应者的父类,UIView.UIViewController.UIWindow.UIApplication都直接或间接的集成了UIResponder.关于