【iOS】—— 初识block

block

什么是block?

Blocks是带有自动变量(局部变量)的匿名函数。

block语法

标准格式:
在这里插入图片描述

例子:

^int (int count) {
	return count + 1;
}

返回值类型默认void,如果是void 我们也可以默认省略(void)。
在这里插入图片描述

Block变量

在c中,我们可以将函数地址赋值给函数指针类型,同理,Block语法也可以赋值给Block类型的变量,声明Block类型变量示例如下:

int (^blk) (int);

Block类型变量和一般变量完全相同,可以作为以下用途:

  • 自动变量
  • 函数参数
  • 静态变量
  • 静态全局变量
  • 全局变量

将Block赋值为Block类型变量:

void (^blk) (void) = ^{
	
};

函数中Block作为参数:

int func(int (^blk) (int));

在函数返回值中指定Block类型,可以将Block作为返回值:

blk func() {
    return ^(void){
        printf("!");
    };
}

截获自动变量值

通过Block语法和Block类型变量的说明,我们己经理解了“ 带有自动变量值的匿名函数” 中的“匿名函数”。而“带有自动变量值” 究竟是什么呢?“带有自动变量值” 在Blocks中表现 为“ 截获自动变量值”。截获自动变量值的实例如下:

void interceptAutomaticVariable() {       //截获自动变量
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    void (^blk)(void) = ^{
        printf(fmt, val);
    };
    val = 2;
    fmt = "These values were changed. val = %d\n";
    blk();
}

该源代码中,Block 语法的表达式使用的是它之前声明的自动变量fmt 和val 。Blocks 中, Block 表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值。因为Block 表达式保存 了自动变量的值,所以在执行Block 语法后,即使改写Block 中使用的自动变量的值也不会影响 Block 执行时自动变量的值。该源代码就在Block语法后改写了Block中的自动变量val 和fmt。 下面我们一起看一下执行结果:
在这里插入图片描述
执行结果并不是改写后的值“These values were changed. val= 2”,而是执行Block语法时的自动变量的瞬间值。该Block语法在执行时,字符串指针“val = %d” 被赋值到自动变量fmt中,int 值10被赋值到自动变量val 中,因此这些值被保存(即被截获),从而在执行块时使用。

__block说明符

实际上,自动变量值截获只能保存执行Block 语法瞬间的值。保存后就不能改写该值。下面我们来尝试改写截获的自动变量值,看看会出现什么结果。下面的源代码中,Block 语法之前声明的自动变量val 的值被赋予1。

void the__block() {     //__block测试
    int val = 0;
    void (^blk) (void) = ^{
        val = 1;
    };
    blk();
    printf("val = %d\n", val);
}

该代码会出现报错:
在这里插入图片描述

这时候就需要给val加上__block关键字了。

void the__block() {     //__block测试
    __block int val = 0;
    void (^blk) (void) = ^{
        val = 1;
    };
    blk();
    printf("val = %d\n", val);
}

输出结果:
在这里插入图片描述

使用附有__block 说明符的自动变量可在Block 中赋值,该变量称为__block 变量。

没有__block 变量时的源代码:

struct __block_impl {
  	void *isa;
  	int Flags;
  	int Reserved;
  	void *FuncPtr;
};

struct __main_block_impl_0 {
  	struct __block_impl impl;
  	struct __main_block_desc_0* Desc;
  	const char *fmt;
  	int val;
  	__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
    	impl.isa = &_NSConcreteStackBlock;
    	impl.Flags = flags;
    	impl.FuncPtr = fp;
    	Desc = desc;
  	}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself {
  	const char *fmt = __cself->fmt;
  	int val = __cself->val; 
  
	printf(fmt, val);
}

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[]) {
	int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
    return 0;
}

通过上述两个源代码的对比发现,这里多了val,fmt这两个变量,这就是block截获的变量,其实截获的本质就是通过结构体将截获变量的值存储起来,就是因为在定义的时候已经将值存储起来了,所以你之后无论再怎么改变改变了的值他都不会受影响。要注意的是,Block语法表达式中没有使用的自动变量不会被追加,即代码块中没有涉及到的变量是不会被截获的,就像这里的变量dmy。

添加__block之后的源代码:

//__block说明符修饰后的变量的结构体
struct __Block_byref_val_0 {
	void *__isa;  //指向所属类的指针
	__Block_byref_val_0 *__forwarding;  //指向自己的内存地址的指针
	int __flags;  //标志性参数,暂时没用到所以默认为0
	int __size;  //该结构体所占用的大小
	int val;  //该结构体存储的值,即原变量的赋的值
};

//block本体
struct __main_block_impl_0 {
	struct __block_impl impl;  //block的主体
	struct __main block desc 0* Desc;  //存储该block的大小
	__Block_byref_val_0 *val;  //__block修饰的变量的值
	//构造函数
	__main_block_impl_0(void *fp, struct __main_block_desc 0 *desc, __Block_byrefval_0 *_val, int flags=0) : val(_val->__forwarding) {
	impl.isa = &_NSConcreteStackBlock;
	impl.Flags = flags;
	impl.FuncPtr = fp;
	Desc = desc;
};

//封装的block逻辑,存储了block的代码块
static void __main_block_func_0(struct__main_block_impl_0 *_cself) {
	__Block_byref_val_0 *val =__cself->val;
	
	(val->__forwarding->val) = 1;
}
static void_main_block_copy_0(struct __main_block_impl_0* dst, struct __main_block_impl_0* src) {
    //根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
	_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}

static void __main_block_dispose_0(struct __main_block_imp1_0* src) {
    //自动释放引用的auto变量(相当于release)
	_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}

static struct __main_block_desc_0 {
	unsigned long reserved;  //保留字段
	unsigned long Block_size;  //block大小
	void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);  //copy的函数指针,下面使用构造函数进行了初始化
	void (*dispose)(struct __main_block_impl_0*);  //dispose的函数指针,下面使用构造函数进行了初始化
}
    //构造函数,初始化保留字段、block大小及两个函数
    __main_block_desc_0_DATA = {
	0,
	sizeof(structmain_block_impl_0),
	__main_block_copy_O, 
	__main_block_dispose_0
};
int main() {
    //之前的 __block int val = 10;变成了结构体实例
	struct __Block_byref_val_0 val = {
		0,  //isa指针
		&val,  //指向自身地址的指针
		0,  //标志变量
		sizeof(__Block_byref_val_0),  //block大小
		10  //该数据的值
	};
	blk = &__main_block_impl_0(
__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);

	return 0;
}

通过上述的代码和之前代码进行比对,发现使用__block说明符修饰的变量变成了一个结构体,它是利用这个结构体来进行存储数据的。

__block变量用结构体成员变量__forwarding可以实现无论__block变量配置在栈上还是堆上时都能够正确的访问__block变量。

__block变量的__Block_byref_val_0结构体并不在Block用__main_block_impl_0结构体中,这是为了在多个Block中使用__block变量,即共用这个结构体。

具体来看下图:
请添加图片描述
分析:

  • 我们上面block内部拿到的变量实际就是在堆上的。当block进行copy被复制到堆上时,_Block_object_assign函数内会做这一系列操作 会改变 __forwarding 指向
  • 当block在栈时,__Block_byref_a_0结构体内的__forwarding指针指向结构体自己
  • 当block被复制到堆中时,栈中的__Block_byref_age_0结构体也会被复制到堆中一份,而此时栈中的__Block_byref_a_0结构体中的__forwarding指针指向的就是堆中的__Block_byref_a_0结构体,堆中__Block_byref_a_0结构体内的__forwarding指针依然指向自己
  • 通过__forwarding指针巧妙的将修改的变量赋值在堆中的__Block_byref_a_0中

截获的自动变量

那么截获Objective-C 对象,调用变更该对象的方法也会产生编译错误吗?

id array = [[NSMutableArray alloc] init];
    void (^blk) (void) = ^ {
        id obj = [[NSObject alloc] init];
        [array addObject:obj];
    };

这是没有问题的,而向截获的变量array 赋值则会产生编译错误。该源代码中截获的变量值 为NSMutableArray 类的对象。如果用C语言来描述,即是截获NSMutableArray类对象用的结构体实例指针。虽然赋值给截获的自动变量array 的操作会产生编译错误,但使用截获的值却不会有任何问题。下面源代码向截获的自动变量进行赋值,因此会产生编译错误。

    id array2 = [[NSMutableArray alloc] init];
    void (^blk2) (void) = ^ {
        array2 = array;
    };

这时候就会有和之前类似的错误:
在这里插入图片描述

block的三种存储类型

  • _NSConcreteStackBlock(栈区)
  • _NSConcreteGlobalBlock(数据区)
  • _NSConcreteMallocBlock(堆区)

在这里插入图片描述

那么三种储存方式有什么区别呢?
简单来说,没有捕获自动变量的就是数据区,捕获了自动变量但是没有进行copy操作就是栈区,copy之后就变成了堆区。

NSGlobalBlock

如果一个 block 没有访问外部局部变量,或者访问的是全局变量,或者静态局部变量,此时的 block 就是一个全局 block ,并且数据存储在全局区。

//block1没有引用到局部变量
    int a = 10;
    void (^block1)(void) = ^{
         NSLog(@"hello world");
    };
    NSLog(@"block1:%@", block1);

    //    block2中引入的是静态变量
    static int a1 = 20;
    void (^block2)(void) = ^{
        NSLog(@"hello - %d",a1);
    };
    NSLog(@"block2:%@", block2);

在这里插入图片描述

NSStackBlock

    int b = 10;
    NSLog(@"%@", ^{
        NSLog(@"hello - %d",b);
    });

输出结果:
在这里插入图片描述

我们神奇的发现,按照概念上所说写出的为什么不是NSStackBlock呢,这个问题困扰了我很久,经过查阅了很多资料后明白,在ARC环境下,系统会自动将block进行拷贝操作,我们改成MRC试试,果然,结果正确了。
那么在ARC下怎么让block存储在栈上呢?

    int c = 10;
    NSLog(@"%@", (__weak)^{
        NSLog(@"hello - %d",c);
    });

我们可以这么做,在之前我们学习过weak关键字,weak关键字的作用是弱引用避免循环引用
我们来看下面一段话:
在这里插入图片描述

基本可以明白,weak避免了循环引用。

除此之外还发现了一个神奇的问题:
我们分别打印这四个值:

    int b = 10;
    NSLog(@"%@", ^{
        NSLog(@"hello - %d",b);
    });
    
    int c = 10;
    NSLog(@"%@", (__weak)^{
        NSLog(@"hello - %d",c);
    });
    
    int a = 10;
    NSLog(@"%@", [^{
        NSLog(@"hello - %d",a);
    } description]);
    
    int age = 15;
    NSLog(@"%@", [^{
        NSLog(@"block----%d", age);
    } class]);

在这里插入图片描述

这个东西真的蛮抽象的,只有第一个打印了malloc类型,其他都是stack类型,这又是什么原因呢?

这个问题我也解释不太清楚,等后面学清楚了再来补充,敬请期待······

补充:

什么时候栈上的 Block 会复制到堆呢?
  • 调用Block的copy实例方法时
  • Block作为函数返回值返回时
  • 将Block 赋值给附有__strong修饰符id类型的类或Block类型成员变量时
  • 在方法名中含有usingBlock的Cocoa框架方法或 Grand Central Dispatch的 API 中传递 Block 时

NSMallocBlock

    int a = 10;
    void (^block1)(void) = ^{
        NSLog(@"%d",a);
    };
    NSLog(@"block1:%@", [block1 copy]);

    __block int b = 10;
    void (^block2)(void) = ^{
        NSLog(@"%d",b);
    };
    NSLog(@"block2:%@", [block2 copy]);

输出结果:
在这里插入图片描述

block的父类

    void (^block1)(void) = ^{
        NSLog(@"block1");
    };
    NSLog(@"%@",[block1 class]);
    NSLog(@"%@",[[block1 class] superclass]);
    NSLog(@"%@",[[[block1 class] superclass] superclass]);
    NSLog(@"%@",[[[[block1 class] superclass] superclass] superclass]);

在这里插入图片描述

block循环引用

如果在Block中使用附有__strong修饰符的对象类型自动变量,那么当Block 从栈复制到堆时,该对象为Block 所持有。这样容易引起循环引用。我们来看看下面的源代码:

typedef void (^blk_t)(void);
@interface MyObject : NSObject
{
	blk_t blk_;
}
@end

@implementation MyObject

- (id)init {
	self = [super init];
	blk_ = ^{
		NSLog(@"self = %@", self);
	};
	return self;
}
- (void)dealloc {
	NSLog(@"dealloc");
}
@end

int main() {
	id o = [[MyObject alloc] init];
	NSLog(@"%@", o);
	return 0;
}

该源代码中MyObject 类的 dealloc 实例方法一定没有被调用。
MyObject 类对象的 Block 类型成员变量blk_持有赋值为Block 的强引用。即 MyObject 类对象持有Block。init 实例方法中执行的 Block 语法使用附有__strong 修饰符的 id 类型变量 self。并且由于Block 语法赋值在了成员变量blk_中,因此通过Block 语法生成在栈上的 Block 此时由栈复制到堆,并持有所使用的self。self持有Block,Block 持有 self。这正是循环引用。
在这里插入图片描述
为避免此循环引用,可声明附有__weak 修饰符的变量,并将 self 赋值使用。

-(id)init {
	self = [super init];
	id __weak tmp = self;
	blk_ = ^{
		NSLog(@"self = %@",tmp);
	};
	return self;
}

在这里插入图片描述
在该源代码中,由于Block存在时,持有该Block 的 MyObject 类对象即赋值在变量tmp中的 self 必定存在,因此不需要判断变量 tmp 的值是否为nil。
另外,还可以使用__block变量来避免循环引用。

typedef void (^blk_t)(void);
@interface MyObject : NSObject {
	blk_t blk_;
}
@end
@implementation MyObject
- (id)init {
	self = [super init];
	
	__block id tmp = self;
	blk_ = ^{
		NSLog(@"self = %@", tmp);
		tmp = nil;
	};
	return self;
}
- (void)execBlock {
	blk_();
}
-(void)dealloc {
	NSLog(@"dealloc");
}
@end

int main() {
	id o = [[MyObject alloc] init];
	[o execBlock];
	return 0;
}

该源代码没有引起循环引用。但是如果不调用execBlock实例方法,即不执行赋值给成员变量blk_的 Block,便会循环引用并引起内存泄漏。在生成并持有MyObject 类对象的状态下会引起以下循环引用。

  • MyObject 类对象持有Block
  • Block 持有__block变量
  • __block变量持有MyObject类对象
    在这里插入图片描述

如果不执行 execBlock 实例方法,就会持续该循环引用从而造成内存泄漏。
通过执行 execBlock实例方法,Block 被实行,nil 被赋值在__block变量tmp中。

 blk_ = ^{
	NSLog(@"self = %@", tmp);
	tmp = nil;
};

因此,__block 变量 tmp 对 MyObject 类对象的强引用失效。避免循环引用的过程如下所示:

  • MyObject类对象持有 Block
  • Block 持有__block变量
    在这里插入图片描述
    下面我们对使用block变量避免循环引用的方法和使用__weak修饰符及__unsafe_unretained 修饰符避免循环引用的方法做个比较。
    使用__block变量的优点如下:
  • 通过__block 变量可控制对象的持有期间
    在不能使用__weak修饰符的环境中使用__unsafe_unretained 修饰符即可(不必担心悬垂指针)
  • 在执行 Block 时可动态地决定是否将 nil 或其他对象赋值在__block 变量中。使用__block变量的缺点如下:
  • 为避免循环引用必须执行Block
    存在执行了 Block 语法,却不执行 Block 的路径时,无法避免循环引用。若由于 Block 引发了循环引用时,根据 Block 的用途选择使用__block变量、__weak 修饰符或__unsafe_unretained修饰符来避免循环引用。

那么__weak和__unsafe_unretained有什么区别呢?

附有_unsafe_unretained修饰符的变量不属于编译器的内存管理对象,__unsafe_unretained会更易造成野指针
具体可以看看下面两个例子:

	__strong NSString *yourString = [[NSString alloc] initWithUTF8String:"your string"];
    __weak NSString *myString = yourString;
    yourString = nil;
    __unsafe_unretained NSString *theirString = myString;
    //现在所有的指针都为nil
	__strong NSString *yourString = [[NSString alloc] initWithUTF8String:"string 1"];
    __weak NSString *myString = yourString;
    __unsafe_unretained NSString *theirString = myString;
    yourString = nil;
    //现在yourString与myString的指针都为nil,而theirString不为nil,但是是野指针。

关于block的知识很重要也很繁杂,包括书上一些源码和前面遇到的一些问题目前还没能太理解,后续会继续补充。

未完待续······

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值