Objective-C的lambda表达式block

本篇博文介绍iOS4引入的C语言扩展功能的“Blocks”。如果学习过lambda表达式,那么对这个知识在使用上并不难。但是这次介绍的blocks和C++中lambda表达式又一些区别。
(博文较长,层层递进的方式阐述,如有需要,选择阅读)

1. 面试题

1)block的实质是什么?一共有几种block?都是什么情况下生成的?
2)为什么在默认情况下无法修改被block捕获的变量? __block都做了什么?
3)下面的代码输出是?

void test1() {
    int a = 10;
    void (^block)() = ^{
        NSLog(@"a is %d", a);
    };
    a = 20;//a的值改变之后,block应该输出什么?
    block(); 
}

上述面试题会在后面的介绍中一一解决。

2. Blocks模式

2.1 block语法

在C++、Java等大多数编程语言中,都有lambda表达式,这是叫的名称不一样,如闭包,block等。
在objective-c中,其格式为:

^ 返回值 参数列表 {代码块/表达式};

例如:

^ BOOL (int left, int right){
	return a > b;
}

如果没有返回值、参数列表,可以用void代替,也都可以将其省略。如:

^{NSLog(@"block的使用")}

2.2 block简单理解

下面来看一下C语言里面的指针。

int (*funcPtr)(int num);
int Func(int a) {
	printf("%d",a);
	return a;
}
funcPtr = Func;
(*funcPtr)(10);

现在把 * 变成 ^。

int (^funcPtr)(int num);

上述的int (^funcPtr)(int num)就是一个block类型。于是可以这样使用。

int (^funcPtr)(int num) = ^int(int a) {
	NSLog(@"%d",a);
	return a;
}
funPtr(10);

于是对于block来说,很类似一个函数指针,它也可以使用typedef来提高代码的阅读质量,便于理解。
对与函数指针来说:

typedef char (*FUNPTR)(int); 
PTRFUN funPtr;//定义一个函数指针变量 

对于block来说

typedef char (^blk_t)(int);
blk_t blk;//定义一个block变量

综上,block就是一个类型,这个类型组要是组织和描述代码块,这个被描述的对象就是一个block对象,但是这个对象有的特殊。

2.3 block的使用

介绍完基本的语法,现在来使用和学习block的特殊在哪。

2.3.1 无参无返回值的定义和使用

void (^blockNothing)(void) = ^ {
        NSLog(@"无参无返回值的定义和使用");
    };

2.3.2 无参有返回值的定义和使用

int (^blockWithRet)(void) = ^int {
        NSLog(@"无参无返回值的定义和使用");
        return 1;
    };

2.3.3 有参无返回值的定义和使用

void (^blockWithParm)(int parm) = ^(int count) {
        NSLog(@"无参无返回值的定义和使用");
    };

2.3.4 有参有返回值的定义和使用

int (^blockNor)(int parm) = ^ int (int parm) {
        NSLog(@"无参无返回值的定义和使用");
        return parm;
    };

2.3.5 做函数参数

void funcWithParm(int (^block)(int)) {
    
}
但是通常都会用typedef提高阅读品质
typedef int (^block_t)(int);
void funcWithParm(block_t blk) {
    
}

2.3.6 做返回值

typedef int (^block_t)(int);
block_t funcWithRet() {
    return ^int(int parm)  {
        return parm;
    };
}

有了上面的基础,就可以解决面试题(1)的一个小问题一共有几种block?其实就是问一个变量可以有几种形式。对于一个变量来说,它可以存在数据段(如全局变量、静态变量等),也可以在栈上(临时变量、参数等),也可以在堆上(malloc,alloc等)。于是block变量也不例外,也是上述的三种。对于block来说,有三种标记来标示三种block。

类型存储位置
_NSConcreteStackBlock
_NSConcreteGlobalBlock数据段
_NSConcreteMallocBlock

3、block的实现

前面说过block经过组织和描述,现在就来看看是怎么组织和描述的,并且这到底是个什么变量。

3.1 C++代码

下面是Objectiv-C的代码

int main(int argc, const char * argv[]) {
    void (^block)(void) = ^{
        printf("Block\n");
    };
    block();
    return 0;
}

将其转换为C++代码如下:(在当前目录下,在终端上输入xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m)

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
// 描述block的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// __cself指针指向的是一个__main_block_impl_0对象,其实传入是一个block对象,
// 由于最终是block自己回调那个这个函数,因此这个__cself可以看作是self指针。
        printf("Block\n");
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(int argc, const char * argv[]) {
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

3.2 简单block的数据结构

// block 类,类似一个抽象类,任何block都会有这个类的对象作为成员
struct __block_impl {
      void *isa; // 指向存储域类型,类似指向类对象
      int Flags; // 标志变量通常
      int Reserved; // 保留变量
      void *FuncPtr; // 函数指针
};  

// 用来描述block的类
static struct __main_block_desc_0 { 
  size_t reserved; // 保留值
  size_t Block_size; // block大小
} 

// 可以理解为一个block子类,0表示第几个block
// 只是通过组合实现的类似继承的效果,是has-a,不是is-a
struct __main_block_impl_0 {
  struct __block_impl impl; // 类似父类
  struct __main_block_desc_0* Desc; // 描述,这个block的大小
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

下面在main函数中,找到这个block是怎么初始化的

void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

// 简化一下,去除强转:
// 好像也没做啥,就是初始化了一个对象,然后调用内部函数指针执行的函数
void (*block)(void) = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA);

block->FuncPtr(block);

3.3 block截获变量的数据结构

先来贴上如下代码,将全局变量,静态局部变量,局部变量定义。

int globalVar = 10;
int main(int argc, const char * argv[]) {
    static int staticVar = 20;
    int norVar = 30;
    void (^block)(void) = ^{
        printf("%d %d %d\n",globalVar, staticVar, norVar);
    };
    block();
    return 0;
}

转换为C++代码后:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *staticVar; // 静态变量指针
  int norVar; // 局部变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_staticVar, int _norVar, int flags=0) : staticVar(_staticVar), norVar(_norVar) {
  //构造函数多了两个参数
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
// 描述block的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
		// 静态局部变量通过地址进行访问
		int *staticVar = __cself->staticVar; // bound by copy
		// 局部变量传值访问
		int norVar = __cself->norVar; // bound by copy
        printf("%d %d %d\n",globalVar, (*staticVar), norVar);
    }

可以发现block截获了使用了的变量,方式就是创建自己的成员变量,并用截获的值进行赋值,并且规律是:

  • 不截获全局变量
  • 截获静态变量的地址
  • 截获局部变量的值。

一般我们可能回在block 中改变我们截获的值,比如改变局部变量。

int globalVar = 10;
int main(int argc, const char * argv[]) {
    static int staticVar = 20;
    int norVar = 30;
    void (^block)(void) = ^{
        ++norVar;
        printf("%d %d %d\n",globalVar, staticVar, norVar);
    };
    block();
    return 0;
}

于是回出现下面的错误。
Variable is not assignable (missing __block type specifier)
但是改变其他变量的时候,又不会出现这样的问题。那么现在看到C++代码之后就明朗了,因为局部变量是指传递的,不能在内部进行修改。这就解决了面试题(2)的第一问为什么在默认情况下无法修改被block捕获的变量? 但是如果非要修改,可以用关键字__block
但是在这之前,先来看看面试题(3)。

void test1() {
    int a = 10;
    void (^block)() = ^{
        NSLog(@"a is %d", a);
    };
    a = 20;
    block(); 
}

因此可以轻易的得出该变量的值是 10.

3.4 __block关键字

把上面的局部变量,改成下面这样,就不会产生错误。现在又来转换成C++代码来解析。

__block int norVar = 30;

直接分割数据结构解析。

#pragma clang assume_nonnull end
int globalVar = 10;//全局变量
// __block关键字描述的变量最终会被描述和组织成一个类
struct __Block_byref_norVar_0 {
  void *__isa;
__Block_byref_norVar_0 *__forwarding; // 这里有一个自身类型的指针,类型链表的next指针一样
 int __flags;
 int __size;
 int norVar; // 存储的值
};
//block变量
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *staticVar;
  __Block_byref_norVar_0 *norVar; // __block对象的地址
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_staticVar, __Block_byref_norVar_0 *_norVar, int flags=0) : staticVar(_staticVar), norVar(_norVar->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

看一下代码块这个的描述方式。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_norVar_0 *norVar = __cself->norVar; // bound by ref
  int *staticVar = __cself->staticVar; // bound by copy
		// 重点:对象->自己->值,这里会不会多此一举呢?后面解释
        ++(norVar->__forwarding->norVar);
        printf("%d %d %d\n",globalVar, (*staticVar), (norVar->__forwarding->norVar));
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->norVar, (void*)src->norVar, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->norVar, 8/*BLOCK_FIELD_IS_BYREF*/);}
// 描述中新增了上面两个函数的指针
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

//main函数
int main(int argc, const char * argv[]) {

    static int staticVar = 20;
    __attribute__((__blocks__(byref))) __Block_byref_norVar_0 norVar = {(void*)0,(__Block_byref_norVar_0 *)&norVar, 0, sizeof(__Block_byref_norVar_0), 30};

    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &staticVar, (__Block_byref_norVar_0 *)&norVar, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

main函数中有对__block类的一个对象的初始化。

__attribute__((__blocks__(byref))) __Block_byref_norVar_0 norVar = {(void*)0,(__Block_byref_norVar_0 *)&norVar, 0, sizeof(__Block_byref_norVar_0), 30};
简化一下:
__Block_byref_norVar_0 norVar = {
				 0,
				 &norVar, // 传递的是自己的地址
				 0, 
				 sizeof(__Block_byref_norVar_0), 
				 30
}

总结一下:在它的成员变量里面有一个__forwarding指针指向了自己。于是访问的时候,norVar对象找到自己的__forwarding成员,从而找到norVar的值。这样是不是感觉多此一举,为什么不直接访问这个norVar的值?这个就要说一下block的存储域了。
到这里就解决了面试题(2)的第二个问题__block都做了什么?

++(norVar->__forwarding->norVar);

3.5 存储域

在一开始我们知道一共有三种block,就像变量一样。因此对于block的这三个存储域可以这样理解:当我门创建一个全局变量的时候,那么这个变量的就在数据段上,创建局部变量、返回值的时候是在栈上,block是一样的:

void (^globalBlock)(void) = ^{
    printf("_NSConcreteGlobalBlock\n");
};
int main(int argc, const char * argv[]) {
    
    void (^stackBlock)(void) = ^{
        printf("_NSConcreteStackBlock\n");
    };
    
    globalBlock();
    stackBlock();
    
    return 0;
}

但是对于_NSConcreteMallocBlock来说,我们是创建不出来的。就像我们不能直接创建出一个存在堆上的int型变量,但是可以通过复制来存在堆上。

例如,我们malloc一个int大小的空间,然后将一个int的值复制进去。

Blocks提供了将Block和__block变量从栈上复制到堆上的方法。之所以复制,是为了解决栈上的block的生命周期短的问题。复制到堆上所以Block将_NSConcreteMallocBlock赋值给impl.isa
下面举一个例子。

typedef void (^blk_t)(int);
blk_t testFunc(int rate) {
    return ^(int parm){
        printf("return a block with parm %d\n", parm * rate);
    } ;
}
int main(int argc, const char * argv[]) {
    blk_t res = testFunc(10);
    res(10);
    return 0;
}

在ARC环境下,这个是没有问题的,因为ARC会自动加入copy操作。但是非ARC环境下:

error:
      returning block that lives on the local stack
    return ^(int parm){
           ^~~~~~~~~~~~
1 error generated.

修改如下:

typedef void (^blk_t)(int);
blk_t testFunc(int rate) {
    return [^(int parm){
        printf("return a block with parm %d\n", parm * rate);
    } copy];
}
int main(int argc, const char * argv[]) {
    blk_t res = testFunc(10);
    res(10);
    return 0;
}

在其他变量的情况下,我们是不用在意复制这个问题的,因为会返回一个临时变量进行赋值,但是block是一个特殊的变量,是一段代码。满足下面的条件编译器会自动帮助我们进行添加copy操作:
1)Block作为返回值的时候
2)将Block赋值给附有__strong修饰符id类型或Block成员变量的时候
3)不进行手动添加的情况如下:向方法或函数的参数中传递Block时(除在方法名含有usingBlock的Cocoa框架方法或Grand Central Dispatch 的API、ARC环境外)
举个例子:

@interface BlockClass : NSObject
-(id)getBlockArray;
@end
@implementation BlockClass
-(id)getBlockArray {
    int tmpVal = 0;
    return [[NSArray alloc]initWithObjects: ^{
        printf("%d\n", tmpVal);
    },
  nil];
}
@end
int main(int argc, const char * argv[]) {
   
    BlockClass *blockclass = [BlockClass new];
    void (^block)(void) =  [[blockclass getBlockArray] objectAtIndex:0];
    block();
    return 0;
}

上述代码会发生崩溃。
在这里插入图片描述
在ARC环境下还是会自动添加copy。
在这里插入图片描述
这样修改后就不会了。

-(id)getBlockArray {
    int tmpVal = 0;
    return [[NSArray alloc]initWithObjects: [^{
        printf("%d\n", tmpVal);
    } copy],
  nil];
}

3.6 __forwarding

下面改说一下__block了。还记得__block的结构体里面有一个__forwarding的指针吗?这个指针就是在__block在被复制到堆上的时候发生作用。由于Block中使用了__block变量,当Block被复制到了堆上的时候,__block也要被复制到堆上。
如果多个BLock中使用了同一个__block变量,__block会被第一次被使用的时候复制到堆上,然后增加引用计数。
在这里插入图片描述
当block复制到堆上后,__block变量的数据结构会发生下面的变化。
在这里插入图片描述
于是无论是在block中,block外使用__block变量,又或者__block在堆上或者在栈上,都可以访问同一个__block变量。

4. Block的循环引用

现在来到Block的最后一个内容,循环引用。在说到Objective-C的内存管理的时候,也说到了循环引用的问题。Block的循环引用的问题用一个例子来说明:

/**
 循环引用
 */
typedef void (^blk_t)(void);
@interface MyClass : NSObject
@property(nonatomic, strong) blk_t block;
@end

@implementation MyClass
-(instancetype) init {
    self = [super init];
    _block = ^{
        NSLog(@"%@",[self class]);
    };
    return self;
}
-(void)dealloc {
    NSLog(@"dealloc");
}
@end
int main(int argc, const char * argv[]){
    MyClass *obj = [MyClass new];
    NSLog(@"%@", [obj class]);
    return 0;
}

在这里插入图片描述
没有调用dealloc。
这是因为MyClass对象堆Block时强引用,而Block使用了__strong修饰符id类型self。并且BLock由于满足Block赋值给了作为成员变量的block,因此复制到了堆上。如此:block持有self,self持有block。
其实编译器都提醒了:
在这里插入图片描述
更改方法为:

-(instancetype) init {
    self = [super init];
    id __weak tmpSelf = self;
    _block = ^{
        NSLog(@"%@",[tmpSelf class]);
    };
    return self;
}

5. 结语

到此block语法结束。本篇博文内容较多,有些地=地方可能存在错误和不足,也有一些地方介绍的不好,但这都是学习block的心得与经验,望前辈与大佬不吝指正与批评。后续还会对内容进行精练与添加……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值