說明:對于block底層不是太熟悉或者下面題目中有不太懂的地方,建議先去看看我另外一篇博客:OC中block的底層實現原理。
1. 第一題
下面代碼運行結果是什么?
int d = 1000; // 全局變量
static int e = 10000; // 靜態全局變量
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10; // 局部變量
static int b = 100; // 靜態局部變量
__block int c = 1000;
void (^block)(void) = ^{
NSLog(@"a = %d",a);
NSLog(@"b = %d",b);
NSLog(@"c = %d",c);
NSLog(@"d = %d",d);
NSLog(@"e = %d",e);
};
a = 20;
b = 200;
c = 2000;
d = 20000;
e = 200000;
block();
}
return 0;
}
// ***************打印結果***************
2020-01-08 08:50:54.621532+0800 CommandLine[72269:7909757] a = 10
2020-01-08 08:50:54.621871+0800 CommandLine[72269:7909757] b = 200
2020-01-08 08:50:54.621912+0800 CommandLine[72269:7909757] c = 2000
2020-01-08 08:50:54.621969+0800 CommandLine[72269:7909757] d = 20000
2020-01-08 08:50:54.621994+0800 CommandLine[72269:7909757] e = 200000
解釋:
- block在捕獲普通的局部變量時是捕獲的
a
的值,后面無論怎么修改a
的值都不會影響block之前捕獲到的值,所以a的值不變。 - block在捕獲靜態局部變量時是捕獲的
b
的地址,block里面是通過地址找到b
并獲取它的值。所以b
的值發生了改變。 -
__block
是將外部變量包裝成了一個對象并將c
存在這個對象中,實際上block外面的c
的地址也是指向這個對象中存儲的c
的,而block底層是有一個指針指向這個對象的,所以當外部更改c
時,block里面通過指針找到這個對象進而找到c
,然后獲取到c
的值,所以c
發生了變化。 - 全局變量在哪里都可以訪問,block并不會捕獲全局變量,所以無論哪里更改
d
和e
,block里面獲取到的都是最新的值。
2. 第二題
下面代碼能正常編譯嗎?不能的話是那些代碼不能通過編譯呢?
int d = 1000; // 全局變量
static int e = 10000; // 靜態全局變量
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10; // 局部變量
static int b = 100; // 靜態局部變量
__block int c = 1000;
NSMutableArray *array1 = nil;
__block NSMutableArray *array2 = nil;
void (^block)(void) = ^{
a = 20;
b = 200;
c = 2000;
d = 20000;
e = 200000;
array1 = [NSMutableArray array];
[array1 addObject:@"111"];
array2 = [NSMutableArray array];
[array2 addObject:@"222"];
};
block();
}
return 0;
}
解答:
a = 20;
無法通過編譯,因為a
是局部變量,其作用域和生命周期僅限于它所在的大括號內部,而block底層是將塊中的代碼封裝到了一個函數中,在那個函數中修改a
就相當于在一個函數中去修改另外一個函數中的局部變量,這樣肯定是無法通過編譯。
array1 = [NSMutableArray array];
無法通過編譯。原因和上面一樣,array1
是一個指針,這里是想在一個函數中去給另外一個函數中的變量重新賦值一個指針,所以無法通過編譯。
其它的都可以通過編譯。
全局變量在哪里都可以訪問到,所以在block里面可以修改,實際上block并不會捕獲全局變量存到block內部。
__block
修飾的變量(以變量c
為例)是被包裝成了一個對象,c
就存儲在對象中,block外面 的c
的地址實際上就是這個對象中存儲的c
的地址,而block里面也存儲著一個指針指向這個對象進而能訪問到這個對象中的c
,所以是可以直接修改的。
[array1 addObject:@"111"];
可以通過編譯是因為block捕獲了array1
的值(也就是數組的地址)存儲在block里面,這里是通過這個地址找到數組,然后對數組中的元素進行操作,所以是可以通過編譯的。所以對于一個對象類型的變量,block內部只要不是想修改這個變量的值,都不需要用__block
來修飾這個變量(比如增、刪、修改集合類型對象里面的元素,或者修改一個實例對象的屬性等都不需要用__block
修飾)。
3. 第三題
下面代碼運行結果是什么?
- (void)test{
__block Person *person = [[Person alloc] init];
person.age = 20;
__weak Person *weakPerson = person;
self.block = ^{
NSLog(@"block-begin:%@ age = %d",[NSThread currentThread],weakPerson.age);
[NSThread sleepForTimeInterval:1.0f];
NSLog(@"block-eng:%@ age = %d",[NSThread currentThread],weakPerson.age);
};
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.block();
});
[NSThread sleepForTimeInterval:0.2f];
NSLog(@"test-end:%@",[NSThread currentThread]);
}
// ***************打印結果***************
2020-01-09 09:50:50.309564+0800 AppTest[76619:8356081] block-begin:<NSThread: 0x600001b1aa40>{number = 6, name = (null)} age = 20
2020-01-09 09:50:50.509657+0800 AppTest[76619:8356009] test-end:<NSThread: 0x600001b7ee40>{number = 1, name = main}
2020-01-09 09:50:51.314372+0800 AppTest[76619:8356081] block-eng:<NSThread: 0x600001b1aa40>{number = 6, name = (null)} age = 0
解釋:
weakPerson
是一個弱指針,所以self.block
對person
是弱引用。然后在并發隊列中通過異步函數添加一個任務來執行self.block();
,所以是開啟了一個子線程來執行這個任務,此時打印age
值是20
,然后子線程開始睡眠1秒鐘。與此同時主線程也睡眠0.2秒,0.2秒后主線程執行完最后的打印操作,test
函數就執行完了。而由于person
是一個局部變量,而且self.block
對它也是弱引用,所以在test
函數執行完后person
對象就被釋放了。再過0.8秒鐘,子線程結束睡眠,此時weakPerson
所指向的對象已經變成了nil,所以打印的age
是0。
此時如果將主線程的睡眠時間改的比子線程睡眠時間長的話結果又不一樣,因為子線程睡眠結束時主線程還在睡眠睡眠,也就是test
方法還沒執行完,那person
對象就還存在,所以子線程睡眠前后打印的age
都是20。
// 主線程睡眠時間改為:[NSThread sleepForTimeInterval:1.2f];
// ***************打印結果***************
2020-01-09 10:01:26.951395+0800 AppTest[76646:8360116] block-begin:<NSThread: 0x6000027316c0>{number = 5, name = (null)} age = 20
2020-01-09 10:01:27.955117+0800 AppTest[76646:8360116] block-eng:<NSThread: 0x6000027316c0>{number = 5, name = (null)} age = 20
2020-01-09 10:01:28.152426+0800 AppTest[76646:8360048] test-end:<NSThread: 0x60000274dc80>{number = 1, name = main}
4. 第四題
下面代碼運行結果是什么?
- (void)test{
__block Person *person = [[Person alloc] init];
person.age = 20;
__weak Person *weakPerson = person;
self.block = ^{
__strong Person *strongPerson = weakPerson;
NSLog(@"block-begin:%@ age = %d",[NSThread currentThread],strongPerson.age);
[NSThread sleepForTimeInterval:1.0f];
NSLog(@"block-eng:%@ age = %d",[NSThread currentThread],strongPerson.age);
};
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.block();
});
[NSThread sleepForTimeInterval:0.2f];
NSLog(@"test-end:%@",[NSThread currentThread]);
}
// ***************打印結果***************
2020-01-09 10:02:01.319436+0800 AppTest[76662:8360768] block-begin:<NSThread: 0x6000010b2a00>{number = 4, name = (null)} age = 20
2020-01-09 10:02:01.520491+0800 AppTest[76662:8360684] test-end:<NSThread: 0x6000010a8680>{number = 1, name = main}
2020-01-09 10:02:02.324449+0800 AppTest[76662:8360768] block-eng:<NSThread: 0x6000010b2a00>{number = 4, name = (null)} age = 20
解釋:
和前一題相比,這里在block內部定義了一個__strong
修飾的strongPerson
。這里要說明一下,__strong
的作用就是保證在block中的代碼塊在執行的過程中,它所修飾的對象不會被釋放,即便block外面已經沒有任何強指針指向這個對象了,這個對象也不會立馬釋放,而是等到block執行結束后再釋放。所以在實際開發過程中__weak
和__strong
最好是一起使用,避免出現block運行過程中其弱引用的對象被釋放。
注意__strong
只是保證在block運行過程中
弱引用對象不被釋放,為什么要再強調一遍這個問題,請看下面一道題。
5. 第五題
下面代碼運行結果是什么?
- (void)test{
__block Person *person = [[Person alloc] init];
person.age = 20;
__weak Person *weakPerson = person;
self.block = ^{
__strong Person *strongPerson = weakPerson;
NSLog(@"block-begin:%@ age = %d",[NSThread currentThread],strongPerson.age);
[NSThread sleepForTimeInterval:1.0f];
NSLog(@"block-eng:%@ age = %d",[NSThread currentThread],strongPerson.age);
};
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self test1];
});
NSLog(@"test - end");
}
- (void)test1{
self.block();
}
// ***************打印結果***************
2020-01-09 10:17:45.993182+0800 AppTest[76745:8367628] test - end
2020-01-09 10:17:45.993225+0800 AppTest[76745:8367733] block-begin:<NSThread: 0x6000038b9bc0>{number = 5, name = (null)} age = 0
2020-01-09 10:17:46.997579+0800 AppTest[76745:8367733] block-eng:<NSThread: 0x6000038b9bc0>{number = 5, name = (null)} age = 0
解釋:
在并發隊列中通過異步函數添加任務執行test1
,是開啟一個新線程來執行,而新線程是先睡眠0.1秒再執行test1
,所以會先執行異步函數后面的代碼,所以等到開始執行test1
時,test
已經執行結束了,所以在執行block之前person
就已經被釋放了,這種情況下__strong
修飾符是不起作用的。
如果我將并發隊列換成串行隊列會怎么樣?
dispatch_async(dispatch_queue_create("testQueue", DISPATCH_QUEUE_SERIAL), ^{
[NSThread sleepForTimeInterval:0.1f];
[self test1];
});
// ***************打印結果***************
2020-01-09 10:34:46.120004+0800 AppTest[76893:8377451] test - end
2020-01-09 10:34:46.222708+0800 AppTest[76893:8377542] block-begin:<NSThread: 0x6000022ce400>{number = 5, name = (null)} age = 0
2020-01-09 10:34:47.226216+0800 AppTest[76893:8377542] block-eng:<NSThread: 0x6000022ce400>{number = 5, name = (null)} age = 0
其實這里不管是改成主隊列還是自定義的串行隊列結果都一樣,只要保證是異步函數就行,異步函數不會阻塞當前線程,所以執行test1
時test
已經執行完了。
這種情況下如果我將異步函數換成同步函數,其他地方不變:
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
[NSThread sleepForTimeInterval:0.1f];
[self test1];
});
// ***************打印結果***************
2020-01-09 10:26:02.978640+0800 AppTest[76834:8372825] block-begin:<NSThread: 0x600000cf5040>{number = 1, name = main} age = 20
2020-01-09 10:26:03.979483+0800 AppTest[76834:8372825] block-eng:<NSThread: 0x600000cf5040>{number = 1, name = main} age = 20
2020-01-09 10:26:03.979717+0800 AppTest[76834:8372825] test - end
因為同步函數會阻塞當前線程,所以是等test1
執行結束后,test
才會繼續執行后面的代碼,所以person
是在block執行結束后才被釋放的。
6. 第六題
下面代碼運行結果是什么?
- (void)test{
__block Person *person = [[Person alloc] init];
person.age = 20;
__weak Person *weakPerson = person;
self.block = ^{
__strong Person *strongPerson = weakPerson;
NSLog(@"block-begin:%@ age = %d",[NSThread currentThread],strongPerson.age);
[NSThread sleepForTimeInterval:1.0f];
NSLog(@"block-eng:%@ age = %d",[NSThread currentThread],strongPerson.age);
};
[self performSelector:@selector(test1) withObject:nil afterDelay:.0f];
NSLog(@"test - end");
}
- (void)test1{
self.block();
}
// ***************打印結果***************
2020-01-09 10:38:48.341591+0800 AppTest[76937:8379870] test - end
2020-01-09 10:38:48.357055+0800 AppTest[76937:8379870] block-begin:<NSThread: 0x600002e1ad00>{number = 1, name = main} age = 0
2020-01-09 10:38:49.358265+0800 AppTest[76937:8379870] block-eng:<NSThread: 0x600002e1ad00>{number = 1, name = main} age = 0
解釋:
performSelector:withObject:afterDelay
這個方法底層實現實際上是將一個定時器添加到了runloop
中,然后等時間到了后就執行test1
方法。雖然這里最后一個參數傳的是0,也就是等待0秒后執行test1
,但它并不是立馬執行,因為需要先喚醒runloop
,這是要耗一定時間的,所以會先執行后面的方法。所以等到開始執行test1
是test
已經執行結束了,person
已經釋放了。
7. 第七題
下面代碼會造成什么上面后果(self的block是用copy修飾的)?
- (void)test{
self.age = 20;
self.block = ^{
NSLog(@"%d",self.age);
};
self.block();
}
解答:
會因循環引用而導致內存泄露。因為self
通過一個強指針指向了block,而block內部又捕獲了self
而且用強指針指向self
,所以self
和block
互相強引用對方而造成循環引用。
解決這個問題很簡單,只需要定義一個弱指針指向self
,然后block內部就是用一個若指針指向self
,所以結果是self
強引用block,block弱引用self
,所以不會造成循環引用。
- (void)test{
self.age = 20;
__weak typeof(self) weakSelf = self;
self.block = ^{
NSLog(@"%d",weakSelf.age);
};
self.block();
}
這里說明一點,我們判斷會不會造成循環引用關鍵看block有沒有捕獲并強引用self
。我們只需要記住以下下幾點:
- 如果block內部使用到了某個變量,而且這個變量是局部變量,那么block會捕獲這個變量并存儲到block底層的結構體中。
- 如果捕獲的這個變量是用
__weak
或__unsafe_unretained
修飾的,那么block內部就是用弱指針指向這個變量(也就是block不持有這個對象),否則block內部就是用強指針指向這個對象(也就是block持有這個對象)。 -
self
是一個局部變量。因為self
是所有OC方法的一個隱藏參數,所以它是一個局部變量。 - 如果
self
并不持有這個block,block內部怎么引用self
都不會造成循環引用。
8. 第八題
下面代碼是否會造成循環引用:
- (void)test{
self.age = 20;
self.block = ^{
NSLog(@"%d",self.age);
};
}
解答:
會造成循環引用,這里雖然沒有調用block,但并不影響它們相互引用的結果。
- (void)test1{
self.age = 20;
self.block = ^{
NSLog(@"%d",_age);
};
self.block();
}
解答:
會造成循環引用。block里面雖然看不到self
,實際上_age
這種寫法只是省略了self
,完整寫法是self->_age
,所以是會造成循環引用的。這一點開發中要格外注意。
- (void)test{
self.block = ^{
[self setAge:10];
};
self.block();
}
解答:
會造成循環引用。OC中調用方法就是給某個對象發送消息,所以調用方法時是需要用到self
的,所以block會捕獲self
并強引用它。
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"%d",self.age);
});
[UIView animateWithDuration:1.0f animations:^{
NSLog(@"%d",self.age);
}];
解答:
不會造成循環引用。當block是某個函數的參數時,雖然block內部是對self
強引用的,但self
并不持有block,所以不會造成循環引用。
- (void)test1{
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
self.block = ^{
NSLog(@"%d",self.age);
};
self.block();
});
}
解答:
會造成循環引用。這是一個嵌套的block,雖然外層block不會循環引用,但是里面的block會造成循環引用。
typedef void(^MyBlock)(void);
- (void)test{
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
self.age = [self testWithBlock:^{
NSLog(@"%d",self.age);
}];
});
}
- (int)testWithBlock:(MyBlock)myBlock{
myBlock();
return 10;
}
解答:
不會造成循環引用。這也是嵌套的block。外層block并沒有被self
持有,所以不會造成循環引用。我們主要看下里面那個block,其實里面的block就是一個函數的參數,self
并不持有它,所以不會造成循環引用。
- (void)test1{
self.block = [self blockWithBlock:^{
NSLog(@"%d",self.age);
}];
self.block();
}
- (MyBlock)blockWithBlock:(MyBlock)myBlock{
myBlock();
return ^{
NSLog(@"block作為返回值");
};
}
解答:
不會造成循環引用。這道題很具有迷惑性。有人會覺得self
是持有block的,而block內部又強引用了self
,所以會造成循環引用。其實仔細觀察會發現這里有2個不同的block,self
持有的是一個block,而強引用self
的又是另外一個block,所以它們并不會造成循環引用。
- (void)test1{
self.block = [self blockWithBlock:^{
NSLog(@"作為參數的block");
}];
self.block();
}
- (MyBlock)blockWithBlock:(MyBlock)myBlock{
myBlock();
return ^{
NSLog(@"block作為返回值--%d",_age);
};
}
解答:
會造成循環引用。這道題同樣具有迷惑性。表面上看起來self
持有的是一個block,作為參數的又是另外一個block,而且這個block里面也沒有用到self
。其實這里的關鍵在于第二個函數的返回值,它返回的就是一個block,這個block里面強引用了self
,而且將這個block賦值給了self
的block,所以self
是強指針指向這個返回的block的。所以它們構成了循環引用。