最近看到了一道Block的面試題,還蠻有意思的,來給你們分享一下。ios
本文從一道Block面試題出發,層層深刻到達Block原理的講解,把面試題吃得透透的。面試
題外話:objective-c
不少人以爲Block的定義很怪異,很難記住。但其實和C語言的函數指針的定義對比一下,你很容易就能夠記住。架構
// Block
returnType (^blockName)(parameterTypes)
// 函數指針
returnType (*c_func)(parameterTypes)
複製代碼
例如輸入和返回參數都是字符串:框架
(char *) (*c_func)(const char *);
(NSString *) (^block)(NSString *);
複製代碼
好了,下面正式開始~iphone
如下代碼存在內存泄露麼?函數
- (void)viewDidLoad {
[super viewDidLoad];
NSNotificationCenter *__weak center = [NSNotificationCenter defaultCenter];
id token = [center addObserverForName:UIApplicationDidEnterBackgroundNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification * _Nonnull note) {
[self doSomething];
[center removeObserver:token];
}];
}
- (void)doSomething {
}
複製代碼
答案是存在!flex
block
中,咱們使用到的外部變量有self
和center
,center
使用了__weak
說明符確定沒問題。ui
center
持有token
,token
持有block
,block
持有self
,也就是說token
不釋放,self
確定無法釋放。spa
咱們注意到[center removeObserver:token];
這步會把token
從center
中移除掉。按理說,center
和self
是否是就能夠被釋放了呢?
咱們來看看編譯器怎麼說:
編譯器告訴咱們,token
在被block捕獲以前沒有初始化![center removeObserver:token];
是無法正確移除token
的,因此self
也無法被釋放!
爲何沒有被初始化?
由於token在後面的方法執行完纔會被返回。方法執行的時候token尚未被返回,因此捕獲到的是一個未初始化的值!
如下代碼存在內存泄露麼?
- (void)viewDidLoad {
[super viewDidLoad];
NSNotificationCenter *__weak center = [NSNotificationCenter defaultCenter];
id __block token = [center addObserverForName:UIApplicationDidEnterBackgroundNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification * _Nonnull note) {
[self doSomething];
[center removeObserver:token];
}];
}
- (void)doSomething {
}
複製代碼
此次代碼在token
以前加入了__block
說明符。
提示:此次編譯器沒有警告說token沒有被初始化了。
答案是仍是存在!
首先,證實token
的值是正確的,同時你們也能夠看到token
確實是持有block
的。
那麼,爲何還會泄露呢?
由於,雖然center
對token
的持有已經沒有了,token
如今還被block
持有。
可能還有同窗會問:
加入了__block說明符,token對象不是仍是center返回以後才能拿到麼,爲何加了以後就沒問題了呢?
緣由會在Block原理部分詳細說明。
如下代碼存在內存泄露麼?
- (void)viewDidLoad {
[super viewDidLoad];
NSNotificationCenter *__weak center = [NSNotificationCenter defaultCenter];
id __block token = [center addObserverForName:UIApplicationDidEnterBackgroundNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification * _Nonnull note) {
[self doSomething];
[center removeObserver:token];
token = nil;
}];
}
- (void)doSomething {
}
- (void)dealloc {
NSLog(@"%s", __FUNCTION__);
}
複製代碼
答案是不存在!
咱們能夠驗證一下:
能夠看到,咱們添加token = nil;
以後,ViewController
被正確釋放了。這一步,解除了token
與block
之間的循環引用,因此正確釋放了。
有人可能會說:
使用__weak typeof(self) wkSelf = self;就能夠解決self不釋放的問題。
確實這能夠解決self不釋放的問題,可是這裏仍然存在內存泄露!
雖然面試題解決了,可是還有幾個問題沒有弄清楚:
__block
說明符token
未被初始化,而有這個說明符以後就沒問題了呢?token
和block
爲何會造成循環引用呢?剛剛的面試題比較複雜,咱們先來看一個簡單的:
Block轉換爲C函數以後,Block中使用的自動變量會被做爲成員變量追加到 __X_block_impl_Y結構體中,其中 X通常是函數名, Y是第幾個Block,好比main函數中的第0個結構體: __main_block_impl_0。
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[])
{
@autoreleasepool
{
int age = 10;
MyBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
}
return 0;
}
複製代碼
順便說一下,這個輸出:age = 10
在命令行中對這個文件進行一下處理:
clang -w -rewrite-objc main.m
複製代碼
或者
xcrun -sdk iphoneos clang -arch arm64 -w -rewrite-objc main.m
複製代碼
區別是下面指定了SDK和架構代碼會少一點。
處理完以後會生成一個main.cpp的文件,打開後會發現代碼不少,不要怕。搜索int main就能看到熟悉的代碼了。
int main(int argc, const char * argv[])
{
/* @autoreleasepool */
{ __AtAutoreleasePool __autoreleasepool;
int age = 10;
MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
age = 18;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
複製代碼
下面是main函數中涉及到的一些結構體:
struct __main_block_impl_0 {
struct __block_impl impl; //block的函數的imp結構體
struct __main_block_desc_0* Desc; // block的信息
int age; // 值引用的age值
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock; // 棧類型的block
impl.Flags = flags;
impl.FuncPtr = fp; // 傳入了函數具體的imp指針
Desc = desc;
}
};
struct __block_impl {
void *isa; // block的類型:全局、棧、堆
int Flags;
int Reserved;
void *FuncPtr; // 函數的指針!就是經過它調用block的!
};
static struct __main_block_desc_0 { // block的信息
size_t reserved;
size_t Block_size; // block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
複製代碼
有了這些信息,咱們再看看
MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
複製代碼
能夠看到,block
初始化的時候age
是值傳遞,因此block
結構體中age=10
,因此打印的是age = 10。
Block中修改捕獲的自動變量有兩種方法:
使用靜態變量、靜態全局變量、全局變量
從Block語法轉化爲C語言函數中訪問靜態全局變量、全局變量,沒有任何不一樣,能夠直接訪問。而靜態變量使用的是靜態變量的指針來進行訪問。
自動變量不能採用靜態變量的作法進行訪問。緣由是,自動變量是在存儲在棧上的,當超出其做用域時,會被棧釋放。而靜態變量是存儲在堆上的,超出做用域時,靜態變量沒有被釋放,因此還能夠訪問。
添加 __block修飾符
__block存儲域類說明符。存儲域說明符會指定變量存儲的域,如棧auto、堆static、全局extern,寄存器register。
好比剛剛的代碼加上 __block說明符:
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[])
{
@autoreleasepool
{
int __block age = 10;
MyBlock block = ^{
age = 18;
};
block();
}
return 0;
}
複製代碼
在命令行中對這個文件進行一下處理:
xcrun -sdk iphoneos clang -arch arm64 -w -rewrite-objc main.m
複製代碼
咱們看到main函數發生了變化:
原來的age
變量:int age = 10;
如今的age
變量:__Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};。
int main(int argc, const char * argv[])
{
/* @autoreleasepool */
{ __AtAutoreleasePool __autoreleasepool;
__Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
複製代碼
原來咱們知道添加 __block說明符,咱們就能夠在block裏面修改自動變量了。
恭喜你,如今你達到了第二層!__block說明符,其實會把自動變量包含到一個結構體中。
這也就解釋了問題1爲何加入__block說明符,token能夠正確拿到值。
MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
複製代碼
此次block初始化的過程當中,把age
這個結構體傳入到了block結構體中,如今就變成了指針引用。
struct __Block_byref_age_0 {
void *__isa; //isa指針
__Block_byref_age_0 *__forwarding; // 指向本身的指針
int __flags; // 標記
int __size; // 結構體大小
int age; // 成員變量,存儲age值
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // 結構體指針引用
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
複製代碼
咱們再來看看block中是如何修改age
對應的值:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // 經過結構體的self指針拿到age結構體的指針
(age->__forwarding->age) = 18; // 經過age結構體指針修改age值
}
複製代碼
看到這裏可能不明白__forwarding的做用,咱們以後再講。如今知道是age是指針引用修改爲功的就能夠了。
從C代碼中咱們能夠看到Block的是指是Block結構體實例,__block變量實質是棧上__block變量結構體實例。從初始化函數中咱們能夠看到,impl.isa = &_NSConcreteStackBlock;
,即以前咱們使用的是棧Block。
其實,Block有3中類型:
void (^blk)(void) = ^{
NSLog(@"Global Block");
};
int main() {
blk();
NSLog(@"%@",[blk class]);//打印:__NSGlobalBlock__
}
複製代碼
全局Block確定是存儲在全局數據區的,可是在函數棧上建立的Block,若是沒有捕獲自動變量,Block的結構實例仍是 _NSConcreteGlobalBlock,而不是 _NSConcreteStackBlock:
void (^blk0)(void) = ^{ // 沒有截獲自動變量的Block
NSLog(@"Stack Block");
};
blk0();
NSLog(@"%@",[blk0 class]); // 打印:__NSGlobalBlock__
int i = 1;
void (^blk1)(void) = ^{ // 截獲自動變量i的Block
NSLog(@"Capture:%d", i);
};
blk1();
NSLog(@"%@",[blk1 class]); // 打印:__NSMallocBlock__
複製代碼
能夠看到沒有捕獲自動變量的Block打印的類是NSGlobalBlock,表示存儲在全局數據區。 但爲何捕獲自動變量的Block打印的類倒是設置在堆上的NSMallocBlock,而非棧上的NSStackBlock?這個問題稍後解釋。
設置在棧上的Block,若是超出做用域,Block就會被釋放。若 __block變量也配置在棧上,也會有被釋放的問題。因此, copy方法調用時,__block變量也被複制到堆上,同時impl.isa = &_NSConcreteMallocBlock;
。複製以後,棧上 __block變量的__forwarding
指針會指向堆上的對象。因 此 __block變量不管被分配在棧上仍是堆上都可以正確訪問。
編譯器如何判斷什麼時候須要進行copy操做呢?
在ARC開啓時,自動判斷進行 copy:
__strong
修飾的自動變量。usingBlock
的Cocoa
框架方法或GCD
相關API傳遞Block。若是不能自動 copy,則須要咱們手動調用 copy方法將其複製到堆上。好比向不包括上面提到的方法或函數的參數中傳遞Block時。
ARC環境下,返回一個對象時會先將該對象複製給一個臨時實例指針,而後進行retain
操做,再返回對象指針。runtime/objc-arr.mm
提到,Block的retain
操做objc_retainBlock
函數其實是Block_copy
函數。在實行retain
操做objc_retainBlock
後,棧上的Block會被複制到堆上,同時返回堆上的地址做爲指針賦值給臨時變量。
當Block從棧複製到堆上時候,__block變量也被複制到堆上並被Block持有。
__block int val = 0;
void (^block)(void) = [^{ ++val; } copy];
++val;
block();
複製代碼
利用 copy操做,Block和 __block變量都從棧上被複制到了堆上。不管是{ ++val; }
仍是++val;
都轉換成了++(val->__forwarding->val);
。
Block中的變量val
爲複製到堆上的 __block變量結構體實例,而Block外的變量val
則爲複製前棧上的 __block變量結構體實例,但這個結構體的__forwarding
成員變量指向堆上的 __block變量結構體實例。因此,不管是是在Block內部仍是外部使用 __block變量,均可以順利訪問同一個 __block變量。
下面咱們看看面試題的C代碼。
@interface Test : NSObject
@end
@implementation Test
- (void)test_notification {
NSNotificationCenter *__weak center = [NSNotificationCenter defaultCenter];
id __block token = [center addObserverForName:@"com.demo.perform.once"
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification * _Nonnull note) {
[self doSomething];
[center removeObserver:token];
token = nil;
}];
}
- (void)doSomething {
}
@end
複製代碼
在命令行中對這個文件進行一下處理,由於用到了 __weak說明符,須要額外指定一些參數:
xcrun -sdk iphoneos clang -arch arm64 -w -rewrite-objc -fobjc-arc -mios-version-min=8.0.0 -fobjc-runtime=ios-8.0.0 main.m
複製代碼
這個會更復雜一些,但咱們只看重要的部分:
struct __Block_byref_token_0 {
void *__isa;
__Block_byref_token_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
__strong id token; // id類型的token變量 (strong)
};
struct __Test__test_notification_block_impl_0 {
struct __block_impl impl;
struct __Test__test_notification_block_desc_0* Desc;
Test *const __strong self; // 被捕獲的self (strong)
NSNotificationCenter *__weak center; // center對象 (weak)
__Block_byref_token_0 *token; // token結構體的指針
__Test__test_notification_block_impl_0(void *fp, struct __Test__test_notification_block_desc_0 *desc, Test *const __strong _self, NSNotificationCenter *__weak _center, __Block_byref_token_0 *_token, int flags=0) : self(_self), center(_center), token(_token->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
複製代碼
如今咱們看到block
結構體 __Test__test_notification_block_impl_0中持有token
,同時以前咱們看到token
也是持有block
的,因此形成了循環引用。
這也就回答了問題2。
下面咱們看看block
的IMP函數是如何解決循環引用問題的:
static void __Test__test_notification_block_func_0(struct __Test__test_notification_block_impl_0 *__cself, NSNotification * _Nonnull __strong note) {
__Block_byref_token_0 *token = __cself->token; // bound by ref
Test *const __strong self = __cself->self; // bound by copy
NSNotificationCenter *__weak center = __cself->center; // bound by copy
((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("doSomething"));
((void (*)(id, SEL, id _Nonnull __strong))(void *)objc_msgSend)((id)center, sel_registerName("removeObserver:"), (id)(token->__forwarding->token));
(token->__forwarding->token) = __null;
}
複製代碼
能夠看到,token = nil;
被轉換爲了(token->__forwarding->token) = __null;
,至關於block
對象對token
的持有解除了!若是你以爲看不太明白,我再轉換一下:
(__cself->token->__forwarding->token) = __null; // __cself爲block結構體指針
複製代碼
細心的同窗可能發現:
impl.isa = &_NSConcreteStackBlock;
複製代碼
這是一個棧類型的block
呀,聲明週期結束不是就該被系統回收釋放了麼。咱們使用了ARC同時咱們調用是方法名中含有usingBlock
,會主動觸發 copy操做,將其複製到堆上。
Block最常問的就是循環引用、內存泄露問題。
注意要點:
另外,須要再強調一下的是:
面試題中的block代碼若是一次都沒有執行也是會內存泄露的!
可能有人會說使用__weak typeof(self) wkSelf = self;
就能夠解決self
不釋放的問題。
確實這能夠解決self
不釋放的問題,可是這裏 仍然存在內存泄露! 咱們仍是須要從根上解決這個問題。
補充:
上面講的時候集中在說token
和block
的循環引用,ViewController
的問題我簡單帶過了,可能同窗們看的時候沒有注意到。
我在這裏專門拎出來講一下:
token
和block
循環引用,同時block
持有self
(ViewController
),致使ViewController
也無法釋放。
若是但願優先釋放ViewController
(無論block
是否執行),最好給ViewController
加上__weak
說明符。
此外,破除token
和block
的循環引用,實際有兩種方法:
token = nil;
。token
也使用__weak
說明符id __block __weak token
。注意:
如下說法不夠嚴謹,也可能存在問題:
最簡單粗暴的解決辦法:你們都
__weak
。NSNotificationCenter *__weak wkCenter = [NSNotificationCenter >defaultCenter]; __weak typeof(self) wkSelf = self; id __block __weak wkToken = [wkCenter addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { [wkSelf doSomething]; [wkCenter removeObserver:wkToken]; }]; 複製代碼
這個問題具體要看
NSNotificationCenter
具體是怎麼實現的。token
使用__weak
說明符,可是若是NSNotificationCenter
沒有持有token
,在函數做用域結束時,token
會被銷燬。雖然不會有循環引用問題,可是可能致使沒法移除這個觀察者的問題。
若是以爲本文對你有所幫助,給我點個贊吧~