Block的實現(xiàn)原理

摘要:

我們在開發(fā)的過程中經(jīng)常使用到block,不僅如此,Apple的api里面也有很多使用到block,好比gcd里面也是大量使用了block。block在語法上來說比較簡潔,不過還是需要注意不要引起了循環(huán)引用。

裸block:

我們先來看看最基本的block編譯之后長啥樣:

typedef void (^Block)();

int main(int argc, const char * argv[]) {
    
    @autoreleasepool
    {
        Block b = ^(){
            printf("Hello Gay");
        };
    }
    return 0;
}

我們使用 clang-rewrite-objc filename 指令編譯一下到底是什么鬼

struct __block_impl {
  void *isa;     //什么類型的block
  int Flags;     //block的一些附加信息杈女,里面包含了何種類型的block
                 //以及引用計數(shù),下面講到copy的時候就能知道到底是干嘛的了
  int Reserved;  //保留位
  void *FuncPtr; //函數(shù)指針
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  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;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
            printf("Hello Gay");
}

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[]) {
    { 
        __AtAutoreleasePool __autoreleasepool; 
        Block b = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    }
    return 0;
}

我們可以看到聲明的block以結(jié)構(gòu)體的方式進(jìn)行了存儲吊圾,從側(cè)面上來說达椰,block是一個指向結(jié)構(gòu)體的指針。其中__block_impl結(jié)構(gòu)體記錄的是block的相關(guān)信息包括block的類型以及引用計數(shù)等项乒。

block里面捕獲變量之后

我們假設(shè)在block里面捕獲了一個變量啰劲,看看內(nèi)部會變成什么樣?

typedef void (^Block)();

int main(int argc, const char * argv[]) {
    @autoreleasepool
    {
        int i = 0;
        Block b = ^(){
            printf("%d",i);
        };
    }
    return 0;
}

同樣clang編譯之后

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int i; //相對于未捕獲變量的block來說檀何,多了一個變量來保存值
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _i, int flags=0) : i(_i) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) 
{
   int i = __cself->i; // bound by copy
   printf("%d",i);
 }

int main(int argc, const char * argv[]) 
{
    { 
        __AtAutoreleasePool __autoreleasepool; 
        int i = 0;
        Block b = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, i));
        //注意這里的i傳的是值蝇裤,因此意味著在當(dāng)我們在外部修改這個值的時候,你調(diào)用block打印出來的時候频鉴,這個值依舊是之前的值
    }
    return 0;
}

乍一看栓辜,好像跟剛才的大差不差,__main_block_impl_0里面產(chǎn)生了一個變量用來保存之前捕獲的值垛孔。

block捕獲可修改的變量之后

貼上low B代碼

typedef void (^Block)();

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

    @autoreleasepool
    {
        __block int i = 0;
        Block b = ^(){
            printf("%d",i);
        };
    }
    return 0;
}

我們再次編譯一下??

struct __Block_byref_i_0 {
  void *__isa;  //什么類型的數(shù)據(jù)
__Block_byref_i_0 *__forwarding;  //這個到copy的時候就用得著了藕甩,主要是保證copy之后能夠找到在堆上的那個變量
 int __flags;//變量的引用計數(shù)
 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",(i->__forwarding->i));
}

//輔助block copy的時候似炎,對捕獲變量的存儲方案
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*/);}

//輔助block release的時候辛萍,對捕獲變量的釋放策略
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}

//這里面添加copy和dispose兩個函數(shù)
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[]) {

    { __AtAutoreleasePool __autoreleasepool; 

       //這里將結(jié)構(gòu)體i的__forwarding指針指向了自身
        __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};

        Block b = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));
//570425344干嘛的?后面識別block類型的時候就能用上了

    }
    return 0;
}

通過編譯我們可以看到羡藐,block的描述里面多了兩個函數(shù)(關(guān)于copy和release)

Block_copy的實現(xiàn)

之前的熱身內(nèi)容是為了讓大家思路更加清(meng)晰(bi)。
我們可以在編譯器上使用Block_copy()或者在Block.h里面查看到關(guān)于Block_copy的定義

#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))

runtime.c中_Block_copy函數(shù)以這種方式實現(xiàn)了

void *_Block_copy(const void *arg) {
    return _Block_copy_internal(arg, WANTS_ONE);
}

該函數(shù)內(nèi)部調(diào)用了_Block_copy_internal,同樣我們在runtime.c中能夠查看到該函數(shù)的實現(xiàn)方式

先貼上幾個block類型的枚舉,在這里能夠看到Block_private.h

enum{
    BLOCK_REFCOUNT_MASK =     (0xffff),
    BLOCK_NEEDS_FREE =        (1 << 24), //堆block
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25),
    BLOCK_HAS_CTOR =          (1 << 26), */* Helpers have C++ code. */*
    BLOCK_IS_GC =             (1 << 27),
    BLOCK_IS_GLOBAL =         (1 << 28),   //全局block
    BLOCK_HAS_DESCRIPTOR =    (1 << 29)
};

_Block_copy_internal實現(xiàn)如下

static void *_Block_copy_internal(const void *arg, const int flags) 
{
    struct Block_layout *aBlock;
    
    const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
 
    if (!arg) return NULL;
    
    aBlock = (struct Block_layout *)arg;
   //   還記得上面的那個初始化bloc時候賦值給flags標(biāo)志位的570425344嗎
   //   570425344 & (1 << 24) = 0 不滿足跳過
    if (aBlock->flags & BLOCK_NEEDS_FREE) { 
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
   //這邊也是不滿足條件的
    else if (aBlock->flags & BLOCK_IS_GC) {
       //此處省略若干行代碼悯许。仆嗦。。
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) 
    {
        return aBlock;
    }

    //最后來到這
    //isGC在runtime.c文件里面能夠找到先壕,該變量被初始化為false
    // Its a stack block.  Make a copy.
    // 對棧block的copy
    if (!isGC) {
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return (void *)0;
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed


        //這里是重點給flags標(biāo)識為加上BLOCK_NEEDS_FREE標(biāo)識位
        //同時增加了一個引用計數(shù)
        result->flags |= BLOCK_NEEDS_FREE | 1;

       //修改isa指針
        result->isa = _NSConcreteMallocBlock;                  
        if (result->flags & BLOCK_HAS_COPY_DISPOSE) 
       {
            //如果存在copy的輔助函數(shù)瘩扼,會調(diào)用該輔助函數(shù)谆甜,
            //當(dāng)捕獲了引用的時候,顯然是滿足條件的集绰。
            (*aBlock->descriptor->copy)(result, aBlock); // do fixup
        }
        return result;
    }
    else {
        //此處省略规辱。。栽燕。
    }
}

至此罕袋,相信都知道了copy是怎么樣一個過程了。當(dāng)對一個堆上的block再次進(jìn)行調(diào)用copy的時候碍岔,因為我們之前給flags打入了BLOCK_NEEDS_FREE這個值浴讯,所以最后走的是這個判定條件

    if (aBlock->flags & BLOCK_NEEDS_FREE) { 
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }

結(jié)果就是增加引用計數(shù),然后返回該block蔼啦。

輔助copy/dispose函數(shù)

1.普通變量的copy

在前面我們說到榆纽,如果有block復(fù)制到堆上的時候,有copy輔助函數(shù)的捏肢,該函數(shù)會被調(diào)用奈籽。以__block int i = 0為例子生成的輔助函數(shù)如下

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*/);
}

在此之前,先賦上block中copy輔助函數(shù)鸵赫,flags標(biāo)識位能夠支持的類型

enum {
    BLOCK_FIELD_IS_OBJECT   =  3,  /* id, NSObject, __attribute__((NSObject)), block, ... */
    BLOCK_FIELD_IS_BLOCK    =  7,  /* a block variable */
    BLOCK_FIELD_IS_BYREF    =  8,  // __block修飾的基本數(shù)據(jù)類型
    BLOCK_FIELD_IS_WEAK     = 16,  /* declared __weak, only used in byref copy helpers */
    BLOCK_BYREF_CALLER      = 128  /* called from __block (byref) copy/dispose support routines. */
};

runtime.c里面_Block_object_assign函數(shù)的實現(xiàn)方式如下

void _Block_object_assign(void *destAddr, const void *object, const int flags) 
{
    ...
    此處省略部分代碼
    ...
    
    //之前的生成的輔助函數(shù),flags = 8 => BLOCK_FIELD_IS_BYREF
   else if ((flags & BLOCK_FIELD_IS_BYREF) == BLOCK_FIELD_IS_BYREF)  
    {
       //在這里調(diào)用了_Block_byref_assign_copy唠摹,看下面的那個函數(shù)
        _Block_byref_assign_copy(destAddr, object, flags);
    }
    ...
    此處省略部分代碼
    ...   
}

static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {
    
    struct Block_byref **destp = (struct Block_byref **)dest;
    
    struct Block_byref *src = (struct Block_byref *)arg;
        
    if (src->forwarding->flags & BLOCK_IS_GC) {
        ;   // don't need to do any more work
    } else if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        
        //當(dāng)初次拷貝時,flags為0奉瘤,進(jìn)入此分支會進(jìn)行復(fù)制操作并改變flags值勾拉,置入BLOCK_NEEDS_FREE和初始的引用計數(shù)

        bool isWeak = ((flags & (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)) == (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK));

        struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak);

        copy->flags = src->flags | _Byref_flag_initial_value; //  _Byref_flag_initial_value

        copy->forwarding = copy; // patch heap copy to point to itself (skip write-barrier)

        src->forwarding = copy;  // patch stack to point to heap copy

        copy->size = src->size;

        if (isWeak) {
            copy->isa = &_NSConcreteWeakBlockVariable;  // mark isa field so it gets weak scanning
        }
        // 當(dāng)發(fā)現(xiàn)對象存在輔助的copy函數(shù)的時候,把copy函數(shù)的賦值給在堆上的對象
        if (src->flags & BLOCK_HAS_COPY_DISPOSE) {
            copy->byref_keep = src->byref_keep;
            copy->byref_destroy = src->byref_destroy;
            (*src->byref_keep)(copy, src);
        }
        else {
            // just bits.  Blast 'em using _Block_memmove in case they're __strong
            _Block_memmove(
                (void *)&copy->byref_keep,
                (void *)&src->byref_keep,
                src->size - sizeof(struct Block_byref_header));
        }
    }
    // already copied to heap
    //當(dāng)再次拷貝對象時盗温,則僅僅增加其引用計數(shù)
    else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    // 將原來對象的forwarding 指向現(xiàn)在在堆上的對象
    _Block_assign(src->forwarding, (void **)destp);
}

上面就是一個被__block修飾的基本數(shù)據(jù)類型拷貝到堆上的時候藕赞,copy函數(shù)的實際調(diào)用過程。

普通oc對象的復(fù)制

以捕獲NSObject對象為栗子
輔助函數(shù)如下:

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

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

同樣是調(diào)用_Block_object_assign這個函數(shù)卖局,不過最終走的是這個

if ((flags & BLOCK_FIELD_IS_OBJECT) == BLOCK_FIELD_IS_OBJECT) 
{
        _Block_retain_object(object);
        _Block_assign((void *)object, destAddr);
}

把該對象retain之后斧蜕,然后再賦值,這也就是說為什么block里面的對象需要使用weak的原因砚偶。

__block修飾的oc對象的復(fù)制

還是以NSObject為栗子批销,輔助函數(shù)如下:

//131即為BLOCK_FIELD_IS_OBJECT|BLOCK_BYREF_CALLER 

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

同樣是調(diào)用_Block_object_assign這個函數(shù),最終走的是這個

    if ((flags & BLOCK_BYREF_CALLER) == BLOCK_BYREF_CALLER) {
        if ((flags & BLOCK_FIELD_IS_WEAK) == BLOCK_FIELD_IS_WEAK) {
            _Block_assign_weak(object, destAddr);
        }
        else {
            // 最終走的是這個
            // do *not* retain or *copy* __block variables whatever they are
            _Block_assign((void *)object, destAddr);
        }
    }

門面上看起來似乎沒有retain住被__block修飾的對象染坯,實際上在這里:

struct __Block_byref_test_0 {
  void *__isa;
__Block_byref_test_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *object; (在ARC下均芽,這里retain住了對象,所以說__block在ARC下并不能解決循環(huán)引用問題单鹿,但是在MRC下掀宋,此處相當(dāng)于__unsafe_unretained, 在MRC下可以解決問題)
};

小結(jié)

通過上面的分析,相信大家對block有了更加清晰的理解。??
如果你看完以上內(nèi)容覺得so easy劲妙,那么你可能是大神湃鹊。
如果你看完以上內(nèi)容覺得啥玩意,那么可能是我寫的太渣了镣奋。
如果文章中币呵,有錯誤的地方歡迎大家提出指正。

附錄

本文參考了以下兩篇帖子侨颈,特此奉上:
沒事蹦蹦的Block實現(xiàn)原理
BobooO的iOS中block介紹(四)揭開神秘面紗(下)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末余赢,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子肛搬,更是在濱河造成了極大的恐慌没佑,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件温赔,死亡現(xiàn)場離奇詭異蛤奢,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)陶贼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門啤贩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人拜秧,你說我怎么就攤上這事痹屹。” “怎么了枉氮?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵志衍,是天一觀的道長。 經(jīng)常有香客問我聊替,道長楼肪,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任惹悄,我火速辦了婚禮春叫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘泣港。我一直安慰自己暂殖,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布当纱。 她就那樣靜靜地躺著呛每,像睡著了一般。 火紅的嫁衣襯著肌膚如雪惫东。 梳的紋絲不亂的頭發(fā)上莉给,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天毙石,我揣著相機(jī)與錄音廉沮,去河邊找鬼颓遏。 笑死,一個胖子當(dāng)著我的面吹牛滞时,可吹牛的內(nèi)容都是我干的叁幢。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼坪稽,長吁一口氣:“原來是場噩夢啊……” “哼曼玩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起窒百,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤黍判,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后篙梢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體顷帖,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年渤滞,在試婚紗的時候發(fā)現(xiàn)自己被綠了贬墩。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡妄呕,死狀恐怖陶舞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情绪励,我是刑警寧澤肿孵,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站疏魏,受9級特大地震影響停做,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蠢护,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一雅宾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧葵硕,春花似錦眉抬、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至介评,卻和暖如春库北,著一層夾襖步出監(jiān)牢的瞬間爬舰,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工寒瓦, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留情屹,地道東北人。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓杂腰,卻偏偏與公主長得像垃你,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子喂很,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

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