理清 Block 底层结构及其捕获行为

来自掘金 《理清 Block 底层结构及其捕获行为》

image

Block 的本质

本质

  1. Block 的本质是一个 Objective-C 对象,它内部也拥有一个 isa 指针。
  2. Block 是封装了函数及其调用环境的 Objective-C 对象

底层数据结构

一个简单示例:

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

    void (^block)(void) = ^{
        NSLog(@"hey");
    };
    block();
    return 0;
}

将以上 Objective-C 源码转换成 c++ 相关源码,使用命令行 :
xcrun -sdk iphoneos xclang -arch arm64 -rewrite-objc 文件名

c++ 的结构体与一般的类相似。

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

    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

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

    return 0;
}

其中 Block 的数据结构为:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
};

impl 变量数据结构:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr; 
};

FuncPtr:函数实际调用的地址,因为 Block 可看作是捕获自动变量的匿名函数。

Desc 变量数据结构:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}

Block 的类型

Objective-C 中 Block 有三种类型,其最终类型都是 NSBlock 。

  • NSGlobalBlock (_NSConcreteGlobalBlock)
  • NSStackBlock (_NSConcreteStackBlock)
  • NSMallocBlock (_NSConcreteMallocBlock)

Block 类型的不同,主要根据捕获变量的不同行为产生:

Block 类型 行为
NSGlobalBlock 没有访问 auto 变量
NSStackBlock 访问 auto 变量
NSMallocBlock NSStackBlock 调用 copy

在内存中的存储位置

image

内存五大区:栈、堆、静态区(BSS 段)、常量区(数据段)、代码段

copy 行为

不同类型的 Block 调用 copy 操作,也会产生不同的复制效果:

Block 类型 副本源的配置存储域 复制效果
__NSConcreteStackBlock 从栈复制到堆
__NSConcreteGlobalBlock 数据段(常量区) 什么也不做
__NSConcreteMallocBlock 引用计数增加
  • 在 ARC 环境下,编译器会在以下情况自动将栈上的 Block 复制到堆上:
  1. Block 作为函数返回值
  2. 将 Block 赋值给 __strong 指针
  3. 苹果 Cocoa、GCD 等 api 中方法参数是 block 类型

在 ARC 环境下,声明的 block 属性用 copy 或 strong 修饰的效果是一样的,但在 MRC 环境下,则用 copy 修饰。

捕获变量

为了保证在 Block 内部能够正常访问外部变量,Block 有一套变量捕获机制:

变量类型 是否捕获到 Block 内部 访问方式
局部 auto 变量 值传递
局部 static 变量 指针传递
全局变量 直接访问

若局部 static 变量是基础类型 int val ,则访问方式为 int *val
若局部 static 变量是对象类型 JAObject *obj ,则访问方式为 JAObject **obj

基础类型变量

一个简单示例:

int age = 10;
// static int age = 10;
void (^block)(void) = ^{
     NSLog(@"age is %d", age);
};
block();
  • 捕获局部 auto 基础类型变量生成的 Block 结构体 struct __main_block_impl_0 变为:
struct __main_block_impl_0 {
  ···
  int age; // 传递值
}
  • 捕获局部 static 基础类型变量生成的 Block 结构体 struct __main_block_impl_0 变为:
struct __main_block_impl_0 {
  ···
  int *age; // 传递指针
}
  • 捕获全局基础类型变量生成的结构体 struct __main_block_impl_0 没有包含 age ,因为作用域为全局,可直接访问。

对象类型变量

一个简单示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
void (^block)(void) = ^{
     NSLog(@"age is %d", person.age);
};
block();
  • 捕获局部 auto 对象类型变量生成的 Block 结构体 struct __main_block_impl_0 变为:
struct __main_block_impl_0 {
  ···
  JAPerson *person;
}
  • 捕获局部 static 对象类型变量生成的 Block 结构体 struct __main_block_impl_0 变为:
struct __main_block_impl_0 {
  ···
  JAPerson **person;
}
  • 捕获全局对象类型变量生成的结构体 struct __main_block_impl_0 没有包含 person ,因为作用域为全局,可直接访问。

copy 和 dispose 函数

当捕获的变量是对象类型或者使用 __Block 将变量包装成一个 _Block_byref变量名_0 类型的 Objective-C 对象时,会产生 copydispose 函数。

一个简单示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
void (^block)(void) = ^{
     NSLog(@"age is %d", person.age);
};
block();

其中生成的 Block 的数据结构中多了 JAPerson 类型指针变量 person :

struct __main_block_impl_0 {
  ···
  JAPerson *person;
}

Desc 变量数据结构多了内存管理相关的函数:

static struct __main_block_desc_0 {
  ···
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
}

这两个函数的调用时机:

函数 调用时机
copy 栈上的 Block 复制到堆时
dispose 堆上的 Block 被废弃时

copy 和 dispose 底层相关源码

// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// Lose the reference, and if heap based and last reference, recover the memory
BLOCK_EXPORT void _Block_release(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);


// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_dispose(const void *, const int)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

当 Block 内部访问了对象类型的 auto 变量时:

  • 如果 Block 是在栈上,将不会对 auto 变量产生强引用。
  • 如果 Block 被拷贝到堆上,会调用 Block 内部的 copy 函数,copy 函数内部会调用 _Block_object_assign 函数,_Block_object_assign 函数会根据 auto 变量的修饰符(__strong、__weak、__unsafe_unretain)作出相应的内存管理操作。

注意:若此时变量类型为对象类型,这里仅限于 ARC 时会 retain ,MRC 时不会 retain 。

  • 如果 Block 从堆上移除,会调用 Block 内部的 dispose 函数,dispose 函数内部会调用 _Block_object_dispose 函数,_Block_object_dispose 函数会自动 release 引用的 auto 变量。

使用 __weak 修饰的 OC 代码转换对应的 c++ 代码会报错:
error: cannot create __weak reference because the current deployment target does not support weak references
此时终端命令需支持 ARC 并指定 Runtime 版本:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

内存管理

修改局部 auto 变量

局部 static 变量(指针访问)、全局变量(直接访问)都可以在 Block 内部直接修改捕获的变量,而局部 auto 变量则主要通过使用 __block 存储域修饰符来修改捕获的变量。

  • __block 修饰符可以用于解决 Block 内部无法修改局部 auto 变量值的问题
  • __block 修饰符不能用于修饰全局变量、静态变量(static)

编译器会将 __block 修饰的变量包装成一个 Objective-C 对象。

一个简单示例:

__block int age = 10;
void (^block)(void) = ^{
   NSLog(@"age is %d", age);
};
block();

其中 Block 的数据结构多了一个 __Block_byref_age_0 类型的指针:

struct __main_block_impl_0 {
  ···
  __Block_byref_age_0 *age; // by ref
}

__Block_byref_age_0 结构体:

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age; // age 真正存储的地方
};

两个注意点:

    1. 此处指针 val 是指向 age 的指针,而第二个 val 指的是 age 的值。
image
    1. 源码里面通过 age->__forwarding->age 的方式去取值,是因为这两个 age 都可能仍在栈上,此时直接 age->age 访问会有问题,而 copy 操作时 __forwarding 会指向堆上的 __Block_byref_age_0 ,此时就算第一个 age 仍在栈上,通过 age->__forwarding 会重新指向堆上的 __Block_byref_age_0 ,此时再访问 age 便不会有问题 age->__forwarding->age
image
image

__block 的内存管理

使用 __block 修饰符时的内存管理情况:

  • 当 Block 存储在栈上时,并不会对 __block 变量强引用。
  • 当 Block 被 copy 到堆上时,会调用 Block 内部的 copy 函数,copy 函数会调用 __main_block_copy_0 函数对 __block 变量产生一个强引用。如下图
image
image
  • 当 Block 从堆上被移除时,会调用 Block 内部的 dispose 函数,dispose 函数会调用 _Block_object_dispose 函数自动 release __block 变量。如下图
image
image

__weak 和 __block 修饰时的引用情况

    1. 仅用 __weak 修饰

一个简单的示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
__weak typeof(person) weakPerson = person;
void (^block)(void) = ^{
    NSLog(@"person‘s age is %d", weakPerson.age);
};
image
    1. 使用 __block __weak 修饰

一个简单的示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
__block __weak typeof(person) weakPerson = person;
void (^block)(void) = ^{
     NSLog(@"person‘s age is %d", weakPerson.age);
};
block();
return 0;
image

循环引用

常见的循环引用问题:

image

ARC 环境下解决循环引用

    1. 弱引用持有:使用 __weak 或 __unsafe__unretain 解决
image
    1. 手动将一方置为 nil :使用 __block 解决,在 block 内部将一方置为 nil ,因此必须执行该 block
image

MRC 环境下解决循环引用

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

推荐阅读更多精彩内容