2019 iOS面試題大全---全方面剖析面試
- 什么是Block?
- Block變量截獲
- Block的幾種形式
一、什么是Block?
-
Block是將函數及其執行上下文封裝起來的對象。
比如:
NSInteger num = 3;
NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
return n*num;
};
block(2);
通過clang -rewrite-objc WYTest.m命令編譯該.m文件,發現該block被編譯成這個形式:
NSInteger num = 3;
NSInteger(*block)(NSInteger) = ((NSInteger (*)(NSInteger))&__WYTest__blockTest_block_impl_0((void *)__WYTest__blockTest_block_func_0, &__WYTest__blockTest_block_desc_0_DATA, num));
((NSInteger (*)(__block_impl *, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 2);
其中WYTest是文件名,blockTest是方法名,這些可以忽略。
其中__WYTest__blockTest_block_impl_0結構體為
struct __WYTest__blockTest_block_impl_0 {
struct __block_impl impl;
struct __WYTest__blockTest_block_desc_0* Desc;
NSInteger num;
__WYTest__blockTest_block_impl_0(void *fp, struct __WYTest__blockTest_block_desc_0 *desc, NSInteger _num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__block_impl結構體為
struct __block_impl {
void *isa;//isa指針,所以說Block是對象
int Flags;
int Reserved;
void *FuncPtr;//函數指針
};
block內部有isa指針,所以說其本質也是OC對象
block內部則為:
static NSInteger __WYTest__blockTest_block_func_0(struct __WYTest__blockTest_block_impl_0 *__cself, NSInteger n) {
NSInteger num = __cself->num; // bound by copy
return n*num;
}
所以說 Block是將函數及其執行上下文封裝起來的對象
既然block內部封裝了函數,那么它同樣也有參數和返回值。
二、Block變量截獲
1、局部變量截獲 是值截獲。 比如:
NSInteger num = 3;
NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
return n*num;
};
num = 1;
NSLog(@"%zd",block(2));
這里的輸出是6而不是2,原因就是對局部變量num的截獲是值截獲。
同樣,在block里如果修改變量num,也是無效的,甚至編譯器會報錯。
NSMutableArray * arr = [NSMutableArray arrayWithObjects:@"1",@"2", nil];
void(^block)(void) = ^{
NSLog(@"%@",arr);//局部變量
[arr addObject:@"4"];
};
[arr addObject:@"3"];
arr = nil;
block();
打印為1,2,3
局部對象變量也是一樣,截獲的是值,而不是指針,在外部將其置為nil,對block沒有影響,而該對象調用方法會影響
2、局部靜態變量截獲 是指針截獲。
static NSInteger num = 3;
NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
return n*num;
};
num = 1;
NSLog(@"%zd",block(2));
輸出為2,意味著num = 1這里的修改num值是有效的,即是指針截獲。
同樣,在block里去修改變量m,也是有效的。
3、全局變量,靜態全局變量截獲:不截獲,直接取值。
我們同樣用clang編譯看下結果。
static NSInteger num3 = 300;
NSInteger num4 = 3000;
- (void)blockTest
{
NSInteger num = 30;
static NSInteger num2 = 3;
__block NSInteger num5 = 30000;
void(^block)(void) = ^{
NSLog(@"%zd",num);//局部變量
NSLog(@"%zd",num2);//靜態變量
NSLog(@"%zd",num3);//全局變量
NSLog(@"%zd",num4);//全局靜態變量
NSLog(@"%zd",num5);//__block修飾變量
};
block();
}
編譯后
struct __WYTest__blockTest_block_impl_0 {
struct __block_impl impl;
struct __WYTest__blockTest_block_desc_0* Desc;
NSInteger num;//局部變量
NSInteger *num2;//靜態變量
__Block_byref_num5_0 *num5; // by ref//__block修飾變量
__WYTest__blockTest_block_impl_0(void *fp, struct __WYTest__blockTest_block_desc_0 *desc, NSInteger _num, NSInteger *_num2, __Block_byref_num5_0 *_num5, int flags=0) : num(_num), num2(_num2), num5(_num5->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
( impl.isa = &_NSConcreteStackBlock;這里注意到這一句,即說明該block是棧block)
可以看到局部變量被編譯成值形式,而靜態變量被編成指針形式,全局變量并未截獲。而__block修飾的變量也是以指針形式截獲的,并且生成了一個新的結構體對象:
struct __Block_byref_num5_0 {
void *__isa;
__Block_byref_num5_0 *__forwarding;
int __flags;
int __size;
NSInteger num5;
};
該對象有個屬性:num5,即我們用__block修飾的變量。
這里__forwarding是指向自身的(棧block)。
一般情況下,如果我們要對block截獲的局部變量進行賦值操作需添加__block
修飾符,而對全局變量,靜態變量是不需要添加__block修飾符的。
另外,block里訪問self或成員變量都會去截獲self。
三、Block的幾種形式
-
分為全局Block(_NSConcreteGlobalBlock)、棧Block(_NSConcreteStackBlock)、堆Block(_NSConcreteMallocBlock)三種形式
其中棧Block存儲在棧(stack)區,堆Block存儲在堆(heap)區,全局Block存儲在已初始化數據(.data)區
1、不使用外部變量的block是全局block
比如:
NSLog(@"%@",[^{
NSLog(@"globalBlock");
} class]);
輸出:
__NSGlobalBlock__
2、使用外部變量并且未進行copy操作的block是棧block
比如:
NSInteger num = 10;
NSLog(@"%@",[^{
NSLog(@"stackBlock:%zd",num);
} class]);
輸出:
__NSStackBlock__
日常開發常用于這種情況:
[self testWithBlock:^{
NSLog(@"%@",self);
}];
- (void)testWithBlock:(dispatch_block_t)block {
block();
NSLog(@"%@",[block class]);
}
3、對棧block進行copy操作,就是堆block,而對全局block進行copy,仍是全局block
- 比如堆1中的全局進行copy操作,即賦值:
void (^globalBlock)(void) = ^{
NSLog(@"globalBlock");
};
NSLog(@"%@",[globalBlock class]);
輸出:
__NSGlobalBlock__
仍是全局block
- 而對2中的棧block進行賦值操作:
NSInteger num = 10;
void (^mallocBlock)(void) = ^{
NSLog(@"stackBlock:%zd",num);
};
NSLog(@"%@",[mallocBlock class]);
輸出:
__NSMallocBlock__
對棧blockcopy之后,并不代表著棧block就消失了,左邊的mallock是堆block,右邊被copy的仍是棧block
比如:
[self testWithBlock:^{
NSLog(@"%@",self);
}];
- (void)testWithBlock:(dispatch_block_t)block
{
block();
dispatch_block_t tempBlock = block;
NSLog(@"%@,%@",[block class],[tempBlock class]);
}
輸出:
__NSStackBlock__,__NSMallocBlock__
-
即如果對棧Block進行copy,將會copy到堆區,對堆Block進行copy,將會增加引用計數,對全局Block進行copy,因為是已經初始化的,所以什么也不做。
另外,__block變量在copy時,由于__forwarding的存在,棧上的__forwarding指針會指向堆上的__forwarding變量,而堆上的__forwarding指針指向其自身,所以,如果對__block的修改,實際上是在修改堆上的__block變量。
即__forwarding指針存在的意義就是,無論在任何內存位置, 都可以順利地訪問同一個__block變量。
- 另外由于block捕獲的__block修飾的變量會去持有變量,那么如果用__block修飾self,且self持有block,并且block內部使用到__block修飾的self時,就會造成多循環引用,即self持有block,block 持有__block變量,而__block變量持有self,造成內存泄漏。
比如:
__block typeof(self) weakSelf = self;
_testBlock = ^{
NSLog(@"%@",weakSelf);
};
_testBlock();
如果要解決這種循環引用,可以主動斷開__block變量對self的持有,即在block內部使用完weakself后,將其置為nil,但這種方式有個問題,如果block一直不被調用,那么循環引用將一直存在。
所以,我們最好還是用__weak來修飾self