简书上的所有内容都可以在我的个人博客上找到(打个广告😅)
块(Block)是在 iOS 开发中很常用的一个工具,它可以使我们代码的业务逻辑更加的紧凑。本文会分两部分来讲块,第一部分是块的基础知识,第二部分是对块的本质的一些理解。
块的基础知识
块的声明
块的声明类似于函数指针,只不过把 * 换成了 ^ 。声明时最前面是返回类型,中间是块名,最后是参数类型。
// returnType (^blockName) (parameters)
int (^someBlock) (int, int);
块的实现
// ^returnType(parameters){/*code*/};
^int(int a, int b) {
return a+b;
};
其中返回值是可以省略的,它可以自动判断,如果参数也是空的话我们可以简化成这样:
^{
return 100;
};
块的使用
块的使用就像使用 C 函数一样
someBlock(1,2);
用 typedef 来简化块的声明
typedef int (^CYAddBlock) (int, int);
CYAddBlock addBlock = ^(int a, int b) {
return a+b;
};
addBlock(1,2);
块能捕获在它声明范围里的所有变量
但是不能再块中修改捕获到的变量,如果我们尝试修改编译器会报错,无法通过编译。
int count = 0;
int (^someBlock) () = ^{
return count;
};
int result = someBlock(); // result = 0
用 __block 修饰变量,可以在块中修改变量的值
__block int count = 0;
int (^someBlock) () = ^{
return ++count;
};
int result = someBlock(); // result = 1
其实块在捕获变量时只是拷贝了一份变量的值,而用 __block 修饰后,拷贝的是变量的地址,所以我们就可以在块中修改变量了。这就和 C 语言中函数的参数是类似的道理。
小心产生循环引用
在一个类中,块能直接访问并修改实例变量的值,但是一定要注意不管是通过 self 来访问属性还是直接通过 _instanceVariable 来访问都会捕获 self。因为在直接访问 _instanceVariable 等效于这样:
self->_instanceVariable;
而一旦捕获了self, 我们就一定要注意循环引用导致的内存泄露了。我们在 CYClass 中声明了一个块和一个属性:
typedef void (^CYBlock) ();
@interface CYClass : NSObject
@property (nonatomic, copy)NSString *aString;
@property (nonatomic, copy)CYBlock aBlock;
@end
然后重写它的 init 和 dealloc 方法:
@implementation CYClass
- (instancetype)init
{
if (self = [super init]) {
_aString = @"Hello Cyrus";
_aBlock = ^{
NSLog(@"%@", _aString);
};
}
return self;
}
- (void)dealloc {
NSLog(@"CYClass deinit");
}
@end
然后我们实例化一个对象,紧接着就把它设为 nil 会发现 dealloc 方法并没有被调用,也就是说发生了内存泄露。
CYClass *c = [CYClass new];
c = nil;
就像这样:
我们可以通过 __weak 来打破这个循环,我们修改一下初始化方法:
- (instancetype)init
{
if (self = [super init]) {
_aString = @"Hello Cyrus";
__weak typeof(self) weakSelf = self;
_aBlock = ^{
__strong typeof(self) strongSelf = weakSelf;
NSLog(@"%@", strongSelf.aString);
};
}
return self;
}
在执行之前的代码就会发现这个对象成功的销毁了。
2016-03-12 16:30:24.995 BlockExample[4777:111670] CYClass deinit
块的本质
块的结构
我们可以在这里找到 Block 的定义:
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
看到 Block_layout 结构体里的第一个变量是一个 isa 指针, 我们应该就能猜到 Block 也是一个对象。然后再看第四个变量是一个叫 invoke 的函数指针,它指向的就是 Block 具体实现的函数了,这个函数至少要接受一个 void* 类型的参数,这个参数就是块本身。再下面的 descriptor 指向的就是 Block_descriptor 结构体,里面就是一些 Block 的信息。而在这些变量下面存放的就是 Block 捕获的那些变量。
块的类型
在 OC 中一共有3种块,分别是:
- 全局块,不会访问任何外部变量
- 栈块,保存在栈中, 当函数退出后就会被销毁,通过copy可以复制到堆上
- 堆块,保存在堆中
我们在 MRC 的环境下运行下面的代码:
void (^globalBlock)() = ^{ NSLog(@"global block"); };
NSLog(@"%@", globalBlock);
NSString *str1 = @"stack block";
void (^stackBlcok)() = ^{ NSLog(@"%@", str1); };
NSLog(@"%@", stackBlcok);
NSString *str2 = @"malloc block";
void (^mallockBlock)() = [^{ NSLog(@"%@", str2); } copy];
NSLog(@"%@", mallockBlock);
打印结果:
2016-03-13 16:23:50.676 BlockExample[1760:28902] <__NSGlobalBlock__: 0x1000042b0>
2016-03-13 16:23:50.677 BlockExample[1760:28902] <__NSStackBlock__: 0x7fff5fbff790>
2016-03-13 16:23:50.677 BlockExample[1760:28902] <__NSMallocBlock__: 0x100303fa0>
结果和我们预期的结果是一样的。我们在换成 ARC 的环境下运行一次:
2016-03-13 16:26:05.767 BlockExample[1805:29822] <__NSGlobalBlock__: 0x1000052f0>
2016-03-13 16:26:05.768 BlockExample[1805:29822] <__NSMallocBlock__: 0x100400340>
2016-03-13 16:26:05.769 BlockExample[1805:29822] <__NSMallocBlock__: 0x1004001b0>
我们发现原来的栈块也变成了堆块。这是因为在把一个快赋值给一个strong对象时 ARC 会自动帮我们执行一次copy。如果我们直接这样,那么还是会打印出 __NSStackBlock :
NSString *str1 = @"stack block";
NSLog(@"%@", ^{ NSLog(@"%@", str1); });
// 打印结果
2016-03-13 16:30:06.180 BlockExample[1881:32260] <__NSStackBlock__: 0x7fff5fbff798>