在我们使用OC进行iOS开发时,block的使用场景很多,特别是在GCD、网络访问(如框架AFNetworking)中出镜率很高,在ARC出来之前,一些资深面试官也喜欢问一些block深层次的问题,希望接下来的一些关于block的介绍能或多或少的帮助到各位。
在这里有一篇文章可以帮助我们。博主通过将OC通过命令行clang的方式编译成C代码,发现block就是指向结构体的指针,block的执行体会生成对应的函数。也可以理解为“block就是能够读取其它函数内部变量的函数”,数据结构体定义如下:
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void*,...);
struct Block_descriptor *descriptor;
/*Imported variables*/
};
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src)
void (*dispose)(void *);
};
从上面这段代码可以看出,一个block是由6部分组成:
这里是唐巧的一些总结,包括三种block类型的OC与C源码的分析。(ps:表示看得晕了!)
1.NSGlobalBlock:未引用任何外部变量,可当成函数使用,存储于程序代码区。就像这样的:
void (^test)() = ^{
NSLog(@"This is execute statement");
}
2.NSStackBlock:存储于栈内存,系统自动管理,当其所位于的函数返回后,此类型的block将无效。这种block称为内联block,AFNetworking框架中的网络请求方法用这种block用得很多,就像这样:
NSString *str = @"string";
NSLog(@"block type is %@", ^{
NSLog(@"str is %@", str);
});
/*
打印结果:block is <__NSStackBlock__:0x7fff596b9968>
后面的十六进制数是block的内存地址.可以看到代码中的block内联
在NSLog方法中,并访问了外界的变量,但是外界无论如何都不能有效
调用它,因为方法执后block将被销毁。
*/
3.NSMallocBlock:存储于堆内存,ARC管理它的内存,就像这样:
NSInteger test = 1;
void (^testBlock)(void) = ^(void) {
NSLog(@"这是Block的执行体%zd", test);
};
NSLog(@"block is %@", testBlock);
testBlock();
/*
打印结果:block is <__NSMallocBlock__: 0x7fdebac09580>
testBlock中引用了外部变量,并且可以通过block名调用它。所以此
类型的block可通过两点来判断:
1.捕获使用了外部的变量
2.可以通过block名调用它(注意block嵌套的情况)
*/
注意:NSMallocBlock要在ARC下才能判断出,在非ARC下打印的结果是NSStackBlock,也就是说ARC会自动将栈内存的block复制到堆内存中,目的就是让block的内存由程序员管理(实际上是ARC),并能够控制它的使用生命周期。如果是非ARC下,你就必须手动copy到堆内存。(在ARC如此成熟的今天,这种情况一般可以不用考虑,但还是需要有一定的了解。)对堆栈知识不怎么熟悉的兄弟们可以在这里看到。
前面已经提到在非ARC的状态下,需要手动将栈上的block copy到堆中,那么block的内存又如何来管理呢?请继续往下看。
注意:在对block进行copy时,该block内所引用到的所有block都将被copy,该block的变量引用到的block也会被copy,但是作为参数的block不会被 copy(因为参数只是局部变量,只在它所在block有效,如AFNetworking中网络请求时传入block参数success和failure),copy NSMallocBlock类型的block时,在copy方法结束后,它的引用计数又会降回去。(ps:没搞明白为什么设计这样的机制,而不是retain就增,release就减,而不是始终都是1,希望知道的兄弟们不吝赐教)
其实现在我们在实际开发中基本上没有对block手动copy、retain和release的情况,因为有ARC,但是ARC还是有解决不了的问题,当然这是我们程序员自己造成的。
1.局部变量
局部变量在block中为只读,在block定义时就copy了它所要用的变量值做常量使用,它们的内存地址不同,此时在block执行体内不能对该常量进行修改,外部的局部变量的改变也不会影响block的执行结果。如下:
NSInteger i = 1;
NSInteger (^addBlock)(NSInteger, NSInteger) = ^(NSInteger a, NSInteger b) {
//i++;//error:Variable is not assignable (missing __block type specifier)
return i + a + b;
};
i = 2;
NSLog(@"result is %zd", addBlock(1, 2));//result is 4
2.Static修饰的变量和全局变量
因为它们的内存地址是固定的,block在读取它们的值时是从内存中直接读取,此时就不在是copy的常量,在block中也可修改它们的值,它们值得改变也会影响block的执行结果,如下:
static NSInteger i = 1;
NSInteger (^addBlock)(NSInteger, NSInteger) = ^(NSInteger a, NSInteger b) {
i++;
return i + a + b;
};
i = 2;
NSLog(@"result is %zd", addBlock(1, 2));//result is 6
3.block变量
被__block(双下滑线)修饰的变量,基本类型等同于全局变量和静态变量。
1.全局对象、静态对象和__block对象测试代码如下:
NSString *_testString = nil;
- (void)testGlobalObj {
_testString = @"1";
void (^testBlock)(void) = ^ {
NSLog(@"testString is %@", _testString);
};
_testString = nil;
testBlock();
}
//调用testGlobalObj结果:testString is null
- (void)testStaticObj {
static NSString *_testString = nil;
_testString = @"1";
void (^testBlock)(void) = ^ {
NSLog(@"testString is %@", _testString);
};
_testString = nil;
testBlock();
}
//调用testStaticObj结果:testString is null
- (void)testBlockObj {
__block NSString *_testString = nil;
_testString = @"1";
void (^testBlock)(void) = ^ {
NSLog(@"testString is %@", _testString);
};
_testString = nil;
testBlock();
}
//调用testBlockObj结果:testString is null
2.局部变量测试:
- (void)testLocalObj {
NSString *_testString = nil;
_testString = @"1";
void (^testBlock)(void) = ^ {
NSLog(@"testString is %@", _testString);
};
_testString = nil;
testBlock();
}
//调用testLobalObj结果:testString is 1
3.__weak修饰的局部变量测试:
- (void)testWeakLocalObj {
NSString *_testString = nil;
_testString = @"1";
__weak NSString *_weakString = _testString;
void (^testBlock)(void) = ^ {
NSLog(@"_weakString is %@", _weakString);
};
_testString = nil;
testBlock();
}
//调用testWeakLocalObj结果:_weakString is 1
结论:
ARC所不能解决的问题就是引用循环,是指对象A引用对象B,对象B引用对象A,或者多个对象形成闭环的引用,对象A的内存释放依赖于对象B的内存释放,对象B的内存释放又依赖于对象A的内存释放,最终导致它们始终留在内存中,即使外界已经没有任何指针可以访问它们,这就是所谓的“僵尸对象(zombie object)”。在block中我们的一些行为会导致block的copy(ARC下自动完成),当Block被copy时,会对block中用到的对象强引用(Strong),当对象有强引用时是无法释放内存的。如下:
@property(nonatomic, copy) Block testBlock;
self.testBlock = ^ {
if (self.state) {
self.state(self.sendData);
}
};
对象有一个testBlock属性,对象强引用这个属性,但是testBlock有强引用着对象的其他属性,相应的也就强引用着对象本身,这就构成了引用循环,在ARC下可改为:
@property(nonatomic, copy) Block testBlock;
__weak typeof(self) weakSelf = self;//对自身对象生成一个弱引用
self.testBlock = ^ {
if (weakSelf.state) {
weakSelf.state(weakSelf.sendData);
}
};
testBlock执行体中弱引用这对象的属性,也就弱引用着对象,弱引用的对象是可以释放内存,释放之后弱引用就消失。
其实关于block有很多东西是需要我们注意的,而它的内存管理又是最为复杂的,所以我们在不了解的情况下不要乱用它,Xcode在编译时的静态分析只能对很少的简单循环引用提出警告,尽量避免多层嵌套使用block。从系统的API可以看出,单层内联的block使用起来很多并且都很简单,它的内存管理也不需要ARC来管理,自然而然就避免了很多问题。这篇文章写的很略,想要深入的兄弟们请自行搜索相关文章,笔者功力尚浅,就写到这里,谢谢!
Cooper’s Blog:正确使用Block避免Cycle Retain和Crash