Block本质解密---变量捕获机制

在做项目的时候常用到block, 最近看了一些资料, 对block的有了更深入的理解, 下面记录下。

一、Block底层结构

先看一个简单的block

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        void (^block_test)(void) = ^{
            
            NSLog(@"Hello, World!");
        };
        
        block_test();
    }
    return 0;
}

下面把OC代码转成C++代码, 打开终端执行命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp, 打开main-arm64.cpp文件

先精简一下, 删减部分强制转换类型代码

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ 
    { __AtAutoreleasePool __autoreleasepool; 

        // // 定义blockTest
        void (*blockTest)(void) = &__main_block_impl_0(
                                                       __main_block_func_0, 
                                                       &__main_block_desc_0_DATA
                                                       );
        // 调用blockTest
        blockTest->FuncPtr(blockTest);
    }
    return 0;
}

先看下blockTest是怎么实现的,

找到__main_block_impl_0

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // 构造函数, 类似于OC的init方法, 返回结构体对象
  __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;
  }
};

结构体中有个__main_block_impl_0()构造函数, 类似于OC的init方法, 从构造函数的实现代码可以看出是把 传入的参数赋值给implDesc

看下__block_impl

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

isa指针, 说明 block是一个对象
FuncPtr函数指针 指向block要执行的内容

看下构造函数__main_block_impl_0()传入的参数
1.__main_block_func_0


static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
     // blockTest 执行的内容
     NSLog((NSString *)&__NSConstantStringImpl__var_folders_6l_rp70pg912_z5my021550_xx00000gn_T_main_6c65c0_mi_0);
 }

通过NSLog可以看出它是一个内部封装了 block执行逻辑 的函数。

2.&__main_block_desc_0_DATA

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

__main_block_desc_0_DATA是一个__main_block_desc_0结构体变量, 它有两个参数

  • reserved: 0
  • Block_size: sizeof(struct __main_block_impl_0) block占用内存空间大小

阶段总结:

    1. block本质上也是一个OC对象, 它内部也有个isa指针
    1. block是封装了函数调用以及函数调用环境的OC对象

Block底层结构可以用一张图展示

二、Block变量捕获

为了保证block内部能够正常访问外部的变量, block有个变量捕获机制。
1. auto变量(自由变量、局部变量)
局部变量int age=10;默认是带auto的, 就是auto int age=10;
先看下面代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        
        void (^blockTest)(void) = ^{
            
            NSLog(@"age is %d", age);
        };
        age = 20;
        blockTest();
    }
    return 0;
}

运行看下, 打印结果 age is 10
在调用blockTest()之前已经把age修改成20, 但为什么会打印结果是10呢?

用clang命令, 看下c++代码

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

        int age = 10;
        void (*blockTest)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age);

        age = 20;
        blockTest->FuncPtr(blockTest);
    }
    return 0;
}

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_6l_rp70pg912_z5my021550_xx00000gn_T_main_1c5279_mi_0, age);
}

发现block里也有个变量int age , 也就是说变量age被block捕获, 而且是值传递

2. static局部变量

int main(int argc, const char * argv[]) {
    @autoreleasepool { 
        int age = 10;
        static int hight = 10;
        
        void (^blockTest)(void) = ^{
            
            NSLog(@"age is %d, hight is %d", age, hight);
        };
        
        age = 20;
        hight = 20;
        
        blockTest();
    }
    return 0;  
}

打印结果: age is 10, hight is 20

看下c++代码

// main函数中, 定义block
void (*blockTest)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age, &hight);

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  int *hight;
}

可以看到block里有个指针int *hight, 也就是说static变量hight也被block捕获了, 但是是指针传递, static修饰的变量,在内存中只有一份, 所以打印的是hight修改过的值。
全局变量

int age = 10;
static int hight = 10;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^blockTest)(void) = ^{

            NSLog(@"age is %d, hight is %d", age, hight);
        };
        age = 20;
        hight = 20;

        blockTest();
    }
    return 0;
}

打印结果: age is 20, hight is 20

C++代码
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没有捕获, 可以直接访问。

阶段总结:

    1. 局部变量因为跨函数访问, 需要被block捕获
    1. 自由(局部)变量是值传递, static局部变量是指针传递
    1. 全局变量在每个函数都能访问, 不需要捕获

思考下在block中访问, 成员变量_weight, 会不会捕获 ?
直接用代码验证

@interface Person : NSObject
//@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger weight;
- (void)test;
@end

@implementation Person
- (void)test
{
    void (^blockTest)(void) = ^{
//        NSLog(@"name is %@", _name);
        NSLog(@"weight id %ld", _weight);
    };
    
    blockTest();
}
@end

下面看下C++代码

static void _I_Person_test(Person * self, SEL _cmd) {
    void (*blockTest)(void) = &__Person__test_block_impl_0(__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344);
    
    blockTest->FuncPtr(blockTest);
}

struct __Person__test_block_impl_0 {
  struct __block_impl impl;
  struct __Person__test_block_desc_0* Desc;
  Person *self;
  __Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

下面是block执行内容 --- NSLog
1. 只访问_weight时
static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) {
  Person *self = __cself->self; // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_6l_rp70pg912_z5my021550_xx00000gn_T_Person_4ac00c_mi_0, (*(NSInteger *)((char *)self + OBJC_IVAR_$_Person$_weight)));
}

2. 只访问_name时
static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) {
  Person *self = __cself->self; // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_6l_rp70pg912_z5my021550_xx00000gn_T_Person_a282fe_mi_0, (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_name)));
}

可以看出访问_name、_weight时, block定义时都只传入了参数self570425344, block结构体中有个Person *self, 没有_name和_weight, 所以
在block中使用成员变量, block会捕获self

**解释:**
从函数static void _I_Person_test(Person * self, SEL _cmd)可以知道-(void)test方法默认有两个参数Person * self, SEL _cmd, 所以 self 在test方法中是一个局部变量, 再看下blockTest定义 &__Person__test_block_impl_0(__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344);
传入的第三个参数是就是 局部变量self, 上面已经验证了局部变量因为跨函数访问, 会被block捕获

三、Block类型

block有三种类型, 可以通过class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型

  1. NSGlobalBlock --- block没有访问auto变量
void (^block1)(void) = ^{
            
      NSLog(@"block1");
 };
NSLog(@"block1 %@", [block1 class]) ;

打印结果: block1 __NSGlobalBlock__

没有访问auto变量, 并且有指针强引用, 储存在全局区

  1. NSStackBlock --- block访问auto变量
NSLog(@"block3 %@", [^{
     NSLog(@"block3 age = %d", age);
 } class]);

打印结果: block3 __NSStackBlock__

访问了auto变量,没有指针强引用,直接调用block块, 存放在栈区

  1. NSMallocBlock ---对NSStackBlock执行copy操作
int age = 10;
void (^block3)(void) = ^{
            
      NSLog(@"block3 age = %d", age);
 };
NSLog(@"block3 %@", [block2 class]) ;

打印结果: block3 __NSMallocBlock__

访问了auto变量, 并且有指针强引用, 储存在

到这里可能会有疑问, block访问auto变量应该是NSStackBlock类型的, 存放到栈区才对吧?
理论上的确应该是如此。但上面代码是在ARC下运行的结果, 编译器会根据具体情况将 栈上的block拷贝到堆上

切换到MRC下运行试试

打印结果: block3 __NSStackBlock__

可以看到在MRC环境下是NSStackBlock类型的
下面手动执行下copy操作

int age = 10;
void (^block3)(void) = [^{
            
       NSLog(@"block3 age = %d", age);
 } copy];
NSLog(@"block3 %@", [block3 class]) ;

打印结果: block3 __NSMallocBlock__

阶段总结:

    1. 没有访问auto变量, block是NSGlobalBlock类型
    1. 访问了auto变量, block是NSStackBlock类型
    1. NSStackBlock类型的block调用了copy, block是NSMallocBlock类型
    1. 在ARC环境下,编译器会根据具体情况将栈上的block拷贝到堆上
      • a. block作为函数返回值时
      • b. 将block赋值给__strong指针时
      • c. block作为Cocoa API中方法名含有usingBlock的方法参数时
      • d. block作为GCD API的方法参数时

所以在MRC下block属性建议写法
@property (nonatomic, copy) void (^block)(void);
在ARC下block属性建议写法, 两者均可
@property (nonatomic, copy) void (^block)(void);
@property (nonatomic, strong) void (^block)(void);

四、Block访问对象类型的auto变量

Person *person = [[Person alloc] init];
person.weight = 80;
void (^blockTest)(void) = ^{
        NSLog(@"weight id %ld", person.weight);
};

看C++代码
找到__main_block_impl_0

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__strong person;
}

person对象被block捕获, 但是带着关键字__strong

把person用关键字__weak修饰试下

__weak Person *weak = person;;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__weak weak;
}

发现捕获的person也是带着关键字__weak

再看下__Person__test_block_desc_0

static struct __Person__test_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __Person__test_block_impl_0*, struct __Person__test_block_impl_0*);
  void (*dispose)(struct __Person__test_block_impl_0*);
} __Person__test_block_desc_0_DATA = { 0, sizeof(struct __Person__test_block_impl_0), __Person__test_block_copy_0, __Person__test_block_dispose_0};

发现多了两个函数指针void *copyvoid *dispose

static void __Person__test_block_copy_0(struct __Person__test_block_impl_0*dst, struct __Person__test_block_impl_0*src) 
{
   _Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __Person__test_block_dispose_0(struct __Person__test_block_impl_0*src) 
{
   _Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

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

  • 如果block被拷贝到堆上
    * 会调用block内部的copy函数
    * copy函数内部会调用_Block_object_assign函数,根据
    auto变量的修饰符(__strong,__weak), 形成强引用(retain)弱引用
  • 如果block是从堆上移除
    * 会调用block内部的dispose函数
    * dispose函数内部会调用_Block_object_dispose函数,自动释放
    (release)引用的auto变量
  • 如果block是在栈上,不会对auto变量产生强引用

五、总结:

    1. block访问全局变量,不会捕获变量, 直接访问
      (数据类型int或NSInteger、自定义对象类型如Person、系统对象类型如NSString)
    1. block访问auto变量, 会捕获变量, 是值传递
      • 对象类型的auto变量, 会调用copy函数强引用或弱引用变量,调用dispose函数释放引用的变量
        (自定义对象类型如Person、系统对象类型如NSString)
      • 数据类型auto变量, 不会调用copy、dispose函数
    1. block访问static局部变量, 会捕获变量, 是指针传递
      • 数据类型static局部变量, 不会调用copy、dispose函数
        (数据类型int或NSInteger)
      • 对象类型的static局部变量, 会调用copy、dispose函数
        (自定义对象类型如Person、系统对象类型如NSString)

以上就是关于Block底层结构、变量捕获机制的理解, 后续有新的会补充进来。

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

推荐阅读更多精彩内容