<<iOS 與OS X多線程和內(nèi)存管理>>筆記:Blocks實現(xiàn)(二)


前言


<<iOS 與OS X多線程和內(nèi)存管理>>筆記:Blocks中我寫的都是我們?nèi)粘i_發(fā)過程中所用到的Blocks.這里我們深層次的看一下Blocks的相關(guān)實現(xiàn).


把OC代碼轉(zhuǎn)換為C++結(jié)構(gòu)體代碼


為了使我們更方便看清Block內(nèi)部的運行,我們需要把OC代碼代碼轉(zhuǎn)化為帶有結(jié)構(gòu)體的C++代碼.這里我們就需要使用到clang -rewrite-objc指令.步驟有如下兩步.

  • 打開終端,使用cd指令進入需要轉(zhuǎn)化的文件目錄下,比如我要對桌面上的Test工程下的main.m文件進行轉(zhuǎn)化.終端指令類似于下圖所示.
  • 然后執(zhí)行如下的終端命令 clang -rewrite-objc main.m,如下所示.

然后在當前文件夾下就會出現(xiàn)后綴為.cpp的C++執(zhí)行文件.如下所示.


Block的實現(xiàn)

首先,我們在main函數(shù)中寫一個簡單block匿名函數(shù)并且進行調(diào)用,如下所示.

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^blk)(void) = ^{printf("Block\n");};
        blk();
    }
    return 0;
}

然后,我們通過 clang -rewrite-objc main.m指令把mian.m轉(zhuǎn)變?yōu)镃++文件.里面代碼較多,我們下拉到文件的最底部.

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("Block\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(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    return 0;
}

我們可以看到,我們寫的block已經(jīng)被轉(zhuǎn)化為一個C++語言的函數(shù),如下所示.

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");
}

概念函數(shù)的參數(shù)__cself相當于C++實例方法中指向?qū)嵗陨淼淖兞縯his,或是Objective-C實例方法中指向?qū)ο笞陨淼淖兞縮elf,也就是說參數(shù)____cself為指向Block值的變量.可是我們發(fā)現(xiàn)____cself并沒有在這里使用,這里我們先不做研究,我們先看一下參數(shù)____cself的本質(zhì).

struct __main_block_impl_0 *__cself


  • Block的結(jié)構(gòu)體

我們看到參數(shù)____cself是__main_block_impl_0 結(jié)構(gòu)體的指針,該結(jié)構(gòu)體如下所示.

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;
  }   
};

通過<<iOS 與OS X多線程和內(nèi)存管理>>我們可以了解到兩個成員變量各包含什么信息.


  • Block結(jié)構(gòu)體的成員變量

我們先看一下成員變量impl的結(jié)構(gòu)體(在.cpp文件的頂部位置).如下所示.

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;//今后版本升級所需的區(qū)域
  void *FuncPtr;//函數(shù)指針
};

第二個成員變量Desc主要是存儲今后版本升級所需的區(qū)域和Block大小.具體如下所示.

static struct __main_block_desc_0 {
  size_t reserved; //今后版本升級所需的區(qū)域
  size_t Block_size; //Block大小
}


  • Block的構(gòu)造

接下來我們就看一下__main_block_impl_0的構(gòu)造函數(shù)是如何構(gòu)造的.在main函數(shù)中調(diào)用的源碼如圖所示.

書中為了方便大家理解這句代碼調(diào)用,進行了如下的轉(zhuǎn)換.也就是說blk其實上是指向類型為__main_block_impl_0的tmp結(jié)構(gòu)體指針.

        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;

接下來我們看一下結(jié)構(gòu)體的構(gòu)造函數(shù)的參數(shù).首先是__main_block_desc_0_DATA這個參數(shù).我們在代碼中找到了它的賦值過程.如下所示.

static struct __main_block_desc_0  __main_block_desc_0_DATA = { 
                             0, 
                             sizeof(struct __main_block_impl_0)
};

通過上面的構(gòu)造函數(shù),__main_block_impl_0的值就會如下所示.

    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = 0;
    impl.Reserved = 0;
    impl.FuncPtr = ___main_block_func_0;
    Desc = &__main_block_desc_0_DATA;


  • Block的調(diào)用過程

接下來我們看一下使用block的代碼是如何實現(xiàn)的.

     blk();

找到.cpp文件對應的代碼如下所示.

    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

我們?nèi)サ艮D(zhuǎn)化部分.簡化代碼之后如下所示.這句代碼是什么意思呢?這就是使用函數(shù)的指針調(diào)用函數(shù).正如我們剛剛所示的一樣.正如上一個模塊所說的那樣,___main_block_func_0的函數(shù)指針被賦值到了結(jié)構(gòu)體的FuncPtr中了.另外___main_block_func_0的所需參數(shù)是__main_block_impl_0的類型,也就是blk.所以有以下的函數(shù)調(diào)用.

    (*blk->FuncPtr)(blk);


  • Block的實質(zhì)

這時候我們需要回過頭來說明__main_block_impl_0結(jié)構(gòu)體成員變量 impl中的isa指針.

我們知道isa指針在構(gòu)造函數(shù)中被賦值為&_NSConcreteStackBlock.如下圖所示.

其實Block就是Objective-C對象.為什么這么說呢?首先我們看一下什么叫做Objective-C對象.

在Objective-C中,任何類的定義都是對象。類和類的實例(對象)沒有任何本質(zhì)上的區(qū)別。任何對象都有isa指針。

假定我們創(chuàng)建一個如下的對象.

@interface MyObject : NSObject
{
    int val0;
    int val1;
}
@end

那么基于Objective-C對象的結(jié)構(gòu)體就應該如下所示.

struct MyObject
{
    Class isa;
    int val0;
    int val1;
}

其中的isa指針指向如下所示.具體可查看書中的98頁.


通過比較我們知道Block的結(jié)構(gòu)體中有isa指針._NSConcreteStackBlock就相當于上圖的class_t結(jié)構(gòu)體實例.也就是說Block即為Objective-C的對象.


Block截獲自動變量值的實現(xiàn)


對于Block截獲自動變量值,在<<iOS 與OS X多線程和內(nèi)存管理>>筆記:Blocks中我們已經(jīng)說過了,現(xiàn)在我們列舉一下例子.來看一下是如何實現(xiàn)截獲自動變量值這一過程的.

        int number = 1;
        
        void (^blk)(void) = ^{
            printf("value:%d\n",number);
        };
        number = 3;
        blk();

運行程序.打印結(jié)果如下所示.


通過clang -rewrite-objc main.m指令編譯成C++文件.其中核心代碼如下所示.

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;
  int number;//新增成員變量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _number, int flags=0) : number(_number) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int number = __cself->number; // bound by copy

            printf("value:%d\n",number);
}

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 number = 1;

        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, number));
        number = 3;
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    }
    return 0;
}

這時候我們把Block的結(jié)構(gòu)體拿出來看一下.我們發(fā)現(xiàn)新增了一個成員變量number以及構(gòu)造方法發(fā)生新增了對number的賦值.

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int number;//新增成員變量

  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _number, int flags=0) : number(_number) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

然后看一下main函數(shù)中__main_block_impl_0構(gòu)造函數(shù)的構(gòu)造過程.

        int number = 1;
        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, number));

這一步我們就知道在__main_block_impl_0結(jié)構(gòu)體構(gòu)造的時候已經(jīng)把number的值存儲到了自身成員變量number中了,所以后面number如何改變,那么Block在構(gòu)造完成之后打印的number值就不會發(fā)生改變了.

通過上面的表述,我們可以就了解為什么在不能Block中直接修改變量的值?(面試題).例如下圖所示.

這是為什么呢?我們看一下__main_block_func_0函數(shù)的實現(xiàn),如下所示.我們可以知道傳遞的是__main_block_impl_0結(jié)構(gòu)體的成員變量的值.而不是指針(其實就算是指針也沒有任何的關(guān)系),跟原來的number變量無任何關(guān)系.所以我們不能在函數(shù)中直接修改number變量變量.

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            int number = __cself->number; // bound by copy

            printf("value:%d\n",number);
}


__block說明符的實現(xiàn)


上面一個模塊最后我們說到如果直接在block中給變量賦值會報錯,我們發(fā)現(xiàn)根本原因就是Block結(jié)構(gòu)體中傳遞的是變量值,而不是指針,那么如何解決這一問題呢?這時候__block說明符就出現(xiàn)了.我們看一下C語言代碼,如下所示.

        __block int number = 1;
        
        void (^blk)(void) = ^{
            printf("value:%d\n",number);
            number = 6;
        };
        blk();

但是通過clang -rewrite-objc main.m指令轉(zhuǎn)變的C++代碼去發(fā)生了很大的變化.核心代碼如下所示.

//numbr變量已經(jīng)通過__block的修飾變成了結(jié)構(gòu)體
struct __Block_byref_number_0 {
  void *__isa;
__Block_byref_number_0 *__forwarding;
 int __flags;
 int __size;
 int number;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_number_0 *number; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_number_0 *_number, int flags=0) : number(_number->__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_number_0 *number = __cself->number; // bound by ref

            printf("value:%d\n",(number->__forwarding->number));
            (number->__forwarding->number) = 6;
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->number, (void*)src->number, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->number, 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; 

        __attribute__((__blocks__(byref))) __Block_byref_number_0 number = {(void*)0,(__Block_byref_number_0 *)&number, 0, sizeof(__Block_byref_number_0), 1};

        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_number_0 *)&number, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    return 0;
}

我們看一下主要改變的部分.int number = 1;變成__block int number = 1;之后,C++代碼如下所示.代碼量提升了不是一倍兩倍呀~

struct __Block_byref_number_0 {
  void *__isa;
__Block_byref_number_0 *__forwarding;//指向自身的指針
 int __flags;
 int __size;
 int number;
};

然后我們看一下在main函數(shù)中的構(gòu)造代碼.如下所示.

__attribute__((__blocks__(byref)))  __Block_byref_number_0 number = {(void*)0,(__Block_byref_number_0 *)&number, 0, sizeof(__Block_byref_number_0), 1};

簡化代碼之后,如下所示.

__Block_byref_number_0 number = {
0,
&number,
0, 
sizeof(__Block_byref_number_0), 
1
};

這時候Block結(jié)構(gòu)體的構(gòu)造函數(shù)和新增成員變量也發(fā)生了改變.成員變量變成了指向__Block_byref_number_0類型的結(jié)構(gòu)體.

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_number_0 *number; //新增成員變量

  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_number_0 *_number, int flags=0) : number(_number->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

那么在block中進行賦值的時候是如何操作的呢?這主要是通過__Block_byref_number_0的成員變量__forwarding來完成的.__forwarding是指向本身的指針.我們可以通過__forwarding來找到成員變量number的值.所以在__main_block_func_0函數(shù)實現(xiàn)中有如下的代碼.

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            __Block_byref_number_0 *number = __cself->number; // bound by ref
            printf("value:%d\n",(number->__forwarding->number));
            (number->__forwarding->number) = 6;
}

對于__Block_byref_number_0結(jié)構(gòu)體中的__forwarding指針,我們可以看下面的示意圖.


Block存儲域


通過下面一張表我們了解到Block和__block變量時存儲在棧區(qū)的結(jié)構(gòu)體類型自動變量(一般情況下).

名稱 實質(zhì)
Block 棧上Block的結(jié)構(gòu)體實例
__block 棧上__block變量的結(jié)構(gòu)體實例

接下來我們還是來研究Block結(jié)構(gòu)體的isa指針,在前面的例子中,isa指針是指向_NSConcreteStackBlock的.其實還有很多類似的類.我們先用一張表格來說明每個類的不同點

設(shè)置對象的存儲域 副本源的配置存儲域 復制效果
_NSConcreteStackBlock 從棧區(qū)復制到堆區(qū)
_NSConcreteMallocBlock 引用計數(shù)增加
_NSConcreteGlobaBlock 全局區(qū) 全局區(qū) 什么也不做

通過上面的表格,我們就可以知道兩個面試題的答案,

問: Block的類一共有幾種?
答: 三種,分別是 _NSConcreteStackBlock 、_NSConcreteMallocBlock、_NSConcreteGlobaBlock

問: Block為什么用copy修飾?
答: block在定義成屬性的時候應該使用copy修飾,平常我們使用的block主要是存放在棧區(qū)的(有的也會存放在全局區(qū)).棧區(qū)的block出了作用域之后就會被釋放掉,如果我們在block釋放掉之后還繼續(xù)調(diào)用,那么就會出現(xiàn)crash.理論上,在全局區(qū)的block我們是不需要進行copy的.但是大部分的block是存儲在棧區(qū)的,為了統(tǒng)一規(guī)范管理,所以我們都使用copy對block屬性進行修飾.


__block變量存儲域


上一個模塊是對Block進行了說明,那么對于使用__block變量的Block從棧上復制到堆上是,__block變量會有什么影響呢?

__block變量的配置存儲域 Block從棧區(qū)復制到堆時的影響
從棧復制到堆并被Block持有
被Block持有

上面這張表是表達了什么意思呢? 也就是說:

  1. 如果有一個Block使用某個__block變量,那么__block變量會從棧復制到堆并被Block持有.
  2. 如果有多個Block使用某個__block變量,那么在第一個Block中__block變量會從棧復制到堆并被第一個Block持有.從第二個Block時是持有__block變量,也就是只會增加__block變量的引用計數(shù).

對于__forwarding指針(指向自身的指針),我們曾經(jīng)說過,"不管__block變量配置在棧上還是堆上,都能正確訪問該變量."我們可以通過下面的例子來說明一下情況.

__block int val = 0;

void (^blk)(void) = [^{ ++val; } copy];

++val;

blk();

NSLog(@"%d",val);

通過blk這個Block的copy操作, 被__block修飾的val變量成功的從棧上復制到了堆上了.

所以^{ ++val; }++val;都可以被轉(zhuǎn)化為以下的形式.

++(val.__forwarding->val);

我們可以通過下面的示意圖來表示上面的轉(zhuǎn)變過程.


截獲對象的實現(xiàn)


我們曾經(jīng)說過截獲變量值,現(xiàn)在我們說一下截獲對象的實現(xiàn).演示源碼如下所示.

        void (^blk)(id obj);

        {//array的作用域
        id array = [[NSMutableArray alloc] init];
        blk = [^(id obj){
            
            [array addObject:obj];
            NSLog(@"array count = %ld",[array count]);
        } copy];
        }//array的作用域已經(jīng)結(jié)束

        blk([NSObject new]);
        blk([NSObject new]);
        blk([NSObject new]);

我們知道array的作用域已經(jīng)結(jié)束了(到達注釋位置時候),可以我們調(diào)用block仍然可以訪問到array.如下所示,這是為什么呢?

實際上在blk的實現(xiàn)過程中.已經(jīng)持有了array對象.<<iOS 與OS X多線程和內(nèi)存管理>>是有以下代碼的.

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  id  __strong array; //強引用的array成員變量
};

在Objective-C中,C語言結(jié)構(gòu)體并不能含有__strong修飾符的變量.因為編譯器不知道應該何時進行C語言結(jié)構(gòu)體的初始化和廢棄操作.不能很好的管理內(nèi)存.Objective-C的運行時庫可以很好的把握Block從棧上復制到堆以及堆上的Block被廢棄的時機.從而有效管理成員變量的持有和釋放.為此,在__main_block_desc_0就增建了兩個成員變量copy和dispose,已經(jīng)對應的函數(shù).用于成員變量的持有和釋放.如下圖所示.

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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};

可是我在實際過程中并沒有__strong修飾詞.個人猜想是已經(jīng)進行了缺省操作了.省略了__strong的修飾符.源碼截圖如下所示.大家可以自行試驗操作.


循環(huán)引用的本質(zhì)


上一個模塊我們說了.Block可以持有對象.如果一個對象中含有某個Block的成員屬性(strong修飾).在Block中直接使用self,會造成循環(huán)引用,原因就出現(xiàn)__main_block_impl_0結(jié)構(gòu)體中的obj.__main_block_impl_0對obj是強引用,self對Block變量是強引用,兩者相互引用,最終造成循環(huán)引用.

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  id  __strong obj; //強引用的obj成員變量
};

示意圖如下所示.



結(jié)束


這一篇Block的實現(xiàn)總共寫了三天,加上自己驗證,收獲良多,希望這一篇博客對大家有所幫助.還是希望大家來看一下<<iOS 與OS X多線程和內(nèi)存管理>>原書,自己敲一遍實現(xiàn)源碼,這樣幫助很大,會加深印象.最后感謝各位看官查看本篇文章.如果有任何問題,歡迎聯(lián)系騷棟.歡迎指導批斗.

<<iOS 與OS X多線程和內(nèi)存管理>>的PDF版?zhèn)魉烷T??



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

推薦閱讀更多精彩內(nèi)容