OC学习—Block初探(简易版)
前言
笔者在学习过程中经常遇到使用Block的地方,不太理解,因此来学习了Block的内容
回调
回调是编程中一种延迟执行的机制,允许代码在特定时间发生或任务完成后执行特定的逻辑
回调的本质:控制反转
传统调用流程;
调用方->被调用方->执行完成->返回结果给调用方
回调调用流程:
调用方->被调用方->执行完成->触发回调->执行调用方法的逻辑
核心思想:
将下一步做什么的决策权从被调用方交给调用方
解耦:被调用方无需关心具体的后续逻辑,只负责在特定时机出发回调
四种主要的回调方式
还没学到这里,笔者后面会补充的
Block
Block的定义
-
在iOS开发中,Block是一种闭包(Closure)的概念,可以将一段代码作为一个对象进行传递和存储,类似于函数的指针。Block可以捕获其定义时所在范围内的变量,并在需要时执行这段代码块,Block的使用可以方便的实现回调、异步操作、事件处理等功能
-
Block的本质也是对象,第一段代码块,iOS之所以引入Block,是因为使用Block可以精简代码,减少耦合
Block的用法
-
作为变量使用
// 返回值类型(^变量名)(参数列表)
int (^multiply)(int, int) = ^(int a, int b) {return a * b;
}
//调用Block
int result = multiply(3, 5);
-
作为方法参数
- (void)performOperationWithBlock:(void(^)(void))block {block();
}
[self performOperationWithBlock:^{NSLpg(@"执行Block");
}]
-
作为属性或者实例变量
@property (nonatomic, copy) void (^completionBlock)(void);
self.completionBlock = ^{NSLog(@"执行操作");
}
Block与外部变量
在iOS开发中,Block可以捕获其定义时所在范围内的外部变量,这种特性称之为变量捕获(Variable Capture),当Block内部引用外部变量时,Block会自动捕获这些变量的值或引用,以便在Block执行时使用,在Block中使用外部变量可以实现数据共享与传递(确保Block在脱离于原始作用域后仍能正确访问外部变量)
变量类型 | 类型 | 捕获到block内部 | 访问方式 |
---|---|---|---|
局部变量 | auto | 可以 | 值传递 |
局部变量 | static | 可以 | 指针传递 |
全局变量 | — | 不可以 | 直接访问 |
-
局部变量auto(自动变量),平时我们写的局部变量,默认就有auto,离开作用域就销毁
int age = 20; //等价于 auto int age = 20;
-
局部变量static:修饰局部变量,不会被销毁,由于下面代码可以看得出来Block外部修改static修饰的局部变量,依然可以影响Block内部的值
static int height = 30;
int age = 20 ;
void (^block)(void) = ^{NSLog(@"age us %d height = %d", age, height);
};
age = 25;
height = 35;
block();
//运行后发现是结果为 20 35
解释:
-
对于auto类型的局部变量,闭包会拷贝变量的值到闭包内部,形成值捕获(当闭包定义时,会保存这个值的副本)
-
对于static修饰的静态变量,闭包会引用静态变量的地址,形成引用捕获(静态变量存储在全局静态区,闭包保存的是他的内存地址,当执行修改变量的值时,是修改静态区的值,闭包通过地址访问到新值35)
-
全局变量:全局变量不能也无需捕获到Block内部,因为全局变量是存放在全局静态区的区,直接访问即可,缺点就是内存开销大
在Block内部如何修改Block外部变量?
通常需要_block关键字来声明外部变量,以便于在Block内部对外部变量进行修改,使用这个关键字可以使外部变量在Block内部变为可变,允许对其进赋值操作
在使用_block关键字声明外部变量时,需要注意内存管理的问题,特别是在ARC环境下,避免出现循环引用或者内存泄露的情况,要在适当的时候解除对Block的强引用,以避免出现循环引用的问题,实现代码的正确性和稳定性
捕获的底层原理
1.对于auto类型的局部变量,底层会生成一个结构体,包含变量的副本,闭包被定义时age = 20被拷贝到结构体中,后续修改外部age不影响结构体中的值
struct BlockStruct {int age;//保存捕获的age的值void (*invoke)(struct BlockStruct *self);
}
2.对于static修饰的静态变量,底层生成的结构体不会拷贝副本,直接使用全局地址,闭包通过地址height访问全局静态区的值,修改后闭包能开到最新的值
struct BlockStruct {int* height;void (*invoke)(struct BlockStruct* self);
};
-
如何让局部变量被引用捕获?
如果希望局部变量同样被引用捕获,可以使用_block关键字
_block int age = 20;
Block的分类
根据存储位置分类
-
栈上的Block:默认情况下,存储在栈上,当Block离开作用域时会被销毁
-
堆上的Block:通过调用copy方法,可以将栈上的Block复制到堆上,以延长Block的使用周期
栈上的Block
-
特点
-
默认情况下,Block储存在栈上
-
栈上的Block在定义他的作用域内有效,当作用域结束时,Block会被销毁
-
栈上的Block不可以跨作用域使用
-
-
内存管理
-
栈上的Block不需要手动管理内存,由系统自动管理
-
当Block被复制到堆上时,系统会自动将其从栈上复制到堆上,以延长Block的生命周期
-
-
注意:将栈上的Block传递给异步执行的方法时,需要注意避免在Block执行过程中访问已经释放的栈上变量,可以通过将Block复制到堆上来避免此问题。(只要Block的执行时机可能晚于其定义所在的函数作用域,可就必须将其复制到栈上,避免访问已经释放的Block)
堆上的Block
-
特点
-
通过调用copy方法,可以将栈上的Block复制到堆上
-
堆上Block的生命周期可以延长,不会受到作用域的限制
-
堆上的Block可以在不同的作用域中传递和使用,不会因为作用域结束而失效
-
-
内存管理
-
堆上的Block需要手动管理内存,需要在不使用Block时手动释放
-
在ARC环境下,系统会管理Block的内存,无需手动调用copy和release
-
-
当Block捕获外部变量时,如果Block存储在堆上,需要注意循环引用问题,可以使用_weak修饰符来避免循环引用
总的来说,栈上的Block在作用域内有效,适合用于临时的、局部的逻辑处理;而堆上的Block可以延长生命周期,适合在不同作用域中传递和复用。
Block的三种类型(底层)
-
NSGlobalBlock:存储在全局区,Block不捕获任何外部变量时自动生成,无需copy,生命周期与程序一致
-
NSStackBlock:存储在栈区(随函数返回自动销毁),Block捕获外部变量时默认生产
-
NSMallocBlock:存储在堆区(需要手动管理内存或依赖ARC),通过copy将NSStackBlock复制到堆上
区分栈上和堆上的Block
-
使用NSLog打印Block的地址
-
使用_block变量
-
栈上的block捕获block变量时,变量的值会被Block捕获,但是在作用域结束之后,block的值会被更新为Block执行时的值
-
堆上的Block捕获_block时,变量的值会复制到堆上的Block中,作用域结束后 _block的值不会被更新
_block的双重存储:当 _block被堆上的block捕获时,变量的初始值会被复制到堆上的Block中(就是一个副本)
-
-
使用Block_copy函数:
-
栈:对于栈上的Block使用Block_copy函数会将其复制到堆上,返回一个新的堆上
-
堆:仍会返回一个新的堆上的Block,地址不同
-
-
使用_weak修饰符
-
栈上的Block捕获到_weak变量时,变量会被自动置为nil,因为栈上的block不会强因为用 _weak变量
-
堆上的Block捕获到-weak变量时,变量会正常工作,因为堆上的Block会强引用_weak变量
-
Block的循环引用
循环引用是指两个或者多个对象之间相互持有对方的强引用,导致他们无法被释放从而造成内存泄漏,特别是在Block中捕获了外部的对象并且外部的对象又持有了block
示例:
@interface ViewController : UIViewController
@property (nonatomic, copy) void(^myBlock)(void);
@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];self.myBlock = ^ {//Block中捕获了self,导致循环引用[self doSomething];}
}
- (void)doSomething {NSLog(@"ppppp");
}
在上面代码中,ViewController的实例持有了一个Block,并且在Block中捕获了self,导致了循环引用
解决办法:
1.使用_weak修饰符
_weak typedef(self) weakSelf = self;
self.myblock = ^{[weakSelf doSomthing];
}
2.使用_block修饰符,可以让Block持有一个弱引用指针
什么是异步执行?
异步执行方法是一种程序执行模式,指方法或函数的调用不会阻塞当前线程的后续操作,而是在另一个线程或队列中独立执行。
其实笔者还没学到多线程的内容等后面学了讲解