iOS Block淺淺析

Hsusue發表於2019-03-10

前言

Block真的難,筆者靜下心來讀《Objective-C 高階程式設計 iOS與OS X多執行緒和記憶體管理》,讀的時候順便記錄下來自己的心得,方便以後再翻回,也希望能帶給大家一些幫助。

本文將以一個菜dog的角度,從 Block 不截獲變數、截獲變數不修改、截獲並修改變數 、 截獲物件 四個層次 淺淺探究Block的實現。

Block的語法就不回顧了,不好記Block語法可以翻這篇How Do I Declare A Block in Objective-C?


Block實現

轉成C++ 的原始碼學習,筆者加了適當的註釋方便理解。

不截獲自動變數值

int main()
{
    void (^blk)(void) = ^{printf("Block\n");};
    
    blk();
    
    retrun 0;
}
複製程式碼

將轉為

// block中通用的成員變數 結構體
// 文章後面的程式碼不再給出,但都有用到
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

// 代表Block 的結構體
struct __main_block_impl_0 {
    struct __block_impl impl;// block通用的成員變數
    struct __main_block_desc_0* Desc;// block 的大小
    
    // 建構函式
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

// 原本的程式碼塊 轉到一個C函式
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
    printf("Block\n");
}

// 計算block大小的結構體
// 宣告的同時,初始化一個變數__main_block_desc_0_DATA
static struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long Block_size;
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)
};

int main()
{
    // 宣告定義block
    // 用到了建構函式方法
    void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
    /*
        相當於以下
        struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
        
        struct __main_block_impl_0 *blk = &tmp;
        
        棧上生成的結構體例項的指標,賦值給變數blk。
    */
    
    // 呼叫block
    // 第一個引數為 blk_>FuncPtr,即C函式
    // 第二個引數為 blk本身
    ((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
    /*
        相當於以下
        普通的C函式呼叫
        (*blk->impl.FuncPtr)(blk);
    */
    
    return 0;
}
複製程式碼

即把原本的程式碼塊,轉到一個C函式中。並且建立一個 代表Block 的結構體,最後一個建構函式,Block物件把函式和成員繫結起來。

截獲自動變數不修改的情況

和以上區別在於,Block結構體中的成員變數多了截獲的自動變數,並且建構函式引數也是。

int main()
{
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    void (^blk)(void) = ^{printf(fmt, val);};
    
    val = 2;
    fmt = "These values were changed.val = %d\n";
    
    blk();
    
    return 0;
}
複製程式碼

將轉為

// 跟上面一樣
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

// 代表Block 的結構體
struct __main_block_impl_0 {
    struct __block_impl impl;// block通用的成員變數
    struct __main_block_desc_0* Desc;// block 的大小
    
    // 截獲的自動變數
    // 結構體中有名字一樣的成員變數
    const char *fmt;
    int val;
    
    // 建構函式
    // 引數多了截獲的自動變數
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags = 0) : fmt(_fmt), val(_val) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

// 原本的程式碼塊 轉到一個C函式
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
    const char *fmt = __cself->fmt;
    int val = __cself->val;
    
    printf(fmt, val);
}

// 計算block大小的結構體
// 宣告的同時,初始化一個變數__main_block_desc_0_DATA
static struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long Block_size;
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)
};

int main()
{
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    void (*blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, fmt, val);
    /*
        結構體初始化如下:
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = 0;
        impl.FuncPtr = __main_block_func_0;
        Desc = &__main_block_desc_0_DATA;
        fmt = "val = %d\n";
        val = 10;
    */
    
    return 0;
}
複製程式碼

根據以上,我們知道截獲變數後,實質上是Block結構體中有一個成員變數存了起來。呼叫Block時,是訪問取結構體成員變數,而不是外面的區域性變數。

iOS Block淺淺析

Block中修改值

Block不允許修改外部變數的值。Apple這樣設計,應該是考慮到了block的特殊性,block也屬於“函式”的範疇,變數進入block,實際就是已經改變了作用域。在幾個作用域之間進行切換時,如果不加上這樣的限制,變數的可維護性將大大降低。又比如我想在block內宣告瞭一個與外部同名的變數,此時是允許呢還是不允許呢?只有加上了這樣的限制,這樣的情景才能實現。於是棧區變成了紅燈區,堆區變成了綠燈區。

iOS Block不能修改外部變數的值,指的是棧中指標的記憶體地址。下面舉幾個例子理解。

  • 非OC物件,修改會編譯錯誤。
int val = 0;
void (^blk)(void) = ^{
    val = 1;
};
複製程式碼
  • OC物件,傳送訊息可以,但改指標記憶體地址不行。

以下沒問題

    id array = [[NSMutableArray alloc] init];
    
    void (^blk)(void) = ^{
        id obj = [[NSObject alloc] init];
        [array addObject:obj];
    };
複製程式碼

以下編譯報錯

    id array = [[NSMutableArray alloc] init];
    
    void (^blk)(void) = ^{
        array = [[NSMutableArray alloc] init];
    };
複製程式碼
  • C 陣列 截獲自動變數的方法沒有實現對C語言陣列的截獲。

以下編譯錯誤

    const char text[] = "hello";
    
    void (^blk)(void) = ^{
        printf("%c\n", text[2]);
    };
複製程式碼

需改成指標

    const char *text = "hello";
    
    void (^blk)(void) = ^{
        printf("%c\n", text[2]);
    };
複製程式碼

那麼Block 要怎麼修改變數呢?

方法一:用到靜態或全域性變數

  • C 中有一個變數,允許Block改寫值。

    • 靜態變數
    • 靜態全域性變數
    • 全域性變數
  • 例子

int global_val = 1;// 全域性變數
static int static_global_val = 2;// 靜態全域性變數

int main()
{
    static int static_val = 3;// 靜態變數
    
    void (^blk)(void) = ^{
        global_val *= 1;
        static_global_val *= 2;
        static_val *= 3;
    }
    
    return 0;
}
複製程式碼

轉換後

int global_val = 1;
static int static_global_val = 2;

// 代表Block 的結構體
struct __main_block_impl_0 {
    struct __block_impl impl;// block通用的成員變數
    struct __main_block_desc_0* Desc;
    // 成員變數只多了靜態變數,原因在後面分析
    int *static_val;
    
    // 建構函式
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_staitc_val, int flags=0) : static_val(_static_val) {
        impl.isa = &_NSConcreteStackblock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

// 原本的程式碼塊 轉到一個C函式
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int *static__val = __cself->static_val;
    
    global_val *= 1;
    static_global_val *= 2;
    (*static_val) *=3;
}

// 計算block大小的結構體
// 宣告的同時,初始化一個變數__main_block_desc_0_DATA
static struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long Block_size;
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)
};

int main()
{
    static int static_val = 3;
    blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &static_val);
    
    return 0;
}
複製程式碼

為什麼成員變數只多了靜態變數呢?


這就要先了解 iOS 記憶體區域。 iOS-MRC與ARC區別以及五大記憶體區

  • 棧:
    • 由系統管理分配和釋放
    • 存放函式引數值,區域性變數值
    • iPhone OS下的主執行緒的堆疊大小是1M,第二個執行緒開始就是512KB
    • 區域性變數在程式執行期間不是一直存在,而是隻在函式執行期間存在,函式的一次呼叫結束後,變數就被撤銷,其所佔用的記憶體也被收回。
  • 堆:
    • 由程式猿管理
    • 存放程式猿建立的物件
    • C用malloc/calloc/relloc分配的區域
  • 程式碼區:
    • 存放函式的二進位制程式碼
  • 全域性區(又稱靜態區):
    • 存放全域性變數和靜態變數
    • 程式執行時一直存在
    • 由編譯器管理(分配釋放),程式結束後由系統釋放

全域性區又分為 BSS段 和 資料段(data)。

BSS段:BSS段(bss segment)通常是指用來存放程式中未初始化的或者初始值為0的全域性變數的一塊記憶體區域。BSS是英文Block Started by Symbol的簡稱。BSS段屬於靜態記憶體分配。

資料段:資料段(data segment)通常是指用來存放程式中已初始化的全域性變數的一塊記憶體區域。資料段屬於靜態記憶體分配。

但不同的是C++中,不區分有沒有初始化,都放到一塊去。

  • 文字常量區
    • 存放常量字串
    • 為了節省記憶體,C/C++/OC把常量字串放到單獨的一個記憶體區域。當幾個指標賦值給相同的常量字串時,它們實際上會指向相同的記憶體地址。

iOS Block淺淺析


再回到剛剛的程式碼上,為什麼block結構體中成員變數只多了靜態變數呢?

    int global_val = 1;// 全域性變數
    static int static_global_val = 2;// 靜態全域性變數
    static int static_val = 3;// 靜態變數
複製程式碼

關於它們的區別——全域性變數/靜態全域性變數/區域性變數/靜態區域性變數的異同點

靜態區域性變數雖然程式執行時一直存在,但只對定義自己的函式體始終可見。

編譯後,呼叫block實質上是在 一個新定義的函式 中訪問靜態區域性變數,不能直接訪問,所以需要儲存其指標。而全域性變數可以訪問到,所以沒有加到成員變數中。


方法二:用到__block 說明符

int main()
{
    __block int val = 10;
    
    void (^blk)(void) = ^{val = 1;};
    
    return 0;
}
複製程式碼

轉換後

// 變數將會變成的結構體
// 即val不是int型別,變成此結構體例項
struct __Block_byref_val_0 {
    void *__isa;// __block變數轉化後所屬的類物件
    __Block_byref_val_0 *__forwarding;//指向__block變數自身的指標,後面解釋
    int __flags;// 版本號
    int __size;// 結構體大小
    int val;// 原本的int數值
};

// 代表Block 的結構體
struct __main_block_impl_0 {
    struct __block_impl impl;// block通用的成員變數
    struct __main_block_desc_0* Desc;// block 的大小
    __Block_byref_val_0 *val;// val轉成成員變數,型別為結構體
    
    // 建構函式
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->_forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

// 原本的程式碼塊 轉到一個C函式
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
    __Block_byref_val_0 *val = __cself->val;
    
    // 這裡通過__forwarding賦值?後面解釋
    (val->__forwarding->val) = 1;
}

// 當Block從棧複製到堆時
// 通過此函式把截獲的__block變數移到堆或者引用數+1
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src)
{
    _Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}

// 當Block從堆被廢棄時
// 通過此函式把截獲的__block變數引用數-1
// 相當於物件的delloc方法
static void __main_block_dispose_0(struct __main_block_impl_0*src)
{
    _Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}

// 計算block大小的結構體
// 該結構體有兩個函式
// copy 和 dispose
// 宣告的同時,初始化一個變數__main_block_desc_0_DATA
static struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long 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()
{
    /*
        val變成了__Block_byref_val_0結構體例項
    */
    __Block_byref_val_0 val = {
        0,// isa指標
        &val,//forwarding成員,指向自己
        0,// 版本號
        sizeof(__Block_byref_val_0),
        10 //原來int val的值
    };
    
    blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);
    
    return 0;
}
複製程式碼

從main函式中,我們可以發現,Block轉換成Block的結構體型別**__main_block_impl_0的自動變數,__block變數val轉換為block變數的結構體型別__Block_byref_val_0**的自動變數。它們都在棧上。所以Block的isa指標指向NSConcreteStackBlock。


除了NSConcreteStackBlock,還有兩種型別 NSConcreteGlobalBlock 和 NSConcreteMallocBlock。

設定物件的儲存域
NSConcreteStackBlock
NSConcreteGlobalBlock 全域性區
NSConcreteMallocBlock
  • NSConcreteGlobalBlock 在全域性變數的地方生成的Block為NSConcreteGlobalBlock,如下。在全域性變數的地方不能使用自動變數,也就不存在截獲的問題。
void (^blk)(void) = ^{printf("Global Block\n");};

int main
{
    return 0;
}
複製程式碼

另外只要沒有截獲自動變數,Block型別就是NSConcreteGlobalBlock。

  • NSConcreteMallocBlock 棧上的Block,在出了作用域後會被摧毀,__block變數也是。那麼如果我們要在別的地方呼叫Block,就需要把它們移到堆中,手動管理它們的生命週期。這種Block型別就是NSConcreteMallocBlock。

先來理解為什麼有個forwarding指向自己。

試想,Block如果截獲了自動變數,然後移到堆上,在別的作用域呼叫(很常見)。如果__block變數在棧上已經釋放了,Block訪問__block變數會失敗。所以系統需要在Block變成NSConcreteMallocBlock時,截獲的__block變數也複製到堆上。

Block什麼時候會複製到堆上呢?

  • 呼叫Block的copy方法
  • 將Block作為函式返回值時
  • 將Block賦值給__strong修飾的變數時
  • 向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block引數時
__block變數的配置儲存域 Block從棧賦值到堆時的影響
從棧複製到堆並被Block持有
被Block持有

當Block從棧複製到堆時,__block變數的forwarding 會重新指向其在堆中的記憶體地址。

這樣,無論是在Block語法中、Block語法外使用__block變數,還是__block變數配置在棧上或對上,都可以順利地訪問同一個__block變數。

筆者在書上剛看到這句話時,有點暈,後來想了一段時間應該是以下意思,如果有誤,歡迎大神批鬥。

如下程式碼,有註釋

    __block int val = 0;
    
    void (^blk)(void) = [^{++val;} copy];
    
    ++val;// 轉換為++(val.__forwarding->val);即(棧上的val).__forwarding->val,最終指向堆上的val
    
    blk();// 轉換為++(val.__forwarding->val);即(堆上的val).__forwarding->val,最終指向堆上的val
    
    NSLog(@"%d", val);
複製程式碼

截獲物件

  • __strong 修飾的物件
blk_t blk;

{
    id array = [[NSMutablArray alloc] init];
    blk = [^(id obj) {
        [array addObject:obj];
        NSLog(@"array count = %ld", [array count]);
    } copy];
}

blk([NSObject alloc] init]);
blk([NSObject alloc] init]);
blk([NSObject alloc] init]);
複製程式碼

還記得上面提到的截獲變數不修改,轉為C++,Block結構體中的成員變數多了截獲的自動變數。

這裡,變數作用域結束時,理論上array被廢棄,但執行輸出結果為陣列count123。

這意味著array超出作用域而存在。

會不會也是Block結構體中的成員變數多了截獲的自動變數呢?

轉換為C++後

struct __main_block_impl_0 {
    struct __block_impl impl;// Block通用的成員變數
    struct __main_block_desc_0* Desc;// Block的大小
    // 指向陣列的成員變數
    id __strong array;
    
    // 建構函式
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,id __strong _array, int flags=0) : array(_array) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

// 原本的程式碼塊 轉到一個C函式
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
    id __strong array = __cself->array;
    
    [array addObject:obj];
    
    NSLog(@"array count = %ld", [array count]);
}

// 當Block從棧複製到堆時
// 通過此函式把截獲的物件引用數+1
// 相當於retain
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src)
{
    _Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}

// 當Block從堆被廢棄時
// 通過此函式把截獲的物件release引用數-1
// 相當於物件的delloc方法
static void __main_block_dispose_0(struct __main_block_impl_0 *src)
{
    _Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
}

// 計算block大小的結構體
// 該結構體有兩個函式
// copy 和 dispose
// 宣告的同時,初始化一個變數__main_block_desc_0_DATA
static struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long 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
};
複製程式碼

呼叫block轉換如下。

blk_t blk;

{
    id __strong array = [[NSMutableArray alloc] init];
    
    // 建構函式
    blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000);
    
    blk = [blk copy];
}

// 呼叫,第一個引數為blk本身,第二個引數為id型別物件
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
複製程式碼

可以看到,和猜測一樣,Block結構體中確實多了一個id __strong array

我們知道,我們寫的C語言結構體不能帶有__strong修飾符的變數。原因是編譯器不知道何時進行C語言結構體的初始化和廢棄操作。

但是OC執行時庫把握Block從棧複製到堆以及堆上的Block被廢棄的時機,因此Block用結構體中可以管理好

那麼同時用__block 和 __strong 修飾的物件呢?

上面提到過__block int val,val將變為一個結構體,物件也一樣。

__block id obj = [[NSObject alloc] init];
// 相當於__block id __strong obj = [[NSObject alloc] init];
複製程式碼

轉換為

// 物件將會變成的結構體
struct __Block_byref_obj_0 {
    void *__isa;// __block變數轉化後所屬的類物件
    __Block_byref_val_0 *__forwarding;//指向物件自身的指標,後面解釋
    int __flags;// 版本號
    int __size;// 結構體大小
    void (*__Block_byref_id_object_copy)(void*, void*);// retain物件
    void (*__Block_byref_id_object_dispose)(void*);// release物件
    __strong id obj;//指向物件
};

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
    _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}

static void __Block_byref_id_object_dispose_131(void *src) {
    _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

// 物件變成了__Block_byref_obj_0結構體例項
__Block_byref_obj_0 obj = {
    0,
    &obj,
    0x2000000,
    sizeof(__Block_byref_obj_0),
    __Block_byref_id_object_copy_131,
    __Block_byref_id_object_dispose_131,
    [[NSObject alloc] init]
};
複製程式碼
  • __weak 修飾的物件
blk_t blk;

{
    id array = [[NSMutableArray alloc] init];
    __block id __weak array2 = array;
    
    blk =[^(id obj) {
        [array2 addObject:obj];
        NSLog(@"array count = %ld", [array2 count]);
    } copy];
}

blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
複製程式碼

輸出結果為陣列數目0。

這是由於array在作用域結束時被釋放、廢棄,nil被賦值在array2中。

結論:Block中持有weak宣告的物件,物件引用數不會增加。


總結

  • 不截獲變數 或在全域性變數位置定義的Block

這種Block的型別是NSConcreteGlobalBlock。這種Block把程式碼塊內容轉到一個C函式中,Block結構體較簡單。

  • 截獲但不修改變數

這種Block的結構體中截獲的變數會變成成員變數。

截獲的物件也會變成成員變數(記憶體語義相同),Block複製到堆上時會呼叫__main_block_copy_0,廢棄時呼叫__main_block_dispose_0,對捕獲的強引用物件引用數造成影響。

並且建構函式、呼叫的C函式都會用到截獲的變數。

  • 截獲並修改變數
  1. 全域性和區域性靜態變數

Block結構體中沒有全域性變數和全域性靜態變數,因為可以直接用。但Block結構體會儲存區域性靜態變數的指標。

  1. 用到__block 說明符
  • 變數val

val會變成一個結構體__Block_byref_val_0,其成員變數__forwarding指向本身。

當Block從棧複製到堆時,會呼叫__main_block_copy_0,val會通過_Block_object_assign引用數+1。

當Block銷燬,會呼叫__main_block_dispose_0,val會通過_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF)引用數-1。

  • 物件

物件會變成一個結構體__Block_byref_obj_0,其成員變數__strong id obj指向物件,其成員變數__forwarding指向本身。

如果是強引用物件,Block會通過__Block_byref_id_object_copy_131,和__Block_byref_id_object_dispose_131內部引用和釋放物件。弱引用不對物件生命週期產生影響。


問題

  • Block中是否需要對弱引用的物件強引用?

到底什麼時候才需要在ObjC的Block中使用weakSelf/strongSelf

  • Block屬性中記憶體語義用copy 還是strong?

    在ARC下,這兩種效果都會把Block 從棧上壓到堆上。但事實上,copy更接近Block的本質。

block 使用 copy 是從 MRC 遺留下來的“傳統”,在 MRC 中,方法內部的 block 是在棧區的,使用 copy 可以把它放到堆區.在 ARC 中寫不寫都行:對於 block 使用 copy 還是 strong 效果是一樣的,但寫上 copy 也無傷大雅,還能時刻提醒我們:編譯器自動對 block 進行了 copy 操作。如果不寫 copy ,該類的呼叫者有可能會忘記或者根本不知道“編譯器會自動對 block 進行了 copy 操作”,他們有可能會在呼叫之前自行拷貝屬性值。這種操作多餘而低效。你也許會感覺我這種做法有些怪異,不需要寫依然寫。如果你這樣想,其實是你“日用而不知”。

  • 在這篇文章iOS-Block本質,看到許多關於Block理解的問題,對照著實現看挺有幫助。

參考

  • [1] Kazuki Sakamoto,Tomohiko Furumoto.Objective-C高階程式設計 iOS與OS X多執行緒和記憶體管理[M].北京:人民郵電出版社,2013:79-136.