前言
block可以叫回調代碼塊,是iOS開發中至關重要的形式之一。不同的編程語言都會用到block, 只是體現形式有所不同,例如c/c++
叫函數指針
,javascript
叫它閉包
。它用簡單的方式幫我們解決了很多復雜的問題。但是還是一些區別:
- block的代碼是內聯的,效率高于函數調用
- block對于外部變量默認是只讀屬性
- block被Objective-C看成是對象處理
- block可讀性更高,相比于delegate思路不會打斷
block初識
先從一個簡單的需求來說:傳入兩個數,并且計算這兩個數的和,為此創建了這樣一個block:
int (^sum)(int a, int b) = ^(int a, int b) {
return a + b;
};
block如何將變量傳遞及持有
測試代碼:
傳遞原則:
- 捕獲對象是
基礎類型
變量,如int, double類型時,是值傳遞。 - 只有用__block修飾,變量才可以被修改,可以理解為指針傳遞。
我們來驗證一下,變量a能否在block賦值之后被修改
int a = 10;
_foo.testBlock= ^() {
_testView.backgroundColor = nil;//持有了self
NSLog(@"a:%d", a);//10還是20?
};
a+= 10;
_foo.testBlock();//block調用
輸出結果: 10,它沒有被外界改變。
表面上看起來block里面的a和外面的a一個東西,但實際上相當于生成了一個新的變量a' = a; 所以a值改變,a'不會跟著變;a'值改變,a也不會變。
解決方法:可以__block來修飾int a,也就是
__block int a = 10
最終a的值就是20,它被外界改變了,__block幫我們了解決問題。
但是Why? block內部是以什么形式存在,并捕獲值的呢?接下來我們要一探究竟。
準備工作:clang命令
大家可以用clang(或者gcc) -rewrite-objc xxxxx.m
命令來查看轉化成的c++代碼來了解內幕。如果你引用了UIKIt
庫,這個命令會報錯,那個因為命令里沒有指定sdk的版本,此時用下面的命令完美解決:
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m
__block的奧秘
不帶__block的代碼轉化為cpp代碼:
int a = 10;
((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("setTestBlock:"), ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, self, a, 570425344)));
a+= 10;
((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("testBlock"))();
帶__block的代碼轉化為cpp代碼:
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};//對a的封裝進行初始化
((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("setTestBlock:"), ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, self, (__Block_byref_a_0 *)&a, 570425344)));//這里把封裝的結構體的地址傳遞了進去
(a.__forwarding->a)+= 10;//a+10
((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("testBlock"))();
__Block_byref_a_0的定義如下, 它對a進行了封裝
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;//---->它就是傳遞進來的a
};
我們發現編譯器把int a封裝
成了一個叫__Block_byref_a_0
的結構體,int a只是個迷惑人的假象,所有對a的操作都是對結構體的操作。并且用&
符號將結構體的地址傳遞給了block,所以后面a被修改時,block里面的結構體的a也被同時修改。
Block的父子層級關系及常見類別
1.我們可以打印一個Global block的類及父類的名字:(這段代碼摘自facebook的FBRetainCycleDetector
)
static Class _BlockClass() {
static dispatch_once_t onceToken;
static Class blockClass;
dispatch_once(&onceToken, ^{
void (^testBlock)() = [^{} copy];
blockClass = [testBlock class];
while(class_getSuperclass(blockClass) && class_getSuperclass(blockClass) != [NSObject class]) {
blockClass = class_getSuperclass(blockClass);
}
[testBlock release];
});
return blockClass;
}
結果是 **NSObject -> NSBlock ->__NSGlobalBlock **
事實上block有三種形式:
- __NSGlobalBlock 全局 (未捕獲變量)
- __NSStackBlock 棧 捕獲變量
- __NSMallocBlock 堆 捕獲變量
在 ARC 中,捕獲外部了變量的 block 的類會是 NSMallocBlock 或者 NSStackBlock,如果 block 被賦值給了某個變量在這個過程中會執行 _Block_copy 將原有的 NSStackBlock 變成 NSMallocBlock;但是如果 block 沒有被賦值給某個變量,那它的類型就是 NSStackBlock;沒有捕獲外部變量的 block 的類會是 NSGlobalBlock 即不在堆上,也不在棧上,它類似 C 語言函數一樣會在代碼段中。
2.那什么時候在堆上,什么時候在棧上呢?
在ARC有效時,大多數情況下編譯器會進行判斷,自動生成將Block從棧上復制到堆上的代碼,以下幾種情況棧上的Block會自動復制到堆上:
- 調用Block的copy方法
- 將Block作為函數返回值時
- 將Block賦值給__strong修改的變量時
- 向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block參數時
3.block用strong修飾還是copy修飾呢?
實際上調用retain方法時, block會調用copy方法,所以這兩種修飾是相同的。但是為了語義的明確,推薦用copy修飾。
self.testBlock = ^() {
}
反編譯代碼(也可以打斷點,切換到匯編模式查看匯編代碼)
void -[Foo setTestBlock:](void * self, void * _cmd, void * arg2) {
objc_storeStrong(var_18, arg2);
rax = objc_retainBlock(0x0);
rdi = self->_testBlock;
self->_testBlock = rax;
[rdi release];
objc_storeStrong(0x0, 0x0);
return;
}
runtime源碼如下,實際上還是調用了block_copy方法
id objc_retainBlock(id x) {
return (id)_Block_copy(x);
}
循環引用
開頭說過,block在iOS開發中被視作是對象,因此其生命周期會一直等到持有者的生命周期結束了才會結束。另一方面,由于block捕獲變量的機制,使得持有block的對象也可能被block持有,從而形成循環引用,導致兩者都不能被釋放:
self.foo.testBlock= ^() {
self.view.backgroundColor = nil;//持有了self
NSLog(@"a:%d", a);//10還是20?
};
嚴格意義上講循環引用是因為形成了一個環狀引用,參與者可能是多個,并非只有2個,這會造成這個環上的所有對象都無法被釋放。
解決方法:用__weak來修飾對象,這樣對象被捕獲后不會被強引用,引用計數器不發生變化,然后在真正用要到的時候再strong強引用,防止在使用過程中對象突然釋放
__weak __typeof__ (self) wself = self;
self.foo.testBlock = ^() {
__strong __typeof (wself) sself = wself;
sself.view.backgroundColor = [UIColor whiteColor];
};
結語
還是那句話:源碼下面無秘密。
蘋果底層對于block實現真是煞費苦心。我們了解了原理,用起來會更加得深應手。