深入了解Block的奧秘

前言

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;
};
783864-3ad5d92333756aa7 (1).jpeg

block如何將變量傳遞及持有

測試代碼:

傳遞原則:

  1. 捕獲對象是基礎類型變量,如int, double類型時,是值傳遞。
  2. 只有用__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會自動復制到堆上:

  1. 調用Block的copy方法
  2. 將Block作為函數返回值時
  3. 將Block賦值給__strong修改的變量時
  4. 向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實現真是煞費苦心。我們了解了原理,用起來會更加得深應手。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容