弄透Block

目标

  • Block是什么?
  • 总结Block的使用场景
  • 为什么Block属性需要用copy修饰?
  • __block修饰后为何就可以修改?
  • Block循环引用是怎么产生的?

Block是什么?

Block:带有自动变量的匿名函数,它是C语言的拓展功能,之所以是扩展,是因为C语言不允许存在这样的匿名函数

  1. 匿名函数

匿名函数是指不带函数名称的函数

  1. 带有自动变量

这是因为Block拥有捕获外部变量的功能,在Block中访问一个外部的局部变量,Block会持有它的临时状态,自动捕获变量值,外部局部变量的变化不会影响它的状态

int val = 10;
void (^blk)(void) = ^{
  printf("val=%d\n", val);
};
val = 2;
blk(); // 这里输出的值是10,而不是2,因为block在实现时就会对它所在方法中定义的栈变量进行一次只读拷贝
复制代码
  1. 为了解决block不能修改自动变量的值,可以使用 __block 修饰
__block int val = 10;
void (^blk)(void) = ^{
 printf("val=%d\n", val);
};
val = 2;
blk(); // 这里输出的值是2
复制代码

Block的使用场景

  1. 声明Block属性 利用Block属性响应事件或传递数据

UIViewController 需要监听TableView中Cell的某个按钮的点击事件,既可以通过Delegate回调,也可以利用Block回调 Block回调的思路: 声明一个Block属性,注意这里要用copy。 利用Block属性进行回调

  1. 方法参数为Block 利用Block实现回调

[UIView animateWithDuration:animations:] 为例,animations是一个block对象,利用block实现调用者与UIView之间的数据传递

  1. 链式语法

链式编程思想:将block作为方法的返回值,且返回值的类型为调用者本身,并将该方法以setter的形式返回,从而实现连续调用

//  CaculateMaker.h
//  ChainBlockTestApp

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface CaculateMaker : NSObject

@property (nonatomic, assign) CGFloat result;

/*
* 返回类型 CaculateMaker
* 传入参数 CGFloat num
*/
- (CaculateMaker *(^)(CGFloat num))add;

@end


//  CaculateMaker.m
//  ChainBlockTestApp


#import "CaculateMaker.h"

@implementation CaculateMaker

- (CaculateMaker *(^)(CGFloat num))add;{
    return ^CaculateMaker *(CGFloat num){
        _result += num;
        return self;
    };
}

@end

// 使用
CaculateMaker *maker = [[CaculateMaker alloc] init];
maker.add(20).add(30);
复制代码

为什么Block属性需要用copy修饰?

因为在MRC情况下如果Block属性不使用copy修饰,在使用中会出现崩溃,在ARC情况下,Block属性使用strong修饰会被默认进行copy,所以ARC情况下,Block属性可以使用strong或copy修饰,不然会出现崩溃。
**
为何会有这种现象出现?

Block在内存中的位置分为三种类型:

  • NSGlobalBlock 是位于全局区的block,它是设置在程序的数据区中。
  • NSStackBlock 是位于栈区,当超出变量作用域时,栈上的Block以及__block变量都会被销毁。
  • NSMallocBlock 是位于堆区,在变量作用域结束时不受影响。

这三种类型对应以下三种情况:

  1. Block中没有截获自动变量时Block类型是__NSGlobalBlock__
  2. Block中截获自动变量时Block类型是__NSStackBlock__
  3. 堆中的Block无法直接创建,当对__NSStackBlock__类型的Block进行copy时,会将Block放到堆中,Block类型变为__NSMallocBlock__

当Block类型是__NSStackBlock__时,一旦超出了变量作用域,栈上的Block以及__block变量就会被销毁,从而导致调用Block回调时崩溃。因此,Block属性需要用copy修饰来避免这种情况。

- (void)click:(id)sender {
        TestClass *test = [[TestClass alloc] init];
        
        __block int a = 1;
        // 弱引用,block类型是__NSStackBlock__  当TestClass执行回调时必崩 EXC_BAD_ACCESS
        test.weakBlock = ^() {
            NSLog(@"ok");
            a = 2;
        };
        
        // block类型是__NSStackBlock__  当TestClass执行回调时必崩 EXC_BAD_ACCESS
        test.assignBlock = ^() {
            NSLog(@"ok");
            a = 3;
        };
        
        // block类型是__MallocBlock__ 正常执行
        test.copyBlock = ^() {
            NSLog(@"ok");
            a = 4;
        };
        
        // block类型是__MallocBlock__ 正常执行
        test.strongBlock = ^() {
            NSLog(@"ok");
            a = 5;
        };
        
        NSLog(@"copy property: %@", test.copyBlock);
        NSLog(@"assign property: %@", test.assignBlock);
        NSLog(@"weak property: %@", test.weakBlock);
        NSLog(@"strong property: %@", test.strongBlock);
        
        
        [test start];
    }
复制代码

__block修饰后为何就可以修改局部变量?

首先,需要弄明白一个概念,static声明的静态局部变量是可以在block内进行修改的,为何会有这种区别呢?
因为,静态局部变量存在于应用程序的整个生命周期,而非静态局部变量仅存在于一个局部上下文中,绝大多数情况下,block都是延后执行的,这就有可能出现非静态局部变量被回收的情况,为了避免这个问题,苹果在设计block时,block中非静态局部变量是值传递,这也解释了为何block会持有变量的临时状态,后续再修改,block中的变量值也不再改变。

加上__block修饰的局部变量,被block捕获时,就不再是传递局部变量的值了,而是变成了一个结构体实例。比如:
定义一个 __block int a = 10;  会变成 __Block_byref_a_0 *a; 
__Block_byref_a_0 的结构体如下所示:

struct __Block_byref_a_0 {
 void *__isa;
 __Block_byref_a_0 *__forwarding; // forwarding指针
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 int a; // 原变量同类型变量
};
复制代码

结构体中有一个forwarding指针,此指针指向转换后变量本身,结构体中也有一个原变量一样类型的变量。此后代码中涉及到原变量的地方,都会转换成新变量->forwarding->原变量同类型变量

如果在block中直接修改变量的值,实质的过程是新变量->__forwarding->原变量同类型变量,最终修改的其实是结构体中原变量同类型变量,很明显这个结构体内的变量已经不属于block的外部变量了,所以能在block内修改。

这个新变量也是非静态局部变量,所以如果没有copy,block执行时,新变量有可能已经被栈回收

总结:
block修饰的变量转换成了结构体,结构体内有一个forwarding指针和一个与原变量相同类型的成员变量,forwarding指针指向结构体内的成员变量。无论在block内外,都是通过forwarding来访问的

Block的循环引用是怎么产生的?

我们先看一段block导致循环引用的代码:

TestClass *test = [[TestClass alloc] init]; 

test.copyBlock = ^() {
    NSLog(@"ok: %d", test.result);
};
复制代码

当我们写完这段代码后,Xcode就会提醒我们,这段代码存在循环引用,事实上也确实存在循环引用。接下来我们就来分析一下为什么会产生循环引用。
test的属性block强引用了SecondViewController中的block,SecondViewController中的block又强引用了test的属性result,从而导致了循环引用。

并非所有的block都存在循环引用,下面列举一些常见的block使用的示例:

// self-->requestModel-->block-->self 
[self.requestModel requestData:^(NSData *data) {
    self.name = @"leafly";
}];

// 虽然存在引用环,但是通过主动释放requestModel打破了循环
[self.requestModel requestData:^(NSData *data) {
    self.name = @"leafly";
    self.requestModel = nil;
}];

// t-->block-->self 不存在循环引用
Test *t = [[Test alloc] init];

[t requestData:^(NSData *data) {
    self.name = @"leafly";
}];

// AFNetworking-->block-->self 不存在循环引用
[AFNetworking requestData:^(NSData *data) {
    self.name = @"lealfy";
}];

复制代码

转载于:https://juejin.im/post/5cff063551882565b46023b0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值