Block的实现原理

摘要:

我们在开发的过程中经常使用到block,不仅如此,Apple的api里面也有很多使用到block,好比gcd里面也是大量使用了block。block在语法上来说比较简洁,不过还是需要注意不要引起了循环引用。

裸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
                 //以及引用计数,下面讲到copy的时候就能知道到底是干嘛的了
  int Reserved;  //保留位
  void *FuncPtr; //函数指针
};

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以结构体的方式进行了存储,从侧面上来说,block是一个指向结构体的指针。其中__block_impl结构体记录的是block的相关信息包括block的类型以及引用计数等。

block里面捕获变量之后

我们假设在block里面捕获了一个变量,看看内部会变成什么样?

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传的是值,因此意味着在当我们在外部修改这个值的时候,你调用block打印出来的时候,这个值依旧是之前的值
    }
    return 0;
}

乍一看,好像跟刚才的大差不差,__main_block_impl_0里面产生了一个变量用来保存之前捕获的值。

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;  //什么类型的数据
__Block_byref_i_0 *__forwarding;  //这个到copy的时候就用得着了,主要是保证copy之后能够找到在堆上的那个变量
 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",(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两个函数
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; 

       //这里将结构体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的描述里面多了两个函数(关于copy和release)

Block_copy的实现

之前的热身内容是为了让大家思路更加清(meng)晰(bi)。
我们可以在编译器上使用Block_copy()或者在Block.h里面查看到关于Block_copy的定义

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

runtime.c中_Block_copy函数以这种方式实现了

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

该函数内部调用了_Block_copy_internal,同样我们在runtime.c中能够查看到该函数的实现方式

先贴上几个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实现如下

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标志位的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标识为加上BLOCK_NEEDS_FREE标识位
        //同时增加了一个引用计数
        result->flags |= BLOCK_NEEDS_FREE | 1;

       //修改isa指针
        result->isa = _NSConcreteMallocBlock;                  
        if (result->flags & BLOCK_HAS_COPY_DISPOSE) 
       {
            //如果存在copy的辅助函数,会调用该辅助函数,
            //当捕获了引用的时候,显然是满足条件的。
            (*aBlock->descriptor->copy)(result, aBlock); // do fixup
        }
        return result;
    }
    else {
        //此处省略。。。
    }
}

至此,相信都知道了copy是怎么样一个过程了。当对一个堆上的block再次进行调用copy的时候,因为我们之前给flags打入了BLOCK_NEEDS_FREE这个值,所以最后走的是这个判定条件

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

结果就是增加引用计数,然后返回该block。

辅助copy/dispose函数

1.普通变量的copy

在前面我们说到,如果有block复制到堆上的时候,有copy辅助函数的,该函数会被调用。以__block int i = 0为例子生成的辅助函数如下

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辅助函数,flags标识位能够支持的类型

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修饰的基本数据类型
    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函数的实现方式如下

void _Block_object_assign(void *destAddr, const void *object, const int flags) 
{
    ...
    此处省略部分代码
    ...
    
    //之前的生成的辅助函数,flags = 8 => BLOCK_FIELD_IS_BYREF
   else if ((flags & BLOCK_FIELD_IS_BYREF) == BLOCK_FIELD_IS_BYREF)  
    {
       //在这里调用了_Block_byref_assign_copy,看下面的那个函数
        _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) {
        
        //当初次拷贝时,flags为0,进入此分支会进行复制操作并改变flags值,置入BLOCK_NEEDS_FREE和初始的引用计数

        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
        }
        // 当发现对象存在辅助的copy函数的时候,把copy函数的赋值给在堆上的对象
        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
    //当再次拷贝对象时,则仅仅增加其引用计数
    else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    // 将原来对象的forwarding 指向现在在堆上的对象
    _Block_assign(src->forwarding, (void **)destp);
}

上面就是一个被__block修饰的基本数据类型拷贝到堆上的时候,copy函数的实际调用过程。

普通oc对象的复制

以捕获NSObject对象为栗子
辅助函数如下:

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

同样是调用_Block_object_assign这个函数,不过最终走的是这个

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

把该对象retain之后,然后再赋值,这也就是说为什么block里面的对象需要使用weak的原因。

__block修饰的oc对象的复制

还是以NSObject为栗子,辅助函数如下:

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

同样是调用_Block_object_assign这个函数,最终走的是这个

    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下并不能解决循环引用问题,但是在MRC下,此处相当于__unsafe_unretained, 在MRC下可以解决问题)
};

小结

通过上面的分析,相信大家对block有了更加清晰的理解。🐶
如果你看完以上内容觉得so easy,那么你可能是大神。
如果你看完以上内容觉得啥玩意,那么可能是我写的太渣了。
如果文章中,有错误的地方欢迎大家提出指正。

附录

本文参考了以下两篇帖子,特此奉上:
没事蹦蹦的Block实现原理
BobooO的iOS中block介绍(四)揭开神秘面纱(下)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容