三年后再看block

三年前,開始接觸使用block的時候,覺得block的語法很怪異,也不理解block的原理,只是覺得block比代理更高級,會用block的人就牛逼。那時候看唐巧大神的博客,跟著他的博客學習。

時光荏苒,現(xiàn)在在看唐巧大神的博客,已經很少看到他在更新技術相關的文章了,基本上都是在更新一些看過的書籍之類的,很明顯他轉管理了,應該還很成功。唐巧好多事情,都是走在了絕大多數(shù)程序員的前面,很有前瞻性。

再看看巧大的block文章,當時覺得看起來特別費勁的。

唐巧的文章主要是將block是怎么實現(xiàn)的,現(xiàn)在看來高級編程里面講的要更詳細,細節(jié)更多一些。但是在那個時候,大概6,7年前,他能總結到這個水平,還是很厲害,很超前的。

現(xiàn)在回過頭來,反思自己當初學習的時候,覺得當時的學習方法和心態(tài)都有很大的問題,當時學的費勁,很大程度上,是這兩方面的問題。

問題

  1. 沒有看清楚他整體文章思路,因為有好多c語言的復雜代碼都是通過clang編譯的,并不需要完全記住,給當時看文章的時候的增加了很大的難度
  2. 站在了一個讀者的角度,他這篇文章其實講述的是一個實踐的過程,所以,應該站在他的角度,最好能動手實踐。

方案

  1. 這次在重新開一遍文章
  2. 根據他提供的參考資料,看看自己是否能寫出一遍跟他的文章類似的高質量block文章

block和delegate的區(qū)別

  1. 首先,我想說的是blcok 和 delegate的區(qū)別,其實在iOS 開發(fā)中,使用block 和 delegate 的目的,基本都是為了實現(xiàn)回調。
    block 的語法特點是代碼集中,是一個集中的代碼塊?;谒倪@一個特點,block 比較適合作為api設計的一部分,比如網絡請求,需要有一個異步回調的操作,去把異步下載下來的數(shù)據,發(fā)送給對應的接受者。
    delegate 的聲明部分和實現(xiàn)部分是分開的,比如UITableViewDelegate的聲明和實現(xiàn)分別在UITableView中和某一個UIViewController中,是分散代碼塊。
    delegate 這樣設計適用于公共接口較多的情況,這樣做也更容易解耦
    這個就是iOS 開發(fā)中,block和delegate最明顯的一個區(qū)別了。
    2.也有另外一個區(qū)別,從性能上來說,block的運行成本,要比delegate的運行成本高。
    block出棧時,需要將使用的數(shù)據從棧內存賦值到堆內存。delegate只保存一個對象指針,直接回調,并沒有額外消耗,想比C語言的函數(shù)指針,只多了一個查表動作。

我覺得而面試的時候,基本上也就問道這里就結束了。因為后面的分析,真的挺麻煩的。

block的實現(xiàn)原理

  1. block 是什么
    block 是含有自定義變量匿名函數(shù)
  2. 什么是匿名函數(shù)
    無論是c語言還是oc語言,我們正常聲明一個函數(shù)的時候,都是需要定義函數(shù)名的。block可以聲明匿名函數(shù),用^來作為標識,其實在block的數(shù)據結構定義里面,后續(xù)還是會把匿名函數(shù)轉換為正常的c語言函數(shù)
  3. 什么是自定義變量
    block 具有截獲自動變量的功能,block的代碼塊里面如果有使用了外部的變量,block結構體中,會自動保存使用的外部變量。
    但是保存的代碼塊中的自動變量,不能修改,如果想修改,需要使用__block修飾外部變量。
  4. block 的結構定義
    block的結構定義這塊的代碼還有細節(jié)非常多,當時也是我看的非常亂的,而且現(xiàn)在也沒有完全記住。我看巧大的博客,也是只記錄了block的結構定義的關鍵點,然后通過clang將oc的源碼改寫成c語言進行分析的。

下面我也來按巧神發(fā)現(xiàn)問題解決問題的思路分析一下

  • block的數(shù)據結構介紹
  • block的三種類型及相關的內存管理方式
  • block如何通過capture方式來訪問函數(shù)外部的變量
  1. block的數(shù)據結構介紹
image.png

其實這個block的結構體相對來說是比較比較好理解的,

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};
struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

從圖中我們可以看出來,block 的結構體有6個成員變量,但是這個跟clang 將objective-c轉換過來的還是有區(qū)別的。clang裝換的里面還有構造函數(shù),block的實例是通過構造函數(shù)生成的。不過這個Block_layout看起來更通俗易懂。

  • isa
    任何對象都有isa指針,isa指針指向的是一個類對象,runtime中有詳細的定義
  • flags
    表示block的附加信息,block copy 的實現(xiàn)代碼中有對它的應用
  • reserved
    保留版本號,沒有特別的用處
  • invoeke
    函數(shù)指針,指向具體的block函數(shù)實現(xiàn)的地址
  • Block-descriptor
    block的函數(shù)附加信息,包括大小,copy,dispose的函數(shù)指針。
  • variables
    capture 過來的變量,block能夠訪問它的外部局部變量,就是因為將這些變量復制到了結構體中。
    其實這還存在一個問題,為什么block已經把外部局部變量復制到了結構體中,但是在block代碼塊中修改外部局部變量,仍然要將外部局部變量用__block去修飾。
用clong工具分析得出來的代碼,這個地方巧大做了特別說明,就是為什么clang分析出來的代碼和上面圖中的代碼有些不一樣,clong里面的代碼是嵌套的,而上圖的代碼卻不是嵌套的。

這個問題我以前也沒有注意過,我當時以為是作者為了表述清楚,故意簡化了,原來并不是這樣。

巧神給的代碼例子,我copy過來了

struct SampleA {
    int a;
    int b;
    int c;
};
struct SampleB {
    int a;
    struct Part1 {
        int b;
    };
    struct Part2 {
        int c;
    };
};

原因就是結構體本身,不帶有任何的附加信息
敲黑板,重要的事情,說三遍

  1. block 的三種類型以及相關的內存管理方式
    現(xiàn)在看,三種類型無非就是block的實例放到哪里,可以放到,堆上,棧上,還有全局代變量區(qū)。
    _NSConcreteGlobalBlock 全局的靜態(tài) block,不會訪問任何外部變量。
    _NSConcreteStackBlock 保存到棧區(qū)的block,當函數(shù)返回時會被銷毀。
    _NSConcreteMallocBlock 保存到堆區(qū)的block,當引用計數(shù)為0時被銷毀。

細節(jié)實現(xiàn)

  • 全局的靜態(tài)block如何實現(xiàn)的
    建一個名為 block1.c 的源文件:
#include <stdio.h>
int main()
{
    ^{ printf("Hello, World!\n"); } ();
    return 0;
}

然后在命令行中輸入clang -rewrite-objc block1.c即可在目錄中看到 clang 輸出了一個名為 block1.cpp 的文件。該文件就是 block 在 c 語言實現(xiàn),我將 block1.cpp 中一些無關的代碼去掉,將關鍵代碼引用如下:

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __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;
    }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("Hello, World!\n");
}
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()
{
    (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) ();
    return 0;
}

下面我們就具體看一下是如何實現(xiàn)的。__main_block_impl_0 就是該 block 的實現(xiàn),從中我們可以看出:

  1. 一個 block 實際是一個對象,它主要由一個 isa 和 一個 impl 和 一個 descriptor 組成。
  2. 在本例中,isa 指向 _NSConcreteGlobalBlock, 主要是為了實現(xiàn)對象的所有特性,在此我們就不展開討論了。
  3. 由于 clang 改寫的具體實現(xiàn)方式和 LLVM 不太一樣,并且這里沒有開啟 ARC。所以這里我們看到 isa 指向的還是_NSConcreteStackBlock。但在 LLVM 的實現(xiàn)中,開啟 ARC 時,block 應該是 _NSConcreteGlobalBlock 類型,具體可以看 《objective-c-blocks-quiz》 第二題的解釋。
  4. impl 是實際的函數(shù)指針,本例中,它指向 __main_block_func_0。這里的 impl 相當于之前提到的 invoke 變量,只是 clang 編譯器對變量的命名不一樣而已。
  5. descriptor 是用于描述當前這個 block 的附加信息的,包括結構體的大小,需要 capture 和 dispose 的變量列表等。結構體大小需要保存是因為,每個 block 因為會 capture 一些變量,這些變量會加到 __main_block_impl_0 這個結構體中,使其體積變大。在該例子中我們還看不到相關 capture 的代碼,后面將會看到。

這塊巧大說的很細節(jié)了。幾年前是真沒看懂?,F(xiàn)在感覺還可以。

  • 存在棧上的block的實現(xiàn)
    我們另外新建一個名為 block2.c 的文件,輸入以下內容:
#include <stdio.h>
int main() {
    int a = 100;
    void (^block2)(void) = ^{
        printf("%d\n", a);
    };
    block2();
    return 0;
}

用之前提到的 clang 工具,轉換后的關鍵代碼如下:

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int a;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int a = __cself->a; // bound by copy
    printf("%d\n", a);
}
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 a = 100;
    void (*block2)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a);
    ((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);
    return 0;
}

1.isa指針指向了_NSConcreteStackBlock,說明這是一個分配到棧上的實例。
2.main_block_impl_0 中增加了一個變量 a,在 block 中引用的變量 a 實際是在申明 block 時,被復制到 main_block_impl_0 結構體中的那個變量 a。因為這樣,我們就能理解,在 block 內部修改變量 a 的內容,不會影響外部的實際變量 a。
3.main_block_impl_0 中由于增加了一個變量 a,所以結構體的大小變大了,該結構體大小被寫在了 main_block_desc_0 中。
修改上面的源碼,在變量前面增加 __block 關鍵字:

#include <stdio.h>
int main()
{
    __block int i = 1024;
    void (^block1)(void) = ^{
        printf("%d\n", i);
        i = 1023;
    };
    block1();
    return 0;
}

查看轉換后的代碼

struct __Block_byref_i_0 {
    void *__isa;
    __Block_byref_i_0 *__forwarding;
    int __flags;
    int __size;
    int i;
};
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_i_0 *i; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__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_i_0 *i = __cself->i; // bound by ref
    printf("%d\n", (i->__forwarding->i));
    (i->__forwarding->i) = 1023;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 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()
{
    __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024};
    void (*block1)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344);
    ((void (*)(__block_impl *))((__block_impl *)block1)->FuncPtr)((__block_impl *)block1);
    return 0;
}

轉換后的代碼,明顯比之前增加了好多代碼

  1. 源碼中增加了Block_byref_i_0 的結構體,這個結構體是用來保存截獲并且要修改的變量i的。
    注意,當不用__block修飾的時候,__main_block_impl_0結構體中只是增加一個變量,而用__block修飾的時候,__main_block_impl_0結構體中又增加了一個結構體Block_byref_i_0
  2. __Block_byref_i_0 結構體中帶有 isa,說明它也是一個對象。
  3. main_block_impl_0 中引用的是 Block_byref_i_0 的結構體指針,這樣就可以達到修改外部變量的作用。
  4. 我們需要負責 Block_byref_i_0 結構體相關的內存管理,所以 main_block_desc_0 中增加了 copy 和 dispose 函數(shù)指針,對于在調用前后修改相應變量的引用計數(shù)。
  • NSConcreteMallocBlock 類型的 block 的實現(xiàn)
    NSConcreteMallocBlock 類型的 block 通常不會在源碼中直接出現(xiàn),因為默認它是當一個 block 被 copy 的時候,才會將這個 block 復制到堆中。以下是一個 block 被 copy 時的示例代碼,可以看到,在第 8 步,目標的 block 類型被修改為 _NSConcreteMallocBlock。
static void *_Block_copy_internal(const void *arg, const int flags) {
    struct Block_layout *aBlock;
    const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
    // 1
    if (!arg) return NULL;
    // 2
    aBlock = (struct Block_layout *)arg;
    // 3
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    // 4
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    // 5
    struct Block_layout *result = malloc(aBlock->descriptor->size);
    if (!result) return (void *)0;
    // 6
    memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
    // 7
    result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
    result->flags |= BLOCK_NEEDS_FREE | 1;
    // 8
    result->isa = _NSConcreteMallocBlock;
    // 9
    if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
        (*aBlock->descriptor->copy)(result, aBlock); // do fixup
    }
    return result;
}

變量的復制

  1. 對于block 外部變量的引用,block默認是將其復制到block的結構體中來實現(xiàn)訪問。
image.png

2.對于用__block修飾的外部變量引用,block是復制其應用地址來實現(xiàn)訪問的。


image.png

ARC對block的影響

在 ARC 開啟的情況下,將只會有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 類型的 block。

原本的 NSConcreteStackBlock 的 block 會被 NSConcreteMallocBlock 類型的 block 替代。證明方式是以下代碼在 XCode 中,會輸出 <__NSMallocBlock__: 0x100109960>。在蘋果的 官方文檔 中也提到,當把棧中的 block 返回時,不需要調用 copy 方法了。

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
{
    @autoreleasepool {
        int i = 1024;
        void (^block1)(void) = ^{
            printf("%d\n", i);
        };
        block1();
        NSLog(@"%@", block1);
    }
    return 0;
}

唐巧認為這么做的原因是,由于 ARC 已經能很好地處理對象的生命周期的管理,這樣所有對象都放到堆上管理,對于編譯器實現(xiàn)來說,會比較方便。

花了一下午時間,又重新梳理了block,目前已經基本都可以看懂了,以后還要更加深入的了解block的底層實現(xiàn)。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容