iOS block 机制

作者: liuyanhongwl | 来源:发表于2017-08-30 17:51 被阅读240次

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

    • block 与 外部变量
    • block 的存储域:栈块、堆块、全局块

    定义

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

    访问外部变量

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

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

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

    几种演算

    • block调用 基本数据类型
        {
            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对象

        {
            NSLog(@"\n--------------------block的存储域 全局块---------------------\n");
            
            void (^blk)(void) = ^{
                NSLog(@"Global Block");
            };
            blk();
            NSLog(@"%@", [blk class]);
        }
        /*
         结果:输出 __NSGlobalBlock__
         */
        
        /*
         结论:
         全局块:这种块不会捕捉任何状态(外部的变量),运行时也无须有状态来参与。块所使用的整个内存区域,在编译期就已经确定。
         全局块一般声明在全局作用域中。但注意有种特殊情况,在函数栈上创建的block,如果没有捕捉外部变量,block的实例还是会被设置在程序的全局数据区,而非栈上。
         */
    

    2. 在堆上创建的Block对象

        {
            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对象

        {
            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 机制

        本文链接:https://www.haomeiwen.com/subject/cdsxjxtx.html