Block复习

1. Block的基本结构

void (^testBlock)(void) = ^{
    NSLog(@"臭吉吉~");
};
testBlock();

将包含Block的代码通过clang转换为c++代码(只用了c++的扩展struct,实际上还是c)。我们一句一句看:

  1. Block变量的声明:
void (*testBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

可以看到,testBlock变量,实际上是 __main_block_impl_0 结构体实例的指针

__main_block_impl_0的结构为:

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

其中,用以识别Block对象的类型信息和Block的函数体都在 __block_impl 结构体中声明:

struct __block_impl {
    // 类型
    void *isa;
    // 引用计数等会存在这里
    int Flags;
    // 保留位
    int Reserved;
    // 函数指针
    void *FuncPtr;
};

其中,FuncPtr指向的就是我们在Block中提供的函数体。而isa,即作为描述Block类型使用。由于Block在堆中也是遵循类似自动引用计数的内存管理机制,故可以把Block看做为对象。

而Block的描述信息,则是指向全局的 __main_block_desc_0 结构体的实例。

static struct __main_block_desc_0 {
    // 保留位
    size_t reserved;
    // Block整体的内存占用
    size_t Block_size;
} __main_block_desc_0_DATA = { 
    0, 
    sizeof(struct __main_block_impl_0)
};
  1. Block的执行
((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);

了解了Block的结构,这一句就很好理解了。由于testBlock的地址与 __block_impl 指针的地址相同,因此直接转换为 __block_impl 类型。然后,获取其中的 FuncPtr 函数指针,传入自身作为参数后,直接调用执行。

传入自身作为 FuncPtr 的参数的目的

由于Block的函数体在编译后成为了全局静态c函数(无状态保存)。因此,为了在调用时可以正常访问到捕获的变量,则将自身实例作为参数传入(这与OC调用方法的传参目的一样)。

2.Block捕获的变量

2.1 没有捕获变量

Block在没有捕获任何变量时,其类型(isa)为NSGlobalBlock

2.2 捕获基本类型变量

测试代码:

NSInteger value = 3;
void (^testBlock)(void) = ^{
    NSLog(@"%ld", value);
};
testBlock();

在运行时,此Block的类型为NSMallocBlock。已经被copy到堆中。

对于基本数据类型的变量,捕获后,其值直接保存到 __main_block_impl_0 结构体中:

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    
    // 直接保存值
    NSInteger value;
    
    
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger _value, int flags=0) : value(_value) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
  }
};

由于是值传递,直接修改此Block变量中的value是不会影响原value的值。因此,编译器则直接不允许修改捕获的变量。

而且,这也解释了为何在 FuncPtr 中的需要传入block自身作为参数:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    // 通过自身取出捕获的变量
    NSInteger value = __cself->value;

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_zmhjsn0s5szgbxdxfqvbgqlc0000gn_T_main_cf26fa_mi_0, value);
        }
2.3 捕获对象类型变量

测试代码:

id obj = [[NSObject alloc] init];
void (^testBlock)(void) = ^{
    NSLog(@"%@", obj);
};
testBlock();

在运行时,此Block的类型为NSMallocBlock。已经被copy到堆中。

由于捕获的是对象类型,因此编译后的c++代码与刚才有些不同:

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    
    // 直接保存对象(也就是地址)
    id obj;
    

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

核心结构还是一样,直接将捕获对象保存到了 __main_block_impl_0 结构体中。产生变化的,是 __main_block_desc_0 的结构:

static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
    
    // Block被copy时,捕获的变量执行的copy函数
    void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
    
    // Block释放时,捕获的变量执行的释放函数
    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
};

由于捕获的变量是对象类型,因此,需要在结构体中指定实现内存管理方式的相应实现(clang可以在Block的相关结构体中对OC对象进行内存管理,但需要提供相应实现)。

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变量被copy到堆上时,系统则会调用 _Block_object_assign 函数,对捕获的obj进行retain;而当堆上的Block变量被释放时,系统则会调用 _Block_object_dispose 函数,对捕获的obj进行release操作。

为了行为一致,编译器也不允许对捕获的对象类型变量进行修改。

这可以保证捕获的对象在超出自身作用域后,继续生存(因为已经被堆上的Block保留)。

2.4 捕获__block修饰的基本类型变量

测试代码:

__block NSInteger value = 3;
void (^testBlock)(void) = ^{
    value -= 1;
};
testBlock();
NSLog(@"%ld", value);

首先,还是可以确认的是,在运行时,Block的类型是NSMallocBlock

转换代码后,就可以看到,使用了 __block 修饰符的实现就变了很多。我们还是一句一句来看:

__attribute__((__blocks__(byref))) __Block_byref_value_0 value = {
    (void*)0,
    (__Block_byref_value_0 *)&value, 
    0, 
    sizeof(__Block_byref_value_0), 
    3
};

可以看到,__block 修饰的变量,实际上是一个全局的 __Block_byref_value_0 结构体的实例。我们看一下此结构体的内容:

struct __Block_byref_value_0 {
    // 类型标识
    void *__isa;
    // 指向自身实例的指针
    __Block_byref_value_0 *__forwarding;
    
    int __flags;
    int __size;
    
    // 真正的值
    NSInteger value;
};

可以看到,原始变量的真实值保存在结构体中。此结构体中不仅包含了类型标识、尺寸等信息,还包含了一个指向自身实例的指针。

下面是Block变量声明,只是将 __Block_byref_value_0 的地址传入,没有什么异常:

// testBlock变量声明及赋值
void (*testBlock)(void) = ((void (*)())&__main_block_impl_0(
    (void *)__main_block_func_0, 
    &__main_block_desc_0_DATA, 
    (__Block_byref_value_0 *)&value, 
    570425344)
);


// __main_block_impl_0的结构体
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    
    // 引用传递捕获的变量
    __Block_byref_value_0 *value;
    
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_value_0 *_value, int flags=0) : value(_value->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
  }
};

可以看到,唯一的区别就是,在捕获的带有 __block 修饰的变量,生成的Block变量中,是以引用传递的方式进行储存的。这也就意味着捕获的变量的内容是可以随意修改的,而且,访问或者修改的是 __Block_byref_value_0 的实例,而不是原始的变量

对于Block中的描述信息,其实现也有些许变化:

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

可以看到,使用 __block 修饰的变量,在捕获到Block中后,也需要在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->value, (void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);
}

这里与捕获对象类型变量时,生成的内存管理函数中,区别只是类型不同,是 BLOCK_FIELD_IS_BYREF (捕获的对象类型变量是 BLOCK_FIELD_IS_OBJECT

与对象的保留关系不同,这种方式,实际上是创建一个新对象(结构体实例,如 __Block_byref_value_0 ,内部包含着被捕获的变量的值)直接存储在Block中。当Block被copy到堆上时,再创建一个新的 __Block_byref_value_0 实例,并保存在堆上的Block中

__Block_byref_value_0 的结构中,为什么会包含一个指向自身实例的指针 __forwarding

为了保证访问到捕获变量的一致性。
在Block被copy到堆上时,不仅生成一个新的 __Block_byref_value_0 实例。而且将原始 __Block_byref_value_0__forwarding 指针指向了新的实例。因此,通过形如 value.__forwarding->value 的方式,不管是在栈上,还是在堆上,都可以访问到堆中的同一个变量。

所以,我们最后看一下在Block执行之后,打印语句NSLog。

NSLog(
    (NSString *)&__NSConstantStringImpl__var_folders_z5_zmhjsn0s5szgbxdxfqvbgqlc0000gn_T_main_316d85_mi_0, 
    (value.__forwarding->value)
);

由于是在栈上执行,因此 value.__forwarding->value 最终指向的是堆上的Block中的新 __Block_byref_value_0 实例。

2.5 捕获__block修饰的对象类型变量

测试代码:

__block id obj = [[NSObject alloc] init];
void (^testBlock)(void) = ^{
    obj = [[NSMutableArray alloc] init];
};
testBlock();
NSLog(@"%@", obj);

转换后的代码与 __block 修饰的基本类型变量很相似,都是生成一个对应的结构体实例,然后将变量存储在内部。

我们看一下生成过程(代码经过简化):

__attribute__((__blocks__(byref))) __Block_byref_obj_0 obj = {
    (void*)0,
    (__Block_byref_obj_0 *)&obj, 
    33554432, 
    sizeof(__Block_byref_obj_0), 
    __Block_byref_id_object_copy_131, 
    __Block_byref_id_object_dispose_131, 
    [[NSObject alloc] init]
};

其中,__Block_byref_obj_0 的结构如下所示:

struct __Block_byref_obj_0 {
    void *__isa;
    // 指向自身实例的指针
    __Block_byref_obj_0 *__forwarding;
    int __flags;
    int __size;
    
    // obj
    void (*__Block_byref_id_object_copy)(void*, void*);
    
    // obj释放函数
    void (*__Block_byref_id_object_dispose)(void*);
    
    // 真正的对象
    id obj;
};

可以看到,__block 修饰的对象类型结构体,不仅包含与基本类型一样的成员,额外还包含了两个内存管理函数,用于在自身实例因Block的内存变化导致的变化时,包含的obj进行的保留和释放操作(Block的内存管理 -> __Block_byref_obj_0的内存变化 -> 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);
}

可以看到,以copy方法为例,实际上与描述信息 __main_block_desc_0_DATA 中的 __main_block_copy_0 函数实现一样,都是调用了 _Block_object_assign 函数。只不过参数有些许不同:

src+40偏移量即为 __Block_byref_obj_0 结构体中的obj的地址。131即 BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT

_Block_object_assign 实现为例(节选自苹果的Blocks源代码 Blocks/Sources/runtime.c):

void _Block_object_assign(void *destAddr, const void *object, const int flags) {
    ...
    case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        /*******
         // copy the actual field held in the __block container
         __block id object;
         __block void (^object)(void);
         [^{ object; } copy];
         ********/

        // under manual retain release __block object/block variables are dangling
        _Block_assign((void *)object, destAddr);
        break;
        
        ...
    }
}

static void (*_Block_assign)(void *value, void **destptr) = _Block_assign_default;

static void _Block_assign_default(void *value, void **destptr) {
    *destptr = value;
}

可以看到,在这种情况下,copy操作只是使用一个新的指针指向原始obj。

在ARC下,实际上就是对obj进行了强引用,也就是retain操作;但是在非ARC下,这只是一个指针指向,可能造成悬垂指针访问,切记。

而在 __main_block_desc_0_DATA 中,使用的copy和dispose函数与 __block 修饰的基本类型变量一致:

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

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

最后,再看一下我们在Block函数体中对捕获变量的修改(代码已简化):

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    // 通过引用访问
    __Block_byref_obj_0 *obj = __cself->obj; 
    
    // 通过__forwarding指针访问到的永远是相同的obj
    (obj->__forwarding->obj) = [[NSMutableArray alloc] init];
}

3. 总结

  1. 在ARC环境下,Block在不捕获变量时,是NSGlobalBlock类型;否则,都是NSMallocBlock类型。
  2. 捕获到的基本数据类型变量或OC对象,直接存储值到Block的数据结构中,为值传递,外部修改无效。
  3. 捕获到的__block修饰的基本类型变量或OC对象,是以包装成的新的结构体实例的方式存储到Block的数据结构中,为引用传递,可以进行修改。
  4. ARC环境下,Block从栈上到被copy到堆上时,捕获的OC对象或是__block的OC对象,都会被retain;捕获的__block的基本类型变量,会创建一个新的结构体,保存在copy后的Block中。
  5. 非ARC环境下,使用__block修饰的OC对象,在被Block捕获后,可以防止循环引用(只是指针指向,没有retain操作,ARC下才是默认retain)。在ARC下,使用__weak修饰变量替代。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容