OC底层原理(八):Block

block是经常使用的一种技术,那么block的本质是什么呢?

Block的本质

block本质上也是OC对象,它内部也有isa指针
block是封装了函数调用已经函数调用环境的OC对象

我们创建一个命令行项目,在main函数中创建一个block

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

使用命令将main.m转换成C/C++代码

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

在main.mm中

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

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

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}

__main_block_impl_0这个便是block的底层结构,可以看到它内部确实有isa指针,所以它是一个OC对象

block的变量捕获

我们更新main函数中的代码

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

很明显,运行后会打印10


截屏2021-01-22 21.16.11.png

那么为什么会打印10呢?
我们把main函数转换成c++代码来看看

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

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

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}

可以看到__main_block_impl_0内部多了一个参数int a

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int a = 10;
        void (*blockMe)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        a = 20;
        ((void (*)(__block_impl *))((__block_impl *)blockMe)->FuncPtr)((__block_impl *)blockMe);
    }
    return 0;
}

删掉一些强制转换,精简下代码如下

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int a = 10;
        void (*blockMe)(void) = &__main_block_impl_0(__main_block_func_0,
                                                      &__main_block_desc_0_DATA,
                                                      a);
        a = 20;
        blockMe->FuncPtr(blockMe);
    }
    return 0;
}

可以看到在初始化block的时候,把a(值为10)传入到了block的构造函数里,并把值保存在block内部的int a变量里

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_0w_qpvfdl7j7c3brrgyj3tbv3xm0000gn_T_main_d13ce6_mi_0, a);
        }

在调用block的时候,是从block内部的int a获取值,然后打印,所以不管外部a变成多少,都只会打印10

block对变量的捕获

截屏2021-01-25 20.51.22.png

auto变量就是我们平时写的局部变量,编译器会自动帮我们在前面加上auto修饰符

int a = 10;
//等价于
auto int a = 10;

我们来验证下这三个变量的捕获情况,更新main函数代码如下

int c = 30;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        static int b = 20;
        void (^blockMe)(void) = ^ {
            NSLog(@"%d", a);
            NSLog(@"%d", b);
            NSLog(@"%d", c);
        };
        blockMe();
    }
    return 0;
}

将main.m文件转换成c++代码,在main.cpp文件里block的底层实现如下

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
  int *b = __cself->b; // bound by copy

  NSLog((NSString *)&__NSConstantStringImpl__var_folders_0w_qpvfdl7j7c3brrgyj3tbv3xm0000gn_T_main_7c0631_mi_0, a);
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_0w_qpvfdl7j7c3brrgyj3tbv3xm0000gn_T_main_7c0631_mi_1, (*b));
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_0w_qpvfdl7j7c3brrgyj3tbv3xm0000gn_T_main_7c0631_mi_2, c);
}

可以看到block内部存了a的值,b的地址,c没有捕获,在调用打印函数的时候,a是直接从内部访问值,b则是获取地址访问,c是直接访问
block捕获self
我们新建一个ZJPerson类

@interface ZJPerson : NSObject
@property (nonatomic, copy) void (^blockMe)(void);
- (void)test;
@end

@implementation ZJPerson

-(void)dealloc {
    NSLog(@"123");
}

- (void)test {
    self.blockMe = ^{
        NSLog(@"%@", self);
    };
    self.blockMe();
}

@end

这个类在使用完销毁的时候会打印123,我们在main函数中使用ZJPerson类

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ZJPerson *person = [[ZJPerson alloc]init];
        [person test];
    }
    return 0;
}

按理说,在main函数完成的时候,person会被释放掉,从而打印123日志,但是我们运行项目之后却没有输出


截屏2021-01-25 21.26.15.png

这是什么原因导致的person对象没有释放呢?我们把ZJPerson.m转换成cpp看下

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ZJPerson.m

在ZJPerson.cpp中搜索__ZJPerson__test_block_impl_0

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

可以看见block将self捕获到了内部去,为什么会捕获self呢?我们来看看test的底层实现

static void _I_ZJPerson_test(ZJPerson * self, SEL _cmd) {
    ((void (*)(id, SEL, void (^ _Nonnull)()))(void *)objc_msgSend)((id)self, sel_registerName("setBlockMe:"), ((void (*)())&__ZJPerson__test_block_impl_0((void *)__ZJPerson__test_block_func_0, &__ZJPerson__test_block_desc_0_DATA, self, 570425344)));
    ((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)self, sel_registerName("blockMe"))();
}

在test函数中,self被当作局部变量参数传了进去,依据前面的规则,局部变量会被block捕获进内部,所以self就被block捕获了。
这就造成了self持有block,block持有self的循环引用,从而导致person对象没有释放


block的类型

截屏2021-01-26 21.02.09.png

在我们用代码验证这个类型之前,我们需要把项目的环境从ARC调整为MRC


截屏2021-01-26 21.05.55.png

然后在main函数中,申明三个block对象,block1没有访问auto变量,block2访问了auto变量,block3为block2进行了copy操作

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block1)(void) = ^{
            NSLog(@"123");
        };
        NSLog(@"block1 is %@", [block1 class]);
        
        int a = 10;
        void(^block2)(void) = ^{
            NSLog(@"%d", a);
        };
        NSLog(@"block2 is %@", [block2 class]);
        
        void(^block3)(void) = [block2 copy];
        NSLog(@"block3 is %@", [block3 class]);
        
    }
    return 0;
}

运行之后结果如下


截屏2021-01-26 21.07.42.png

block对对象类型的auto变量的捕获

  1. 如果block在栈上,则不会对auto变量进行强引用
    我们在MRC环境下,更新main函数和ZJPerson类的代码如下
@interface ZJPerson : NSObject
@property (nonatomic, assign) int age;
@end

@implementation ZJPerson
-(void)dealloc {
    [super dealloc];
    NSLog(@"ZJPerson dealloc");
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^stackBlock)(void);
        {
            ZJPerson *person = [[ZJPerson alloc]init];
            person.age = 10;
            stackBlock = ^{
                NSLog(@"----%d", person.age);
            };
            [person release];
        }
        NSLog(@"%@", [stackBlock class]);
        NSLog(@"1111111");
    }
    return 0;
}

并且在NSLog(@"1111111");处打上断点
按照之前捕获auto变量的逻辑来分析,在打断点的地方,person对象并不会dealloc,我们运行看看情况

截屏2021-01-27 21.32.59.png

截屏2021-01-27 21.33.07.png

可以看到person对象已经释放了,说明栈上的block不会对auto变量产生强引用

  1. block被copy到了堆上
    • 会自动调用block内部的copy函数
    • copy函数内部会调用_Block_object_assign函数
    • _Block_object_assign会根据auto变量的修饰符(__strong, __weak, __unsafe_unretained)来决定是否强引用变量,类似于retain
  2. block被移除堆
    • 会调用block内部的dispose函数
    • dispose函数内部会调用_Block_object_dispose函数
    • _Block_object_dispose会根据auto变量的修饰符(__strong, __weak, __unsafe_unretained)来决定是否释放变量,类似于release

block内部访问了auto变量之后的底层代码

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

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

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

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

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

__block修饰符

我们知道在block内部中是不可以修改外部auto变量的值的,如果想要改值的话需要给外部变量加上__block修饰符

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

为什么不加__block就不能改变值呢?
首先age申明在main函数里是一个局部变量
当执行block里的代码时

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_0w_qpvfdl7j7c3brrgyj3tbv3xm0000gn_T_main_7cf10c_mi_0, age);
}

无法访问外部main函数里的age值,它只能改变捕获到内部的age的值,所以在block内部无法修改外部的auto变量

为什么加上__block就可以修改值了呢?我们把它转换成C++代码来看下

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

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_age_0 *age = __cself->age; // bound by ref
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_0w_qpvfdl7j7c3brrgyj3tbv3xm0000gn_T_main_110fcc_mi_0, (age->__forwarding->age));
        }

可以看到,block的底层结构发生了变化,原本应该为int age的成员变量变成了__Block_byref_age_0 *age,而结构体__Block_byref_age_0内部才有int age成员变量

__block将修饰的auto变量包装成一个对象,比如
int age包装成__Block_byref_age_0对象,
__Block_byref_age_0内部持有int age
block对象持有__Block_byref_age_0对象,
当执行block的代码的时候,先获取到__Block_byref_age_0对象,然后再从__Block_byref_age_0对象内部获取到int age修改它的值
这样就解决了block内部访问不到外部auto变量的问题,从而达成可以修改外部变量的效果

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int age = 10;
        void(^blockMe)(void) = ^{
            NSLog(@"%d", age);
        };
        //那这个age,访问的是哪个age呢?
        //是__main_block_impl_0内部的__Block_byref_age_0 *age呢
        //还是__Block_byref_age_0内部的int age
        NSLog(@"%p", &age);
        blockMe();
    }
    return 0;
}

为了解决这个问题,我们将blockMe用底层结构体的方式来实现一下,更新main函数的代码如下

struct __Block_byref_age_0 {
  void *__isa;
 struct __Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(void);
  void (*dispose)(void);
};

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

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  struct __Block_byref_age_0 *age; // by ref
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int age = 10;
        void(^blockMe)(void) = ^{
            NSLog(@"%d", age);
        };
        //那这个age,访问的是哪个age呢?
        //是__main_block_impl_0内部的__Block_byref_age_0 *age呢
        //还是__Block_byref_age_0内部的int age
        struct __main_block_impl_0 *blockImpl = (struct __main_block_impl_0 *)blockMe;
        NSLog(@"%p", &age);
        blockMe();
    }
    return 0;
}

然后再blockMe();处打上断点

截屏2021-02-02 20.47.27.png

然后运行代码,控制台打印如下
截屏2021-02-02 20.51.56.png

可以看到,我们在外部访问的age的地址为0x00007ffeefbff558, 我们在控制台输入如下命令
p/x &(blockImpl->age->age)
这个命令是查看blockMe的底层结构__main_block_impl_0内部的__Block_byref_age_0内部的int age的地址,其输出如下
截屏2021-02-02 20.55.52.png

由此可以得出结论
访问的是是__Block_byref_age_0内部的int age


__block中的__forwarding指针

我们可以看到用__block修饰的int age,其底层结构如下

struct __Block_byref_age_0 {
  void *__isa;
 struct __Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

这个__forwarding指针是啥呢?
这个指针其实是指向它自己的


截屏2021-02-02 21.28.08.png

我们在main函数的底层代码中找到这一段代码

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
        void(*blockMe)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_0w_qpvfdl7j7c3brrgyj3tbv3xm0000gn_T_main_110fcc_mi_1, &(age.__forwarding->age));
        ((void (*)(__block_impl *))((__block_impl *)blockMe)->FuncPtr)((__block_impl *)blockMe);
    }
    return 0;
}

精简一下,删除冗余的代码,如下

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        __Block_byref_age_0 age = {0,
                                   (__Block_byref_age_0 *)&age, 
                                   0,
                                   sizeof(__Block_byref_age_0),
                                   10};
        ……
    }
    return 0;
}

可以看到第二个参数传入的就是自己的地址
那么这个__forwarding指针有什么用呢?
当block在栈区的时候,栈区block的__forwarding指针指向栈区的block,当栈区的block拷贝到堆上的时候,栈区的__forwarding指针指向堆区的block,如下所示

截屏2021-03-02 20.10.26.png

这样就可以保证捕获的变量都是保存在堆区

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

推荐阅读更多精彩内容