zoukankan      html  css  js  c++  java
  • 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(@" --------------------block调用 基本数据类型--------------------- ");
            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(@" --------------------block调用 __block修饰的基本数据类型--------------------- ");
             
            __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(@" --------------------block调用 指针--------------------- ");
             
            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(@" --------------------block调用 指针并修改值--------------------- ");
             
            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(@" --------------------block调用 __block修饰的指针--------------------- ");
             
            __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(@" --------------------block调用 retain cycle--------------------- ");
             
            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(@" --------------------block调用self--------------------- ");
             
            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对象分别会存储到栈、堆、全局数据区域。

    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(@" --------------------block的存储域 全局块--------------------- ");
             
            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(@" --------------------block的存储域 堆块--------------------- ");
             
            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(@" --------------------block的存储域 栈块--------------------- ");
            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执行期间对象被释放。

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

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

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

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

    参考

  • 相关阅读:
    Struts2之页面取得当前actionName
    Javascript跳转页面和打开新窗口等方法
    数据集+树的一种最简单高效的算法
    TRzCheckTree的使用
    FASTSCRIPT脚本实现多国语言
    econtrol form designer添加三方控件
    内存管理六
    内存管理五
    程序启动时只显示登录窗体
    多标签主界面使用TRzPageControl
  • 原文地址:https://www.cnblogs.com/oc-bowen/p/7797829.html
Copyright © 2011-2022 走看看