转载

iOS block 机制

  • 本文为CocoaChina网友liuyanhongwl投稿

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

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

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

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

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

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

参考

正文到此结束
Loading...