詳解 iOS 中的閉包(block)

block 的概念

這篇文章我打算來深究一下 OC 中的 block 到底是何方神圣。后面會介紹用可愛的 clang 指令來看看 block 底層的實現。

塊對象 (block Object)是在 mac OS 10.6 及 iOS 4.0 平臺下可以使用的功能,他不是 OC 而是 C 語言的功能實現。蘋果公司的文檔中將其稱為塊對象或 Block,在其他編程語言中,他與閉包(closure)的功能基本相同。

從 C 語言 block 說起

先從一個 C 函數說起

#include <stdio.h>

void myfunc(int m, void (^b)(void)) {
    printf("%d: ", m);
    b();
}

int global = 1000; // 外部變量(全局靜態變量)

int main(int argc, const char * argv[]) {

    void (^block)(void);
    static int s = 20; // 局部靜態變量
    int a = 20; // 自動變量(局部變量)

    block = ^{     // ============ 1
        printf("%d, %d, %d\n", global, s, a);
    };
    myfunc(1, block);

    s = 0;
    a = 0;
    global = 5000;
    myfunc(2, block);

    block = ^{       // ============ 2
        printf("%d, %d, %d\n", global, s, a);
    };
    myfunc(3, block);

    return 0;
}

仔細讀代碼,想想輸出結果是什么
輸出結果是

1: 1000, 20, 20
2: 5000, 0, 20
3: 5000, 0, 0

上面結果中,第一行沒有問題,第二行是為什么呢?可以發現,變量 global 和 s 的值都改變了,但是局部變量 a 的值沒有改變。第三行顯示的是在代碼 2 處代入塊對象后的變量值,此處的變量 a 的值已經改變了。
綜上,塊對象貌似只在塊句法中保存自動變量的值。(我們所說的自動變量其實就是函數內的局部變量,通常不用 static 關鍵字修飾)
塊對象就是把可以執行的代碼和代碼中可訪問的變量封裝起來,使得之后可以進一步處理的包。
綜上,總結一下

  • block 內部可以直接訪問全局變量(外部變量)和靜態變量,也可以直接改變其值

  • 但是對于局部變量,塊句法會將其從 棧區 copy 一份到 堆區,所以即使最初的變量發生了變化,塊內部在使用的時候也不知道。而且變量的值只可以被讀取不能被改變。自動變量在運行時就相當于 const 修飾的變量。

    image

可以通過 __block 來完成在 block 內部對局部變量的修改。
注意:

__block 變量不是靜態變量,它在塊句法每次執行塊句法時獲取變量的內存區域。也就是說,__block 變量在同一個變量作用域中被多個 塊對象 訪問的時候,其實訪問的是同一塊內存區域。

OC 中 block 的注意點解析

塊句法中使用其他任意實例對象

前面已經講了塊句法中有外部變量或自動變量時這些變量的行為,現在我們來介紹一下塊句法內使用對象時的行為,特別是引用計數器的處理。

void (^cp)(void); // 可以保存塊的靜態變量

- (void)someMethod {
    id obj = ...; // 引用任意實例對象
    int n = 10;
    void (^block)(void) = ^{
        [obj calc: n];
    };
    // ...
    cp = [block copy];
}

如上代碼,塊對象在棧上生成,變量 obj 引用任何實例變量時,塊對象內使用的變量 obj 也會訪問同一個對象,這時實例變量的引用計數不會發生改變。接著塊對象復制到堆區,實例對象的引用計數加 1,由于方法執行結束后自動變量 obj 也會消失,因此這時塊對象就成為了所有者。注意實例對象是被共享的,不是復制的。所以不只是從塊對象,從哪里都可以發送消息。

image
塊句法中使用同一類的實例變量

先上代碼

void (^cp)(void); // 可以保存塊的靜態變量

- (void)someMethod {
    int n = 10;
    void (^block)(void) = ^{
        [ivar calc: n]; // 注:ivar 為該類實例變量
    };
    // ...
    cp = [block copy];
}

這種情況下,當對象唄復制時,self 的引用計數會加 1,而非 ivar。注意,塊句法中的實例變量為整數或實數時也是一樣的(這點容易搞錯)。

image
綜上總結
  • 方法定義內的塊句法中存在實例變量時,可以直接訪問實例變量,也可以修改其值。(因為是指向同一塊內存區域)
  • 方法定義內的塊句法中存在實例變量時,如果被 copy 到堆區,self 引用計數會加 1。實例變量不一定是對象。
  • 塊句法中存在非實例變量的實例對象時,被 copy 后,這個對象的引用計數會加 1。
  • 已經復制后,堆區中某個塊對象即使再次收到 copy 方法,結果也只是塊對象自身的引用計數 1。包含的對象的引用計數不變。
  • 復制的塊對象在被釋放時,也會向包含的對象發送 release。

OC 中的 block 到底是什么呢?

本著刨根問底的精神,就來一探究竟,block 到底是何方神圣。
我們創建一個純凈的 Command Line Tool 項目,在 main.m 中書寫一下簡單的代碼:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        int age = 10;
        void (^block)() = ^{
            NSLog(@"======>%d", age);
        };
        age = 20;
        block();
    }
    return 0;
}

然后打開終端,cd 該目錄下,鍵入

ZK$ clang -rewrite-objc main.m 

然后在該路徑下生成 main.cpp 文件,打開后驚奇發現短短幾句 OC 代碼,竟然生成了 九萬多行 C++ 代碼,別怕,我們寫的核心 block 代碼其實也沒多少行。拉到最下面,就是我們重寫出來的 block C++ 代碼,為了閱讀方便,我對這些代碼進行了稍微處理,比如去掉類型強轉等干擾性代碼,就得到了下面這一片精美的 C++ 代碼,我還貼心地加了一些注釋。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
    // 下面這些代碼值這個結構體的構造函數
    // `int flags=0` 是默認值
    // `: age(_age)` C++ 語法,將 _age 傳給 age 屬性,可知在沒有 __block 情況下,從外部傳進來的 age 直接就賦值給這個結構體的 age。所以相當于寫死了,不能修改。外部改變了也無法獲知。
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp; // block 生成的函數被保存在這個屬性中
    Desc = desc;
  }
};
// 下面這個函數就是 block 最終生成的一個函數體
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_d0_jq4b136d16j6t2ktg954pmcw0000gn_T_main_0f9b1b_mi_0, age);
        }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; // 創建結構體并賦值
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int age = 10;
        // 從下面這句代碼得知,block 就是指向一個結構體的指針。
        // 參1:block 生成的函數
        // 參2:`__main_block_desc_0_DATA` 結構體的指針
        // 參3:將上面的自動變量直接傳遞進去
        void (*block)() = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age)); // 在這里是直接將 10 傳遞進去
        age = 20; // 該處的 age 賦值在 block 里面根本無法感知
        // 調用 block,`(block)->FuncPtr` 就是 `__main_block_impl_0` 函數
        ((block)->FuncPtr)(block);
    }
    return 0;
}

必要的說明已經在上面代碼的注釋中說的很明白,我來總結一下,定義 block 的時候,首先會生成一個結構體 __main_block_impl_0,他有三個參數,參1是 block 生成的函數__main_block_func_0,參2是結構體 __main_block_desc_0_DATA 的地址。參3 就是我們直接傳遞進去的自動變量。三個參數傳遞進去 __main_block_impl_0 后會直接出發其構造函數,上面注釋說明很明確。
那么,目光轉回 __main_block_func_0 函數,int age = __cself->age; 這句代碼是將 age 屬性直接取出來,而這個 age 就是我們剛一開始上面提到的參3傳遞進去的自動變量的值 10,固然打印出來的是 10,不是 20。

還不過癮?那么我們 __block 修飾一下自動變量,看看有什么神奇的地方
注意啦,OC 代碼改成如下

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        __block int age = 10;
        void (^block)() = ^{
            NSLog(@"======>%d", age);
        };
        age = 20;
        block();
    }
    return 0;
}

運行 clang 指令,讓我們看看有哪些變化。

// 這個結構體用來修飾 __block 的自動變量,竟然發現了我們熟悉的老面孔 `isa`!說明他也是一個對象。
struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // by ref
  __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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_age_0 *age = __cself->age; // bound by ref

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_d0_jq4b136d16j6t2ktg954pmcw0000gn_T_main_cff06d_mi_0, (age->__forwarding->age));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        // 變化1:age 不是用 int 修飾了,而是增加一個名為 `__Block_byref_age_0` 的結構體,詳見上面這個結構體的定義有注釋。
        __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {0, &age, 0, sizeof(__Block_byref_age_0), 10};
        // 變化2:注意,下面的參3的 age 多了個 `&` 符號取地址,說明 `__main_block_impl_0` 引用的是結構體 `__Block_byref_age_0`的指針,不向之前直接將自動變量的值傳遞進去了,這也就是為什么 定義 block 后外部自動變量修改了,block 內部依然可以讀到最新值。同時,這樣我們也可以在 block 內部修改外部自動變量的值。
        void (*block)() = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &age, 570425344));
        (age.__forwarding->age) = 20;
        ((block)->FuncPtr)(block);
    }
    return 0;
}

上面的主要變化已經在注釋說明了,我再總結一下重要的變化:

  • 變化1:age 不是用 int 修飾了,而是增加一個名為 __Block_byref_age_0 的結構體,這個結構體用來修飾 __block 的自動變量,竟然發現了我們熟悉的老面孔 isa!說明他也是一個對象。
  • 變化2:__main_block_impl_0 的參3的 age 多了個 & 符號取地址,說明 __main_block_impl_0 引用的是結構體 __Block_byref_age_0的指針,不向之前直接將自動變量的值傳遞進去了,這也就是為什么 定義 block 后外部自動變量修改了,block 內部依然可以讀到最新值。同時,這樣我們也可以在 block 內部修改外部自動變量的值。
  • 變化3:還有像添加了 __main_block_copy_0__main_block_dispose_0 結構體等變化
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。