- 还记得当初刚接触
Block
的时候,第一感觉就是觉得语法怪异,只知道就这么写就对了,然后稀里糊涂地用了一段时间,之后发现在iOS
里,Block
频繁使用,比如官方的API大量用到Block
来回调做事情。经过一段漫长岁月的使用和研究才明白Block
这个东西远远没有这么简单。 - 所以在这里总结一下我所学的关于
Block
的所有知识点,毕竟好记性不如烂笔头,写下来记忆会更加深刻而且写的过程会有更多的思考。
我将会从以下方面来讲解Block
-
Block
的定义 -
Block
的基本使用 -
Block
的底层数据结构 -
Block
的类型 -
Block
捕获变量机制 -
__Block
修饰符究竟做了什么? -
Block
内存管理 -
Block
循环引用 -
Block
交换实现 -
Block
相关面试题 - ...
Block的定义
Blocks是C语言的扩充功能。可以用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的匿名函数。
顾名思义,所谓匿名函数就是不带有名称的函数。
—— 引用自《iOS与OS X多线程和内存管理》
也就是说,Blocks
类似于某些语言中的闭包函数,以下是block
的语法声明
返回值类型 (^变量名)(参数列表) = ^ 返回值类型 (参数列表) 表达式
用代码来表示就是
void (^block)(void) = ^void (void){};
其中右边的返回值类型和参数类型为空的时候可以省略不写
void (^block)(void) = ^{};
当然,我们也可以利用typedef
的特性来定义一个Block
typedef void (^block)(void);
这样使用起来更方便
比如第三方网络框架AFNetworking
就通过这种定义方式大量使用Block
typedef void (^AFURLSessionDidBecomeInvalidBlock)(NSURLSession *session, NSError *error);
typedef NSURLSessionAuthChallengeDisposition (^AFURLSessionDidReceiveAuthenticationChallengeBlock)(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential * __autoreleasing *credential);
typedef NSURLRequest * (^AFURLSessionTaskWillPerformHTTPRedirectionBlock)(NSURLSession *session, NSURLSessionTask *task, NSURLResponse *response, NSURLRequest *request);
typedef NSURLSessionAuthChallengeDisposition (^AFURLSessionTaskDidReceiveAuthenticationChallengeBlock)(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential * __autoreleasing *credential);
typedef id (^AFURLSessionTaskAuthenticationChallengeBlock)(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, void (^completionHandler)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential));
以上摘自AFNetworking
中的AFURLSessionManager
Block的基本使用
block
可以作为属性、参数、返回值等形式使用
- 一、当
block
作为属性时
@property(nonatomic, copy) void (^NormalBlock)(void);
或者
typedef void (^NormalBlock)(void);
@property(nonatomic, copy) NormalBlock block;
这种用法最常见的就是平时我们在cell
中的响应事件的处理,有时使用block
来回调到VC
去处理会更加方便
@interface Cell : UITableViewCell
@property(nonatomic, copy) void (^clickBlock)(void);
@end
@implementation Cell
- (void)clickAction{
if(self. clickBlock){
self. clickBlock();
}
}
@end
@interface VC : UIViewController
@end
@implementation VC
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
Cell *cell = [ProGoldRiceRankCell makeCellWithTableView:tableView];
cell. clickBlock = ^{
//do anything
};
return cell;
}
@end
- 二、当
block
作为参数时
有时候我们需要从一个方法中返回一个值时,但刚好需要经过GCD
延时处理后赋值才返回,这种场景用return
时不行的,因为GCD
中的block
返回值类型为空,那么这时候可以用block
来回调返回值。
typedef void (^NormalBlock)(NSString *value);
- (void)test{
[self doSomeThingWithBlock:^(NSString *value) {
NSLog(@"%@",value);
}];
}
- (void)doSomeThingWithBlock:(NormalBlock)block{
NSString *value = @"1";
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
value = @"2";
block(value);
});
}
- 三、当
block
作为返回值时
我们经常使用的Masonry
框架内部实现就大量用到block
返回值来实现链式调用的语法
[_iconImg mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.left.bottom.right.mas_equalTo(0);
}];
在这里简单说一下Masonry
链式调用的实现原理(想要看完整源码解析的可以看这篇iOS开发之Masonry框架源码解析,个人觉得写得非常不错)
mas_makeConstraints
这个方法的实现如下,可以看到我们平时写的约束代码都是通过Block
传参的方式来对MASConstraintMaker
进行所有的约束设置,然后再调用install
方法安装所有约束
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
make.top.left.bottom.right.mas_equalTo(0);
这一句链式调用内部是这么操作的
- 通过封装好各种约束方法的工厂类
MASConstraintMaker
,首先调用top
- (MASConstraint *)top {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}
- 然后在调用
top
后会返回约束抽象类MASConstraint
(实际上返回的是MASConstraint
的子类MASViewConstraint
或者MASCompositeConstraint
)
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
if ([constraint isKindOfClass:MASViewConstraint.class]) {
//replace with composite constraint
NSArray *children = @[constraint, newConstraint];
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self;//设为代理
[self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
return compositeConstraint;//这里返回MASCompositeConstraint类型
}
if (!constraint) {
newConstraint.delegate = self;//设为代理
[self.constraints addObject:newConstraint];
}
return newConstraint;//这里返回MASViewConstraint类型
}
- 接着再次调用
left
(这次是MASConstraint
里的方法)
- (MASConstraint *)left {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- 而
MASConstraint
通过把MASConstraintMaker
设为代理从而使调用MASConstraint
的left
方法传递到MASConstraintMaker
实现的代理方法里面,然后代理方法又返回约束类MASConstraint
本身,这样就可以连续设置多个约束,而且最终都会调用到最上层工厂类MASConstraintMaker
里的方法
- (MASConstraint *)left {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");
//调用代理方法
return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
}
- 我们来看
mas_equalTo
和offset
- (MASConstraint * (^)(id))mas_equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
- (MASConstraint * (^)(CGFloat))offset {
return ^id(CGFloat offset){
self.offset = offset;
return self;
};
}
这两个方法都是MASConstraint
里的方法,所以设置完约束后返回的MASConstraint
类可以直接调用。
可以看到这两个方法都返回了一个(返回值为MASConstraint
类型的Block
),所以mas_equalTo(0)
相当于(MASConstraint * (^)(id))(0)
,MASConstraint * (^)(id)
看作一个整体Block
的话就相当于Block(0)
,这不就是我们平时调用Block
的方法么!然后调用Block
后返回MASConstraint
类型,从而可以继续调用下一个方法,这就是Block
作为返回值实现链式调用的用法所在。
正所谓光说(看)不练假功夫,那么现在我们亲自实现一个链式调用的例子!!
创建一个Student
类
.h
文件
#import
NS_ASSUME_NONNULL_BEGIN
@class Student;
@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger tall;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) CGSize size;
- (Student * (^)(NSString *))per_name;
- (Student * (^)(int))per_tall;
- (Student * (^)(int))per_age;
- (Student * (^)(CGSize))per_size;
- (Student * (^)(void))run;
@end
NS_ASSUME_NONNULL_END
.m
文件
#import "Student.h"
@interface Student ()
@end
@implementation Student
- (Student * (^)(NSString *))per_name{
return ^ Student * (NSString *name){
self.name = name;
return self;
};
}
- (Student * (^)(int))per_tall{
return ^ Student * (int tall){
self.tall = tall;
return self;
};
}
- (Student * (^)(int))per_age{
return ^ Student * (int age){
self.age = age;
return self;
};
}
- (Student * (^)(CGSize))per_size{
return ^ Student * (CGSize size){
self.size = size;
return self;
};
}
- (Student * (^)(void))run{
return ^ Student * (void){
NSLog(@"我在跑步");
return self;
};
}
@end
在TestVC
里使用
- (void)test{
Student *s = [Student new];
s.per_name(@"小强")
.per_tall(173)
.per_age(18)
.per_size(CGSizeMake(180, 80))
.run();
NSLog(@"我是一名学生,我的名字是%@,身高%ld,年龄%ld,尺寸%@",s.name,s.tall,s.age,NSStringFromCGSize(s.size));
}
打印
2020-08-18 12:02:19.315271+0800 CJJFramework[3846:74527] 我在跑步
2020-08-18 12:02:21.422766+0800 CJJFramework[3846:74527] 我是一名学生,我的名字是小强,身高173,年龄18,尺寸{180, 80}
(lldb)
这就是一个简单的链式语法调用的实现,简单太优美了有木有!比oc
那繁琐的对象.调用简洁太多了。
顺便打个小广告^-^
iOS-CJJTimer 高性能倒计时工具(短信、商品秒杀
Github地址
我封装的一个倒计时工具,里面也用到了链式语法调用,有兴趣的可以看看。
Block的底层数据结构
Block
本质上是一个OC
对象,因为它继承自NSBlock
,而NSBlock
又继承自NSObject
,所以Block
内部是有一个isa
指针的。
并且,Block
是一个封装了函数调用以及函数调用环境的OC
对象。
- 函数调用
void (^block)(void) = ^{
NSLog(@"%d",a);
};
通过窥探底层,我们会发现
NSLog(@"%d",a);
这一句代码会直接存在于
Block
中,在Block
的初始化方法中,传递了一个参数*fp
(最后把函数的地址传给了block->impl->FuncPtr
),这就意味着直接把整段代码块传递进Block
里面存着了(封装了函数的地址,属于引用传递)
- 函数调用环境
Block
里面会封装(存储)外面传进来的自动变量
具体的实现流程接下来会讲到:
通过翻看苹果官方源码或者直接把oc
代码编译成底层语言C++
代码,就可以找到以下源码
-
block
的底层结构如下图所示
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;
}
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
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)};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5l_0xn052bn6dgb9z7pfk8bbg740000gn_T_main_88f00d_mi_0);
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
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;
}
从来没读过源码或者不熟悉C++
的可能会觉得一脸懵,其实Block
可以简化成以下结构
struct __main_block_impl_0{
//struct __block_impl impl; //block的底层信息
void *isa;//说明block是一个oc对象
int Flags;
int Reserved;
void *FuncPtr;//所封装的函数的地址
//struct __main_block_desc_0* Desc; //block的描述信息
size_t reserved;
size_t Block_size;//block的大小
};
可以看到,Block
的底层数据结构就是一个结构体,其简化后所包含的成员变量如下
- void *isa //说明
block
是一个oc
对象 - int Flags // 某些标志,苹果用这个
flags
与上以下的枚举值来判断一些东西
// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
比如通过判断flags & BLOCK_HAS_COPY_DISPOSE
来确定是否存在copy
和dispose
函数,具体后面会讲到
if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
desc += sizeof(struct Block_descriptor_2);
}
以上代码来自苹果官方源码libclosure-74
- int Reserved //版本升级所需的区域大小
- void *FuncPtr //所封装的函数的地址
- size_t reserved //版本升级所需的区域大小
- size_t Block_size //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;
}
};
在初始化block
时传了2个参数,一个是函数对象的地址impl.FuncPtr = fp
(fp
就是函数指针(void *)__main_block_func_0
),另一个是描述对象的地址Desc = desc
(desc
就是描述信息的地址&__main_block_desc_0_DATA
)
Block的类型
Block
有3种类型,可以通过调用class方法查看其类型以及继承链
- 1.全局Block(
_NSConcreteGlobalBlock
)
(__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject)
- 2.栈Block(
_NSConcreteStackBlock
)
(__NSStackBlock__ : __NSStackBlock : NSBlock : NSObject)
- 3.堆Block(
_NSConcreteMallocBlock
)
(__NSMallocBlock__ : __NSMallocBlock : NSBlock : NSObject)
为什么Block
会有三种类型的呢?
这个是由存储它的内存位置决定的,下图展示了在应用程序的内存中,三种Block
所存在的区域,也就是说要判断一个Block
是什么类型,就是看它存在于内存的哪个区域。
那么如何区分三种
Block
,它们之间有什么异同点?
以下就是这三种
Block
的对比
NSGlobalBlock
存储的位置:程序的数据区域(全局区)
环境:没有访问auto变量
copy后的效果:什么也不做NSStackBlock
存储的位置:栈
环境:访问了auto变量
copy后的效果:从栈赋值到堆NSMallocBlock
存储的位置:堆
环境:NSStackBlock调用了copy
copy后的效果:引用计数增加
举例
typedef void (^block0)(void)
int val1 = 10;
- (void)test{
//NSGlobalBlock
block0 = ^{
};
//NSGlobalBlock
void (^block)(void) = ^{
};
//NSGlobalBlock
void (^block1)(void) = ^{
NSLog(@"%d",val1);
};
//MRC下为NSStackBlock,ARC下为NSMallocBlock(ARC下赋值给会把此Block从栈Copy到堆里)
int val2 = 20;
void (^block2)(void) = ^{
NSLog(@"%d",val2);
};
//NSMallocBlock
__block int val3 = 20;
void (^block3)(void) = ^{
NSLog(@"%d",val3);
};
}
Block捕获变量机制
众所周知,为了保证Block
内部能够正常访问外部的变量,Block
有一个捕获变量的机制。
Block
捕获变量后相当于往Block
结构体里增加一个成员变量。
首先变量可以分为两种,局部变量和全局变量。
局部变量分为局部(自动)变量和局部静态变量(static
)
全局变量分为全局变量和全局静态变量(static
)
以下是它们的区别
- 局部变量
- 1.自动变量(意思是,离开作用范围就会自动销毁,所以叫做自动变量,被
Block
捕获时是值传递(捕获的是具体存储的值))
{ auto int a = 0; }
- 2.局部静态变量(会在内存中一直存在,被
Block
捕获时是引用传递(捕获的是变量的地址))
{ static int a = 0; }
- 1.自动变量(意思是,离开作用范围就会自动销毁,所以叫做自动变量,被
- 全局变量(会在内存中一直存在,不会被
Block
捕获)- 全局变量
int a = 0;
- 全局静态变量,只能在本文件访问,不能在外部
extern
static int b = 0;
总结:只有局部变量才会被Block
捕获,全局变量不会被捕获
为什么全局变量不用捕获?
因为随时可以访问
为什么局部变量需要捕获?
作用域的问题,在Block
里面使用Block
外声明的局部变量,相当于跨函数使用这个局部变量,如果不存一份到Block
里面,是无法使用的,会造成访问无效内存,因为外面的局部变量有可能过了作用域就会自动被销毁
例如
typedef void (^Block)(void);
@property(nonatomic, copy) Block block;
- (void)test{
int a = 0;
self.block = ^{
NSLog(@"%d",a);
};
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
[self test];
self.block();
}
以上这段代码,当点击self.view
时会响应touchBegin
,然后调用test
,test
里面创建了一个局部自动变量a
,然后初始化了self.block
变量,里面使用了a
,但是调用完test
后,a
就会销毁,然后才调用Block
,这时候Block
里面再使用a
,如果不事先捕获(存一份),就会崩溃,访问无效内存,这就是为什么局部变量需要捕获,而全局变量不需要捕获的根本原因。
还有一个特殊情况,self会被捕获吗?
- (void)test{
self.block = ^{
NSLog(@"%p",self);
};
}
会,因为self
也是局部变量,我们来回想一下,在OC
里调用方法实际上会传递self
指针的参数,而且捕获的是指针,所以属于引用传递。
objc_msgSend(id self, SEL _cmd, ...)
所以我们之所以能在每一个方法中使用self
,就是因为默认传入self
变量
另一个特殊情况,成员变量会被捕获吗?
@property(nonatomic, copy) NSString *name;
- (void)test{
self.block = ^{
NSLog(@"%@",_name);
};
}
会,因为这里访问的成员变量也是局部变量,相当于
- (void)test{
self.block = ^{
NSLog(@"%@",self->_name);
};
}
__Block修饰符究竟做了什么?
我们来看下面这一段代码
int val = 10;
void (^block)(void) = ^{
val = 20;//这个是错误的,不能通过编译的,因为val是自动局部变量,过了作用域就销毁
//而这里是在另一个栈空间,不能访问val
};
那么如何使得变量val
可以更改呢?
有几种办法
可以把变量val
修饰为全局变量或者静态变量,而更好的办法是用__block
修饰符修饰
__block
修饰符
-
__block
可以用于解决Block
内部无法修改auto
变量值的问题 -
__block
不能修饰全局变量、静态变量(static
) - 编译器会将
__block
变量包装成一个对象(__Block_byref_age_0
类型)
比如说这一段
__block int val = 1;
int (^block)(CGFloat num) = ^ int (CGFloat num){
NSLog(@"这是一个Block");
val = 2;
return val;
};
编译成C++代码如下,我整理了一下格式方便查看
__attribute__((__blocks__(byref))) __Block_byref_ val_0 val =
{
(void*)0,//void *__isa
(__Block_byref_ val_0 *)& val,//__Block_byref_val_0 *__forwarding
0,//int __flags
sizeof(__Block_byref_val_0),//int __size
1 //int val
};
int (*block)(CGFloat num) = (
(int (*)(CGFloat))
&__main_block_impl_0(
(void *)__main_block_func_0, //
&__main_block_desc_0_DATA,
(__Block_byref_val_0 *)& val,
570425344
)
);
自动变量val
被__block
修饰后会包装成__Block_byref_val_0
对象,也就是说Block
的__main_block_impl_0
结构体实例持有指向__block
变量的__Block_byref_val_0
结构体实例的指针。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __Block_byref_val_0{
void *__isa;
__Block_byref_age_0 *__forwarding;//这个指针指向该对象自身的地址
int __flags;
int __size;
int val;
};
会发现里面也有一个val
,其实这里的val
才是Block
捕获进来的那个val
。
还有一个成员变量__forwarding
而且__main_block_impl_0里的__Block_byref_val_0变量并不是存在于Block
结构体里面,Block
只是保存了一个引用了__Block_byref_val_0变量地址的指针,这样就可以在多个不同的Block
里面访问同一个__block
变量了。
看着这个图可能会有疑问了。
为什么不能直接在Block
结构体里面存储val
,而要搞这么麻烦,生成一个val
结构体,然后把val
变量存放到里面呢?
- 我的理解是,因为直接在
Block
中存储val
变量的话,是在栈上存储的,等变量作用域过去之后变量就会销毁,这样就无法在Block
里访问该变量了;而通过把其包装成一个__Block_byref_val_0
类型的对象,把该变量保存在对象里,当Block
从栈copy
到堆上的时候,相当于__block
变量也从栈copy
到堆里存了一份,这样作用域过了之后,Block
仍然可以访问val
变量,而在copy
的过程中,栈上的__block
变量中的__forwarding
指针会变为指向堆上的__block
变量的结构体实例的地址,而通过这种方式,无论是在Block
语法中、Block
语法外使用__block
变量,还是__block
变量配置在栈上或堆上,都可以顺利地访问同一个__block
变量。
Block内存管理
如果
Block
捕获了对象类型的auto
变量会怎么样?
实际上只是多了内存管理方面的操作。
Block
经过copy
之后会在desc
里生成的2个函数
-
copy
函数
调用时机 栈上的Block
复制到堆时 -
dispose
函数
调用时机 堆上的Block
被废弃时
当Block
内部访问了带有__block
修饰符的对象类型的auto
变量时
当
block
在栈上时,并不会对__block
变量产生强引用-
当
block
被copy
到堆时- 会调用
block
内部的copy
函数 -
copy
函数内部会调用_Block_object_assign
函数 -
_Block_object_assign
函数会根据所指向对象的修饰符(__strong
,__weak
,__unsafe_unretained
)做出相应的操作,形成强引用(retain
)或者弱引用(注意:这里仅限于ARC时会retain
,MRC时不会retain
)
- 会调用
-
当
block
从堆中移除时- 会调用
block
内部的dispose
函数 -
dispose
函数内部会调用_Block_object_dispose
函数 -
_Block_object_dispose
函数会自动释放引用的__block变量(release)
- 会调用
对象类型的auto变量、__block变量
//auto
{
(auto) Person *person = [Person new];
void (^block)(void) = ^{
NSLog(@"%@",person);
};
}
//__block
{
__block Person *person = [Person new];
void (^block)(void) = ^{
NSLog(@"%@",person);
};
}
- 当
block
在栈上时,对它们都不会产生强引用 - 当
block
拷贝到堆上时,都会通过copy
函数来处理它们
//传8和3来区别这两种变量
//__block变量(假设变量名叫做a)
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
//对象类型的auto变量(假设变量名叫做p)
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
- 当
block
从堆上移除时,都会通过dispose
函数来释放它们
//__block变量(假设变量名叫做a)
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
//对象类型的auto变量(假设变量名叫做p)
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
Block循环引用
有一个对象A
,一个Block
当A
强引用了Block
,Block
也强引用了A
,这种情况就是循环引用,造成内存泄漏。
用代码表示就是
@interface A : NSObject
@property(nonatomic, copy) void (^block)(void);
@end
@implementation
- (void)viewDidLoad{
[super viewDidLoad];
self.block = ^{
NSLog(@"%@",self);
};
}
@end
如上,self
持有block
属性,然后block
里持有self
,互相强引用,造成谁也释放不了,这只是最简单的一种情况,实际上平时遇到得有可能比这种复杂得多,有自引用循环(A->A
),双向引用循环(A->B->A
),多引用循环(A->B->C->A
)等等,但是只要我们清楚了引用循环的本质,这些情况其实都很容易发现并解决,我们只要切断引用链中随意一方的强引用就可以解决引用循环的问题。
解决方案
在ARC
和MRC
下解决循环引用的方式各有不同。
在ARC
下,可以使用__weak
、__unsafe_unretained
、__block
三种方式解决
//__weak
__weak typeof(self) weakSelf = self;
self.block = ^{
NSLog(@"%p",weakSelf);
};
//__unsafe_unretained
__unsafe_unretained id weakSelf = self;
self.block = ^{
NSLog(@"%p",weakSelf);
};
//__block
//因为ARC下__block会使得Block内部强引用外部的变量
//所以需要调用Block并且手动把变量置空(nil)
__block id weakSelf = self;
self.block = ^{
NSLog(@"%p",weakSelf);
weakSelf = nil;
};
self.block();
在MRC
下,可以使用__unsafe_unretained
、__block
解决
//__unsafe_unretained
__unsafe_unretained id weakSelf = self;
self.block = ^{
NSLog(@"%p",weakSelf);
};
//__block
__block id weakSelf = self;
self.block = ^{
NSLog(@"%p",weakSelf);
};
综上,最好的方法是ARC
下用__weak
,MRC
下用__unsafe_unretained
。
- 还有一种情况
如果要在block
里面访问成员变量的话
@interface A : NSObject
{
NSString *name;
}
@property (nonatomic, copy) void (^block)(void);
@property (nonatomic, copy) NSString *address;
@end
@implementation A
- (void)testMethod {
name = @"名字";
__weak typeof(self) weakSelf = self;
self.block = ^{
//一定要加上这一句才能访问name,不然weakSelf->name会报错Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to strong variable first
__strong __typeof(weakSelf)strongSelf = weakSelf;
NSLog(@"%@-%@", strongSelf->name, weakSelf.address);
};
self.block();
}
@end
分析:
问题一:为什么成员变量name
要加上__strong
修饰一下才能访问呢?
答1:因为weakself
有可能在block
执行过程中就释放了,也就是weakself
指针置为nil
,一旦释放再使用nil
指针去访问成员变量拿到的值也为nil
问题二:而address
却可以直接用weakSelf
访问weakSelf.address
?
答2:因为weakSelf.address
是调用address
的getter
方法,而不是直接访问成员变量,即使weakself释放了,也不一定会影响使用,因为nil访问getter方法无效,给空对象发消息是不会生效的。
另外
在平时开发中,我发现有一些同事看到只要有block
的地方就使用weakself
,即使是工作了三四年的,也有这种问题,实际上就是没有搞懂引用循环的本质,下面举几个block
里使用self
不需要弱引用的例子
例子一:控制器没有强引用block,block强引用self(不需要weakself)
@interface A : NSObject
@end
@implementation
- (void)viewDidLoad{
[super viewDidLoad];
id block = ^{
NSLog(@"%@",self);
};
}
@end
分析:self不持有block,block持有self,不构成双向的循环引用,所以不需要weakself
例子二:类方法的block(不需要weakself)
@interface A : NSObject
@end
@implementation
- (void)viewDidLoad{
[super viewDidLoad];
[UIView animateWithDuration:duration animations:^{
NSLog(@"%@",self);
}];
}
@end
分析:同例子一
例子三:AFNetworking的请求方法的回调block(不需要weakself)
[[AFNetWorkManager sharedManager] requestWithUserMethod:POST Url:url parameters:paramsDic success:^(NetWorkResultModel * _Nonnull resultModel) {
NSLog(@"%@",self);
} failure:^(NSError * _Nonnull error) {
}];
分析:首先大多数封装了AFN
的都是使用单例,正常情况下,如果单例持有了self
,是会造成释放不了self
的,因为除非人为释放,否则单例会在内存中一直存在,那么这里的AFN
的block
引用了self
为什么不需要weakself
呢,是因为AFN
内部已经做了处理,在请求结束之后移除了对block
的引用,所以在这种情况下是不需要使用weakself
的。
strongself
我们经常会使用(weakself
+ strongself
)搭配使用
__weak __typeof(self) weakself = self;
self.block = ^{
__strong __typeof(self) strongSelf = weakself;
};
作用就是防止在block
执行过程中使用了self
,但是self
已经销毁的情况
比如
@interface TestViewController ()
@property (nonatomic, copy) void (^testBlock)(void);
@end
@implementation TestViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.testBlock = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", weakSelf);
[weakSelf dataCollect];
});
};
}
- (void)dealloc {
NSLog(@"TestViewController销毁");
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
self.testBlock();
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)dataCollect {
NSLog(@"发送埋点");
}
@end
2022-04-13 11:13:55.398826+0800 Test[38751:1052715] TestViewController销毁
2022-04-13 11:14:00.395086+0800 Test[38751:1052715] (null)
当我们点击view
的时候,调用了block
的同时触发了dismiss
,这时候由于没有地方对self
有强引用,所以就会走dealloc
方法,等5s之后再触发GCD
里面的代码时,weakself
已经为nil
,所以无法调用dataCollect
。
但如果我们在block
里面使用strongself
,重新强引用self
对象,那么就可以延长self
的生命周期
@interface TestViewController ()
@property (nonatomic, copy) void (^testBlock)(void);
@end
@implementation TestViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.testBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", strongSelf);
[strongSelf dataCollect];
});
};
}
- (void)dealloc {
NSLog(@"TestViewController销毁");
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
self.testBlock();
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)dataCollect {
NSLog(@"发送埋点");
}
@end
2022-04-13 11:16:13.391062+0800 Test[38821:1055142]
2022-04-13 11:16:13.391154+0800 Test[38821:1055142] 发送埋点
2022-04-13 11:16:13.391199+0800 Test[38821:1055142] TestViewController销毁
当然,只要你结合上下文分析出不会出现以上这种情况,只使用weakself
也没问题。
Block交换实现
由于这一主题内容太多,所以另开一篇来谈谈
如何去hook一个block的实现?
传送门->iOS-玩转Block(Hook Block 交换block的实现)
Block相关面试题
一、
Block
的原理是怎样的?本质是什么?
封装了函数调用以及调用环境的OC
对象。(有待补充,结合实际面试情况自由发挥)二、
__block
的作用是什么?有什么使用注意点?
本质:把变量包装成一个对象
作用:可以解决Block
内部无法修改auto
变量值的问题
使用注意:内存管理问题,在MRC
下__block
修饰内部不会对对象产生强引用(retain
);ARC
下会,需要避免循环引用。三、
Block
的属性修饰词为什么是copy
?使用Block
有哪些使用注意?
原因:Block
一旦没有进行copy
操作,就不会在堆上,所以通过copy
到堆上我们可以对Block
进行内存管理
使用注意:循环引用问题
另外:ARC
下用Strong
和Copy
是一样的,都会把Block
copy
到堆里面,MRC
下只能用Copy
,所以结合两种情况,用Copy
是最好的四、
Block
在修改NSMutableArray
,需不需要添加__block
?
不需要,因为只是对数组操作内容,并不是修改他的内存地址