Block使用及原理探究


出于需要,本文分析时引用苹果开源的libclosure-74 源码,请点击对比查看苹果libclosure源码

一、Block概念

1.1 名词解释

也成为匿名函数,是将函数及其执行上下文封装起来的对象。有参数和返回值。

Block的结构是:__block_impl

struct __block_impl {
    void *isa;        // isa 对象
    int Flags;
    int Reserved;
    void *FuncPtr;//函数指针
}

1.2 分类

根据Block 所属的libclosure 所示,一共有6种。具体操作:进入苹果libclosure源码,选择最新的libclosure-74,下载后打开工程文件,搜索NSBlock得知具体6种如下:

void * _NSConcreteStackBlock[32] = { 0 };
void * _NSConcreteMallocBlock[32] = { 0 };
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 };
void * _NSConcreteWeakBlockVariable[32] = { 0 };

其中:NSConcreteStackBlockNSConcreteMallocBlockNSConcreteGlobalBlock 是常用到的栈区、堆区、全局区的block,其余3种为系统级别,实际开发较少会用到,所以先不展开说明。

1.2.1 NSGlobalBlock(全局Block)

当生成的block是独立的,不引用外部变量或者作为参赛参与其他函数,处于全局区,是为全局block

    void (^block)(void) = ^{
        NSLog(@"hello world!");
    };
    block();
    NSLog(@"block       %@", block);
2020-05-25 03:21:33.996335+0800 DDD[12987:411279] hello world!
2020-05-25 03:21:33.996513+0800 DDD[12987:411279] block       <__NSGlobalBlock__: 0x10e714050>

1.2.2 NSStackBlock(栈区)

当block 被copy 前,会处于栈区

以下为相关示例:

    __block int a = 1;

    NSLog(@"%@", ^{
       NSLog(@"aaa %d", a);
    });

结果如下:

2020-05-25 11:36:10.746965+0800 DDD[14013:600887] <__NSStackBlock__: 0x7ffeeb06b188>

1.2.3 NSMallocBlock(堆区)

当block 引用外部变量,此时block 会从全局区移动到堆区

    __block int a = 10;
    void (^block)(void) = ^{
        NSLog(@"a       %d", a);
    };
    block();
    NSLog(@"block       %@", block);
2020-05-25 03:22:31.565749+0800 DDD[13022:421048] a       10
2020-05-25 03:22:31.565921+0800 DDD[13022:421048] block       <__NSMallocBlock__: 0x600000ddc1b0>

1.3 Block 的常见使用

  1. 作为局部变量

    returnType (^blockName)(parameterTypes) = ^returnType(parameters) {
      // do sth..
    };
  2. 作为属性

    @property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);c
  3. 作为函数声明中的参数

    - (void)someMethodThatTakesABlock:(returnType (^)(parameterTypes))blockName;
  4. 作为函数调用中的参数

    [someObject someMethodThatTakesABlock:^returnType (parameters) {
      // do sth
    }];
  5. 作为typedef

    typedef returnType (^TypeName)(parameterTypes);
    TypeName blockName = ^returnType(parameters) {
            // DO STH
    };

二、循环引用及解决

2.1 循环引用的产生

举一个常见的循环引用的例子

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor purpleColor];
    self.name = @"hello";

    // ① self 持有block
    self.block  = ^{
        // ② block 又持有self
        NSLog(@"__name  %@", self.name);
    };
    self.block();
}

-(void)dealloc{
    NSLog(@"进入dealloc");
}

当示例代码中①处 self 持有了属性 block,而 ②种 block 代码块又持有了 self,这样会导致相互引用,最终系统在执行 dealloc 方法时,向任何一个变量发送release消息时,两者都因为引用其他变量而无法释放,最终会引起内存泄漏。示意图如下:

2.2 循环引用的解决

此时我们只讨论由block 引起的循环引用

2.2.1 使用weak 修饰

可以考虑对当前持有block 的对象,进行weak 修饰,创建一个弱引用指向它(原理为加入到弱引用计数表,而非引用技术表),但是并不会增加引用计数。

当前案例中,引用block 的是self,所以创建一个weak 对象 weakSelf。

如下所示,即可跳出 self - block - self 的循环引用

    __weak typeof(self) weakSelf = self;
    self.block  = ^{
        NSLog(@"__name  %@", weakSelf.name);
    };
    self.block();

2.2.2 weak-strong-dance(多级block)

在多级block 的场景下,弱引用会造成一些意外的情况。

对上面的例子做一些小改动,对block代码块执行的NSLog 函数,做一个延迟3秒执行操作,并让这个执行在页面消失之后,也就是dealloc 之后,看看是否还能打印 weakSelf.name

新的代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor purpleColor];
    self.name = @"hello";

    __weak typeof(self) weakSelf = self;
    self.block  = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    NSLog(@"__name  %@", weakSelf.name);
        });
    };
    self.block();
}

-(void)dealloc{
    NSLog(@"进入dealloc");
}

打印结果如下:

2020-05-25 12:43:05.876909+0800 DDD[14542:686386] 进入dealloc
2020-05-25 12:43:06.475894+0800 DDD[14542:686386] __name  (null)

可见,在进入dealloc 之后,添加的弱引用,已经被释放掉了,而设计的3秒后执行 weakSelf.name 也因为 weakSelf 为 nil 了,所以打印为null

解决:在代码块内,添加临时的强引用,将weakSelf 添加到强引用表里,会让dealloc 延迟执行,而当执行完之后block 会对临时的强引用进行释放,避免了循环引用的产生。

代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor purpleColor];
    self.name = @"hello";
    NSLog(@"初始name  %@", self.name);
    __weak typeof(self) weakSelf = self;
    self.block  = ^{
        __strong typeof(self) strongSelf = weakSelf;

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    NSLog(@"__name  %@", strongSelf.name);
        });
    };
    self.block();
}

-(void)dealloc{
    NSLog(@"进入dealloc");
}

打印结果及顺序:

2020-05-25 12:49:42.141133+0800 DDD[14610:698670] 初始name  hello
2020-05-25 12:49:47.634568+0800 DDD[14610:698670] __name  hello
2020-05-25 12:49:47.635013+0800 DDD[14610:698670] 进入dealloc

从结果可以看出,12:49:42 秒进入本页面,47秒后 进入打印strongSelf.name 成功,同时立刻执行dealloc方法。可见修改是成功的。

2.2.3 用临时VC 变量替代

在上文中,都是通过weak 对self修饰,来避免循环引用,实际上,还可以对self 所代替的Controller 引入临时变量,来解决循环引用,话不多说,实战一下。

    __block SecViewController *vc = self;
    self.block  = ^{

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    NSLog(@"__name  %@", vc.name);
            vc = nil;
        });
    };
    self.block();
  1. 创建一个临时变量vc,使用__block 修饰,因为后面会对他做修改。
  2. 在block内部进行打印属性,完毕后置空,跳出循环引用。

打印结果如下:

2020-05-25 12:54:28.262738+0800 DDD[14645:705623] 初始name  hello
2020-05-25 12:54:33.263402+0800 DDD[14645:705623] __name  hello
2020-05-25 12:54:33.263773+0800 DDD[14645:705623] 进入dealloc

可见通过ViewController 来替代self 也算可用性的

2.2.4 将VC 作为block 变量替代

如果block 是可以引入参数的,可以将控制器self 传入进去,作为临时变量,打印后block 会自动销毁,无需担心循环引用,具体如下:

创建block ,引入当前VC 的引用,的代码如下

@property (copy, nonatomic) void (^block)(SecViewController *);

执行函数为:

    self.block  = ^(SecViewController *vc){
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    NSLog(@"__name  %@", vc.name);
        });
    };
    self.block(self);

此时通过self.block(self) 传入self 的引用,block 内部会自动创建一个新的临时变量指向self,该临时变量使用后会销毁。打印机如果如下;

2020-05-25 12:57:17.929178+0800 DDD[14671:710117] 初始name  hello
2020-05-25 12:57:22.929717+0800 DDD[14671:710117] __name  hello
2020-05-25 12:57:22.930072+0800 DDD[14671:710117] 进入dealloc

三、底层探索

3.1 Block 的本质

Block 是一种匿名函数,它的类型是一种对象,本质是结构体。

为了证明这一点,对main.m 进行cpp 源码分析,现在main.m 生成一个block

void (^block)(void) = ^{
};
block();

进入项目文件下,输入编译命令:xcrun -sdk iphonesimulator clang -rewrite-objc main.m 得到main.cpp

打开查看相关cpp 实现如下:

  • OC 代码如下:

        void (^block)(void) = ^{
        };
        block();
  • C++ 源码如下:

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

    将代码精简一下成为:

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

可见主函数为__main_block_impl_0,在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代码块是一个结构体,有以下几个部分组成

  • block 实现 - __block_impl,这个是个指针函数,生成后会返回
  • block 描述信息 - __main_block_desc_0
  • 内部构造函数 - __main_block_impl_0。主要执行了 生成薪的impl 的函数赋值(如isa,flags,funptr)

继续挖, __block_impl 类型的结构体,实现如下:

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

因为有熟悉的isa 的存在,可知其也算个对象类的结构体。

3.2 Block 自动捕获外部变量

接下来看block 是如何捕获外界变量的,一共分为3重大的类型:

  • 局部变量
  • 局部静态变量
  • 全局变量和全局静态变量

3.2.1 局部变量捕获

局部变量的捕获,是指的捕获

此时生成一个变量 int a,在代码块对其进行引用,如下:

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

继续进行cpp 编译,发现int main 函数下,变成了如下

        int a = 10;
        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

通过精简,其内容如下:

    int a = 10;
        block = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a));
        block->FuncPtr(block);

可见__main_block_impl_0传入了新的参数a,接下来看如何实现,新的 main_block_impl_0 结构体如下:

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

可见这里比起之前的结构体,生成了一个新的属性a ,在构造函数里,发现了__main_block_impl_0中也多了一句a(_a) 的代码,可以猜测是生成了一个新的a,将传入的内部属性_a 赋值给 a

继续看main_block_func_0 的实现如下:

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

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

此时静态函数__main_block_func_0的内部对传入的block 进行了处理——接受了其内部的变量a——通过自身新生成一个a,就是这行代码

  int a = __cself->a; // bound by copy

自动生成的注释很清晰的说明了原理,通过copy绑定新的,所以说是值捕获。最终通过NSLog 打印新的变量a

3.2.2 局部静态变量捕获

局部静态变量的捕获,是指针捕获

接下来尝试一下,在block内部生成一个用static修饰的局部静态变量,看看如何捕获

        static NSInteger num = 3;

        NSInteger (^block)(NSInteger) = ^(NSInteger n){

            return n * num;
        };

        num = 1;
        NSLog(@"%ld", (long)block(2));

进入项目文件下,输入编译命令:xcrun -sdk iphonesimulator clang -rewrite-objc main.m 得到main.cpp

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSInteger *num;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger *_num, int flags=0) : num(_num) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static long __main_block_func_0(struct __main_block_impl_0 *__cself, NSInteger n) {
  NSInteger *num = __cself->num; // bound by copy


            return n * (*num);
        }

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, char * argv[]) {
    NSString * appDelegateClassName;
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        static NSInteger num = 3;

        NSInteger (*block)(NSInteger) = ((long (*)(NSInteger))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &num));
        // 精简为 *block = __main_block_impl_0(&__main_block_func_0, &__main_block_desc_0_DATA, &num);

        num = 1;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_ef866b_mi_0, (long)((NSInteger (*)(__block_impl *, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 2));

        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

变化:

  • 生成了新的整形指针NSInteger *num;

  • 将传入的NSInteger *_num 赋值给num

  • 代码块调用有所不同,如下:

    // 原函数
            NSInteger (*block)(NSInteger) = ((long (*)(NSInteger))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &num));
    
    // 精简后
    __main_block_impl_0(&__main_block_func_0, &__main_block_desc_0_DATA, &num)

    可以清晰的看出来,此时传入的num 是一个指针,可见,是进行了指针拷贝,而不是值拷贝,所以这里num 得到了改变,打印结果会是2

2020-05-27 00:11:06.899579+0800 DDD[25215:2116517] 2

3.3.3 全局变量、全局静态变量的捕获

全局变量、全局静态变量的捕获是直接取值

这次来点刺激的,把所有用的到的都写到block 里面,再验证下cpp 会发生什么

static NSInteger num3 = 300;            // 静态变量
NSInteger num4 = 3000;                  // 全局静态变量

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {

        NSInteger num = 30;             // 局部变量
        static NSInteger num2 = 3;      // 局部静态变量

        __block NSInteger num5 = 30000; // __block 修饰的变量

        void(^block)(void) = ^{
            NSLog(@"%ld", (long)num);
            NSLog(@"%ld", (long)num2);
            NSLog(@"%ld", (long)num3);
            NSLog(@"%ld", (long)num4);
            NSLog(@"%ld", (long)num5);
        };

        block();

        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

进入项目文件下,输入编译命令:xcrun -sdk iphonesimulator clang -rewrite-objc main.m 得到main.cpp

不管了,就先一股脑儿的贴上来吧,接下来分析:

static NSInteger num3 = 300;        // 全局变量
NSInteger num4 = 3000;                    // 静态全局变量

struct __Block_byref_num5_0 {        // __block 修饰的变量
  void *__isa;
__Block_byref_num5_0 *__forwarding;
 int __flags;
 int __size;
 NSInteger num5;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSInteger num;
  NSInteger *num2;
  __Block_byref_num5_0 *num5; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger _num, NSInteger *_num2, __Block_byref_num5_0 *_num5, int flags=0) : num(_num), num2(_num2), num5(_num5->__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_num5_0 *num5 = __cself->num5; // bound by ref
  NSInteger num = __cself->num; // bound by copy
  NSInteger *num2 = __cself->num2; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_8fd68e_mi_0, (long)num);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_8fd68e_mi_1, (long)(*num2));
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_8fd68e_mi_2, (long)num3);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_8fd68e_mi_3, (long)num4);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_8fd68e_mi_4, (long)(num5->__forwarding->num5));

        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->num5, (void*)src->num5, 8/*BLOCK_FIELD_IS_BYREF*/);}

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

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, char * argv[]) {
    NSString * appDelegateClassName;
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSInteger num = 30;
        static NSInteger num2 = 3;

        __attribute__((__blocks__(byref))) __Block_byref_num5_0 num5 = {(void*)0,(__Block_byref_num5_0 *)&num5, 0, sizeof(__Block_byref_num5_0), 30000};

        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num, &num2, (__Block_byref_num5_0 *)&num5, 570425344));

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

        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

分析:我们看到了,num、num2、num5 在block 的构造函数__main_block_impl_0 中都有了各自的实现,有的是取值,有的是通过forwarding 传递指针。

但是num3,和num4 呢?通篇下来,没有做任何处理,在block 函数内部,通过NSLog 直接取值了……

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_8fd68e_mi_2, (long)num3);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_8fd68e_mi_3, (long)num4);

结论:静态变量、全局静态变量,因为他们存在全局存储区,占用静态的单元,所以block 调用时是直接把值取过来用,并不需要指针引用。

3.3.4 小结

捕获变量的流程,除了静态变量和全局变量以外,都是block 在内部自动生成一个新的变量,用来接收外部变量。

3.3 Block 为什么要捕获Block()

block() 这种写法和普通的函数不一样,通过上文可以看到,最终实现的代码为

block->FuncPtr(block);

分析一下,这里是 block 结构体执行了其内部函数指针指向的函数,即声明一个函数式的属性,在任何想要调用的的地方调用。最终我们的确是调用了。

3.4 __block 的原理

为了让外部变量在block 内部得到更改,我们通常在外部对变量使用__block 的修饰符,究竟发生了什么呢?

那么这次,对外部加上__block 试试

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

3.4.1 CPP分析

可以看到,构造函数结构体有了变化;

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

与之前的变量不一样,生成了一个__Block_byref_a_0 *a; // by ref 的结构体指针对象,点击查看,结构如下:

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

可见和类的结构类似,有isa、数据a、内存大小_size、类似链表的__forwarding指针。结合__main_block_impl_0 的结构,可以清楚的看到,内部创建的一个结构体指针__Block_byref_a_0 *a,而 __a 的指针指向了 a。即,这里产生了一次指针拷贝。

接下来看看函数的实现:

  (a->__forwarding->a)++;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_acd650_mi_0, (a->__forwarding->a));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

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

3.4.2 源码分析

在上文中,我们看到关键函数是_Block_object_assign,打开libclosure 查看相应的函数实现,得到如下:

// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment.
void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
            /*省略*/
        break;

      case BLOCK_FIELD_IS_BLOCK:
        /*省略*/
        *dest = _Block_copy(object);
        break;

      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        /*******
         // copy the onstack __block container to the heap
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __block ... x;
         __weak __block ... x;
         [^{ x; } copy];
         ********/

        *dest = _Block_byref_copy(object);

注释简单的翻译是:当block 或者block 引用持有对象时,他们的拷贝常规帮助者会使用它们的入口点来做分配工作。

里面的是现实对持有对象类型的switch,重点关注BLOCK_FIELD_IS_BYREF,注释已经说了

copy the onstack __block container to the heap

拷贝栈内__block 容器到堆

太棒了, 这就是我们需要的实现函数,即对__block 的方法做出相应,拷贝到堆区,再来看函数 _Block_byref_copy(object) 具体的实现:

static struct Block_byref *_Block_byref_copy(const void *arg) {
    struct Block_byref *src = (struct Block_byref *)arg;

    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;

        // 问题 - __block 修饰变量 block具有修改能力
        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy

        copy->size = src->size;
}

在这里可以看到,具体的实现了:

  1. 拷贝新的对象到堆区,并置空isa

            struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
            copy->isa = NULL;
  2. 将新的拷贝对象的flag 与之前栈区对象的flags的相等

            // byref value 4 is logical refcount of 2: one for caller, one for stack
            copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
  3. 将新的对象指向原对象,即进行指针拷贝——这个是__block 的关键,新的指向原对象,与原地址指向同一个内存空间,故具备修改能力!

            // 问题 - __block 修饰变量 block具有修改能力
            copy->forwarding = copy; // patch heap copy to point to itself
            src->forwarding = copy;  // patch stack to point to heap copy
  4. 新对象大小与原对象大小相等

    copy->size = src->size;

总结:可见生成的a 是一个__Block_byref_a_0 类型结构体引用,而不是值拷贝的引用。__block 在作用于内部生成了变量的指针,通过改变指针指向的地址,来即改变原变量。

原理图如下:

四、签名

4.1 block 结构分析

block 的结构如下:

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    BlockInvokeFunction invoke;
    struct Block_descriptor_1 *descriptor; //
    // imported variables
};
typedef void(*BlockInvokeFunction)(void *, ...);

分析如下:

  • isa- 指向block父类的指针

  • 用来描述block 对象的flags

    // Values for Block_layout->flags to describe block objects
    enum {
        BLOCK_DEALLOCATING =      (0x0001),  // runtime
        BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
        BLOCK_NEEDS_FREE =        (1 << 24), // runtime
        BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
        BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
        BLOCK_IS_GC =             (1 << 27), // runtime
        BLOCK_IS_GLOBAL =         (1 << 28), // compiler
        BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
        BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
        BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
    };
  • 预留数值

  • 函数调用invoke

  • 描述信息 struct Block_descriptor_1 *descriptor; //

    此处的描述信息,是动态的,如果flags 中BLOCK_HAS_COPY_DISPOSE 值存在,则存在Block_descriptor_2,其结构如下;

    // 可选
    #define BLOCK_DESCRIPTOR_2 1
    struct Block_descriptor_2 {
        // requires BLOCK_HAS_COPY_DISPOSE
        BlockCopyFunction copy;
        BlockDisposeFunction dispose;
    };

    包含的是拷贝函数copy以及销毁函数 dispose

    如果flags 中BLOCK_HAS_SIGNATURE 存在,即block 包含有签名存在,则存在Block_descriptor_3

    方法签名就在3里面:

    • 方法签名
    • 方法布局
    #define BLOCK_DESCRIPTOR_3 1
    struct Block_descriptor_3 {
        // requires BLOCK_HAS_SIGNATURE
        const char *signature;
        const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
    };
  • // 引入的变量等

4.2 block签名的访问

上文中提到的Block_descriptor_2Block_descriptor_3,都可以通过 Block_descriptor_1 的内存偏移进行访问到,block 的签名为@?

4.2.1 代码部署:

先设定一个最简单的代码块块用来检查:

        void(^block)(void) = ^{
        };
        block();

4.2.2 汇编断点配置

  • 连接真机。配置汇编监控打开:Xcode——Debug——Debug Workflow——Always show Dissembly

  • 对block 代码块打断点,运行程序

  • 会跳转到汇编的main 步骤,通过Step Over键进入到 objc_retainBlock函数下

  • 打印当前x0寄存器:register read x0得到当前运行的block 函数的位置为0x0000000104afc028

  • 打印地址为0x0000000104afc028,得到block的类型、签名和内部实现地址:

    <__NSGlobalBlock__: 0x104afc028>
     signature: "v8@?0"
     invoke   : 0x104afa01c (/private/var/containers/Bundle/Application/3F9F1F87-526B-499C-93CE-784F88CE7DB4/DDD.app/DDD`__main_block_invoke)

    具体的细节请见下图

    查看方法签名

4.2.1 访问 Block_descriptor_2

当block 的block_layout结构中flag 参数BLOCK_HAS_COPY_DISPOSE 值为1 即可通过内存偏移访问到,相关原理可以见下方代码:

static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    return (struct Block_descriptor_2 *)desc;
}

函数实现中,第3行desc += sizeof(struct Block_descriptor_1); 即内存地址偏移Block_descriptor_1 的内存位,即可访问到Block_descriptor_2

4.2.2 访问 Block_descriptor_3

同理,在该block的flag 值 BLOCK_HAS_SIGNATURE 为正时,亦可以通过Block_descriptor_1的内存偏移而访问到Block_descriptor_3,当然,如果此时BLOCK_HAS_COPY_DISPOSE 值也为1,必须叠加Block_descriptor_2 的偏移位

static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
        desc += sizeof(struct Block_descriptor_2);
    }
    return (struct Block_descriptor_3 *)desc;
}

五、block的copy 分析

5.1 源码

libclosure 源码中,runtime.cpp这一页,block_copy 的函数实现如下所示:

void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    if (!arg) return NULL;

    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        result->invoke = aBlock->invoke;
#endif
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

5.2 分析

我们知道,基于block 的性质,在block 的使用中,当block被copy 时,会从堆区到栈区进行copy,具体发生了什么,不妨读一读源码,看看苹果工程师是如何设计blockcopy实现:

  1. 先判断block 的类型:先强转arg获取block_layout ,判断其flags

        aBlock = (struct Block_layout *)arg;
  2. 如果是堆block,即BLOCK_NEEDS_FREE:创建的对象需要程序员手动释放——由于此时block 空间已经初始化,所以不做拷贝,只增加引用计数即可,如下;

        if (aBlock->flags & BLOCK_NEEDS_FREE) {
            // latches on high
            latching_incr_int(&aBlock->flags);
            return aBlock;
        }
  3. 如果是全局block –BLOCK_IS_GLOBAL,直接返回

        else if (aBlock->flags & BLOCK_IS_GLOBAL) {
            return aBlock;
        }
  4. 如果是栈区block:进行一次拷贝操作,拷贝到——>堆区

        else {
            // Its a stack block.  Make a copy.
            struct Block_layout *result =
                (struct Block_layout *)malloc(aBlock->descriptor->size);
            if (!result) return NULL;
            memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
    #if __has_feature(ptrauth_calls)
            // Resign the invoke pointer as it uses address authentication.
            result->invoke = aBlock->invoke;
    #endif
            // reset refcount
            result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
            result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
            _Block_call_copy_helper(result, aBlock);
            // Set isa last so memory analysis tools see a fully-initialized object.
            result->isa = _NSConcreteMallocBlock;
            return result;
        }
    1. 创建一个新的Block_layout

              struct Block_layout *result =
                  (struct Block_layout *)malloc(aBlock->descriptor->size);
    2. 如果有函数指针调用,将新的Block_layout 指针指向原函数实现,即替换原方法实现:

      #if __has_feature(ptrauth_calls)
              // Resign the invoke pointer as it uses address authentication.
              result->invoke = aBlock->invoke;
      #endif
    3. 把原来aBlock的数据通过内存拷贝到堆区

      memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
    4. 重置引用计数

           result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
              result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
              _Block_call_copy_helper(result, aBlock);
    5. 重置isa 指向到一个新的堆block

      result->isa = _NSConcreteMallocBlock;

5.3 小结

5.3.1 操作copy 结果

  1. 对栈block进行copy:拷贝一份到堆区
  2. 对全局block进行copy:仍是全局block
  3. 对堆block进行copy:增加引用计数

5.3.2 总结图

俗话说的好,有图有真相,将上述流程汇总成一张图,是这样的

六、release 与dispose

当block 需要释放时,系统内做了哪些工作呢?先看看_Block_object_dispose 函数如下:

void _Block_object_dispose(const void *object, const int flags) {
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        // get rid of the __block data structure held in a Block
        _Block_byref_release(object);
        break;
      case BLOCK_FIELD_IS_BLOCK:
        _Block_release(object);
        break;
      case BLOCK_FIELD_IS_OBJECT:
        _Block_release_object(object);
        break;
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        break;
      default:
        break;
    }
}

可以看到,除了BLOCK_FIELD_IS_BYREF类型的情况,此种情况为通过__block修饰变量进入,其他情况直接调用release 或者不用操作即可。

BLOCK_FIELD_IS_BYREF 的情况下,需要调用release 方法,即_Block_byref_release(object),继续查看如下

static void _Block_byref_release(const void *arg) {
    struct Block_byref *byref = (struct Block_byref *)arg;

    // dereference the forwarding pointer since the compiler isn't doing this anymore (ever?)
    byref = byref->forwarding;

    if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
        int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
        os_assert(refcount);
        if (latching_decr_int_should_deallocate(&byref->flags)) {
            if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
                struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
                (*byref2->byref_destroy)(byref);
            }
            free(byref);
        }
    }
}

释放的过程比较简单:

  1. isa 指回原对象

    byref = byref->forwarding;
  2. 生成一个新的Block_byref_2 指针``byref2,指向byref`下个内存位

    struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
  3. 通过byref2 向原blog引用发送摧毁消息

    (*byref2->byref_destroy)(byref);
  4. 释放原block(byref)

    free(byref);

七、总结

本片篇幅较多,主要从block 的结构、block 循环引用的原理解决、block捕获外部变量,__block 究竟做了什么,以及源码层面上做了大量的分析。总的来说block是很巧妙的一项涉及,合理地用好,可以帮我们极大地提高工作效率。


文章作者: 李佳
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 李佳 !
评论
 上一篇
离屏渲染(Offscreen Render)详细解析 离屏渲染(Offscreen Render)详细解析
〇、序章作为一个面试中经常出现的话题,离屏渲染(Offscreen rendering)恐怕是极高频率提起的——最熟悉的陌生人,我们多少知道一点,了解一下由它的出现带来的屏幕刷新卡顿,但是又不是知道的那么透彻。在这篇文章里,我将比较详细的讲
2020-07-07 李佳
下一篇 
【数据结构与算法】查找算法(二)平衡二叉树 【数据结构与算法】查找算法(二)平衡二叉树
一、概念 平衡二叉树 (Self-Balancing Banary Search Tree 或者 Height-Balanced Binary Search Tree),是一种二叉排序树,其中每一个结点的左子树和右子树的高度差至多等于1。
2020-05-15 李佳
  目录