美文网首页iOS
Block的本质及变量截取

Block的本质及变量截取

作者: 无忘无往 | 来源:发表于2019-02-28 10:29 被阅读14次

    本文的内容主要是基于Clang编译器的官方文档所写。

    在开始探索Block的本质之前,大家先试着分析一下,下面的代码会输出什么:

    void main() {
        __block int a = 13;
        int b = 13;
        NSMutableString *str = [[NSMutableString alloc] initWithString:@"Hello"];
        void(^blockTest)(void) = ^{
            NSLog(@"a = %d, b = %d, str = %@", a, b, str);
        };
        a++;
        b++;
        [str appendString:@"World"];
        blockTest();
    }
    

    如果你对输出结果不是那么有把握的话,那么相信通过今天的这篇文章,你会有一个明确的答案(答案在文章最后)。

    Clang

    先说些题外话,什么是Clang?Clang是C++编写的编译器。我们知道,我们平常代码所写的任何程序,最终都需要通过编译器转换成与语言无关的机器二进制代码。而Clang,则是支持/C++/Objective-C/Objective-C++的编译器。那我们在做OC开发时,可能也会听说LLVM编译器,那么Clang和LLVM之间是什么关系呢?

    它们的关系如下图所示:


    LLVM架构

    Clang是编译器的前端,它会分析具体的编程语言,然后用于生成与机器无关的中间代码。而LLVM是编译器的后端,与具体编程语言无关,而是会去分析统一的中间代码,生成符合对应机器的目标程序。

    这样拆分前端后端的好处在于,前后端可以独立的替换,便于编译器的优化。

    关于Clang,我们了解这些就足够了。

    Block的本质

    回到Block上来。我们在使用Block语法时,总会感觉到有些奇怪:

    ^{
          NSLog(@"Hello");
     };
    

    这么一个^{}是什么鬼?似乎在别的语言中也没有见过这么个关键字定义。其实,^{}对于Clang编译器来说,仅仅是一个语言标记,它会告诉Clang,这里我需要定义一个Block类型的结构体。 而Clang发现这个语言标记时,会将^{}这么一个奇怪的定义,转换为C语言中的结构体。经过Clang转换后的Block,其形式是这样的:

    struct Block_literal_1 {
        // 第一部分. Block基本信息以及 invoke函数指针
        void *isa; // initialized to &__NSGlobalBlock__ or &__NSMallocBlock__ or &__NSStackBlock__ 
        int flags;
        int reserved;
        void (*invoke)(void *, ...);
        
        // 第二部分. Block descriptor指针
        struct Block_descriptor_1 {
        unsigned long int reserved;         // NULL
            unsigned long int size;         // sizeof(struct Block_literal_1)
            // optional helper functions
            void (*copy_helper)(void *dst, void *src);     // IFF (1<<25)
            void (*dispose_helper)(void *src);             // IFF (1<<25)
            // required ABI.2010.3.16
            const char *signature;                         // IFF (1<<30)
        } *descriptor;
        
        // 第三部分. Block所截取的外部变量(如果有的话)
        // imported variables
    };
    

    笔者将Block结构体定义分成了三个部分:

    • Block基本信息以及 invoke函数指针
    • Block descriptor指针
    • Block所截取的外部变量

    在这里我们得出结论:Block的本质是一个C语言的struct

    Block对应的结构体

    上面探讨了Block的本质是一个struct,接下来我们就来详细看一下这个 Block struct的定义。

    Block基本信息以及Block descriptor

    struct Block_literal_1 {
        // 第一部分. Block基本信息以及 invoke函数指针
        void *isa; // initialized to &__NSGlobalBlock__ or &__NSMallocBlock__ or &__NSStackBlock__ 
        int flags;
        int reserved;
        void (*invoke)(void *, ...);
        
         // 第二部分. Block descriptor指针
        struct Block_descriptor_1 {
        unsigned long int reserved;         // NULL
            unsigned long int size;         // sizeof(struct Block_literal_1)
            // optional helper functions
            void (*copy_helper)(void *dst, void *src);     // IFF (1<<25)
            void (*dispose_helper)(void *src);             // IFF (1<<25)
            // required ABI.2010.3.16
            const char *signature;                         // IFF (1<<30)
        } *descriptor;
        ...
    };
    

    我们先来看Block struct的第一部分和第二部分。至于Block的第三部分,外部变量的截取,我们会在下面单独的章节进行讨论。
    当我们声明一个Block时,对应的Block struct会被如下初始化:

    1. 系统会声明并初始化一个Block descriptor结构体。初始化Block descriptor步骤如下
      a. Block descriptor 的size部分会被设置为Block结构体的大小
      b. copy_helper 和 dispose_helper函数指针会被设置为对应的函数指针(如果需要这两个helper 函数的话)

    2. 系统初始化Block 结构体。 初始化Block 结构体的步骤如下:
      a. isa 部分会被设置为__NSGlobalBlock__/__NSMallocBlock__/__NSStackBlock__ 所对应的地址。注意这里是地址,而不是NSMallocBlock这些变量。
      b. flags 会被置为对应的flag数值。比如,如果Block struct需要copy,dispose helper函数时,响应的flag会被置位。同时,flags还有标志Block ABI 版本的功能。
      c. 设置invoke函数指针指向对应的函数。该函数的第一个参数是Block struct本身的指针,而其余的参数则是Block执行时外部要传入的参数(如果有的话)

    举个例子,对于下面的Block:

    ^ { printf("hello world\n"); }
    

    Clang会创建如下内容:

    struct __block_literal_1 {
        void *isa;
        int flags;
        int reserved;
        void (*invoke)(struct __block_literal_1 *);
        struct __block_descriptor_1 *descriptor;
    };
    
    void __block_invoke_1(struct __block_literal_1 *_block) {
        printf("hello world\n");
    }
    
    static struct __block_descriptor_1 {
        unsigned long int reserved;
        unsigned long int Block_size;
    } __block_descriptor_1 = { 0, sizeof(struct __block_literal_1) };
    

    那么Block struct将会如下被初始化:

    struct __block_literal_1 _block_literal = {
         &__NSGlobalBlock__,
         (1<<29), <uninitialized>,
         __block_invoke_1,
         &__block_descriptor_1
    };
    

    这是Clang文档给出的官方例子,但是我们这里不要去纠结flags究竟是设置的什么,因为根据本人的测试,其flags的值并不是1<<29。

    这里有个问题,就是什么时候isa会被设为&__NSGlobalBlock__/&__NSMallocBlock__/&__NSStackBlock__ 呢?

    • 当Block中没有引用外部变量,或引用了全局变量,const 标量或static变量时,Block的isa会被设置为&__NSGlobalBlock__。 这时的Block生命周期是伴随程序始终的。
    • &__NSStackBlock__ 表示这个block, 是在栈上面分配的,出了栈就会消亡。使用了外部栈变量,就会是__NSStackBlock__ 类型。
    • &__NSMallocBlock__ 表示Block复制到堆上面了,可以存储下来,以后使用。当Block引用了外部的OC对象,Block对象或用__block修饰的变量时,Block会被设置为&__NSMallocBlock__ 类型。这里有一点要注意,在ARC的情况下。只要将block赋值给变量,就自动帮你复制了。也就是说,如果将一个栈上的block赋值给另一个block变量,则被赋值的block变量类型是 &__NSMallocBlock__ 类型。

    如下面代码:

        int a = 13;
        NSLog(@"block type is %@", NSStringFromClass([^{NSLog(@"%d", a);} class]));
        blockType1 blk2 = ^{
            NSLog(@"%d", a);
        };
        NSLog(@"block type is %@", NSStringFromClass([blk2 class]));
    

    输出为:


    image

    而对于const类型的引用,

        const int a = 13;  // 这里是const引用
        NSLog(@"block type is %@", NSStringFromClass([^{NSLog(@"%d", a);} class]));
        blockType1 blk2 = ^{
            NSLog(@"%d", a);
        };
        NSLog(@"block type is %@", NSStringFromClass([blk2 class]));
    

    输出为:


    image

    这是因为对于Global,不必需要再在堆上开辟一块内存。

    Block的外部变量截取

    理解Block的关键,在于理解Block是如何处理外部变量的。

    我们先来想一想,Block中会截取那些类型的外部变量:

    • 全局/静态变量
    • 自动(auto)存储类型
    • Block类型
    • NSObject类型
    • __block修饰的变量

    截取全局/静态类型变量

    对于全局/静态变量,Block会直接引用这类变量,不会copy。 例如,

    static int a = 13;
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        NSLog(@"Outside Block, static int a address is %p", &a);
       ^{
            NSLog(@"Inside Block, static int a address is %p", &a);
        }();
       
    }
    

    输出为:


    image

    在Block 外和Block内,static int a的地址是一样的,Block并没有做特殊的处理。

    截取自动存储类型变量

    所谓自动存储类型,指的是auto类型。我们可以理解为栈上的变量(Block类型、__block、NSObject类型除外),其内存会有系统自动释放。

    对于auto类型的变量截取,Clang文档有如下描述:

    Variables of auto storage class are imported as const copies.

    也就是说,auto类型会在Block中用const copy一份。也就是说Block内、外是完全不同的两个变量。

    我们做个测试:

        int b = 12;
        NSLog(@"Outside Block, address of int b is %p", &b);
       ^{
            NSLog(@"Inside Block, address of int b is %p", &b);
        }();
    

    输出为:


    image

    可以看到,在Block外和Block内部,表面上同样的b变量,其地址是不一样的。究其原因,就是因为在Block内部,系统会默默的const copy一份b。

    这时候,Block的数据结构是这样的:

    int x = 10;
    void (^vv)(void) = ^{ printf("x is %d\n", x); }
    x = 11;
    vv();
    
    struct __block_literal_2 {
        void *isa;
        int flags;
        int reserved;
        void (*invoke)(struct __block_literal_2 *);
        struct __block_descriptor_2 *descriptor;
        const int x;   // 这里会有一份const copy
    };
    
    struct __block_literal_2 __block_literal_2 = {
          &_NSConcreteStackBlock,
          (1<<29), <uninitialized>,
          __block_invoke_2,
          &__block_descriptor_2,
          x
     };
    

    一般的,对于标量类型(int, float, bool等基本类型),struct,unions和函数指针类型,都会采用const copy的方式,将Block外部的变量拷贝到Block内部

    这里需要注意一点,在iOS系统中,当我们把一个stack 上的Block赋值给一个Block变量时:

    void (^vv)(void) = ^{ printf("x is %d\n", x); }
    

    会默认调用Block的copy方法,即,上面实际上是如下代码:

    void (^vv)(void) = [^{ printf("x is %d\n", x); } copy];
    

    这样得到的vv,是一个在堆上的Block变量。这时候再输出vv中x的地址,会得到一个堆上的地址。

    因此,我们在做实验的时候,不要输出对拷贝后的Block中变量地址,而应该直接输出Block中的地址:

     ^{
            NSLog(@"Inside Block, static int a address is %p", &a);
        }();
    

    上面代码中并没有赋值,因此会输出栈上的a的const copy地址。

    截取Block类型变量

    对于截取Block类型的变量,在Block内部,会保留const copy其Block指针。
    如下代码:

        int a4 = 13;
        void (^existingBlock)(void) = ^{NSLog(@"Hello %d", a4);};
        NSLog(@"Outside Block, address of block pointer address is %p, block address is %p", &existingBlock, existingBlock);
        ^{
        NSLog(@"Inside Block, address of block pointer address is %p, block address is %p", &existingBlock, existingBlock);}();
    
        blockType1 blk = existingBlock;
        blk();
    

    输出为:


    image

    这里可以看到,对于Block变量,existingBlock(注意,这个existingBlock变量是一个Block指针,而不是Block本身)被const copy了一份到Block中。而对于Block指针所指向的Block实体,并没有发生改变。
    也就说,在Block内部和外部,会有两个Block指针,指向了同一个Block结构体。

    这里再次强调一下,我们所声明的Block变量existingBlock,是一个指向Block类型的指针,而不是Block实体。正如同NSObject *obj = [NSObject new]一样,obj是一个指向NSObject的指针,而不是NSObject实体

    下面是Clang文档的例子:

    void (^existingBlock)(void) = ...;
    void (^vv)(void) = ^{ existingBlock(); }
    vv();
    
    struct __block_literal_3 {
       ...; // existing block
    };
    
    struct __block_literal_4 {
        void *isa;
        int flags;
        int reserved;
        void (*invoke)(struct __block_literal_4 *);
        struct __block_literal_3 *const existingBlock;  // 这里可以看到,在Block内部,是保持了外部的Block指针
    };
    
    void __block_invoke_4(struct __block_literal_2 *_block) {
       __block->existingBlock->invoke(__block->existingBlock);
    }
    
    void __block_copy_4(struct __block_literal_4 *dst, struct __block_literal_4 *src) {
         //_Block_copy_assign(&dst->existingBlock, src->existingBlock, 0);
         _Block_object_assign(&dst->existingBlock, src->existingBlock, BLOCK_FIELD_IS_BLOCK);
    }
    
    void __block_dispose_4(struct __block_literal_4 *src) {
         // was _Block_destroy
         _Block_object_dispose(src->existingBlock, BLOCK_FIELD_IS_BLOCK);
    }
    
    static struct __block_descriptor_4 {
        unsigned long int reserved;
        unsigned long int Block_size;
        void (*copy_helper)(struct __block_literal_4 *dst, struct __block_literal_4 *src);
        void (*dispose_helper)(struct __block_literal_4 *);
    } __block_descriptor_4 = {
        0,
        sizeof(struct __block_literal_4),
        __block_copy_4,
        __block_dispose_4,
    };
    

    这时候Block的数据结构是:

    struct __block_literal_4 _block_literal = {
          &_NSConcreteStackBlock,
          (1<<25)|(1<<29), <uninitialized>
          __block_invoke_4,
          & __block_descriptor_4  // 这里可以看到,在Block内部,是保持了外部的Block指针
          existingBlock,
    };
    

    截取NSObject类型变量

    在Clang中,NSObject类型变量被当做__attribute__((NSObject))类型。Block截取NSObject对象时,同样会做一份const copy NSObject *
    比如:

    @interface MyObject : NSObject
    - (void)sayMyObjectAddress
    @end
    
    @implementation MyObject
    - (void)sayMyObjectAddress {
        NSLog(@"Instance pointer address is %p, Instance address is %p", &self, self);
    }
    @end
    
        MyObject *obj = [MyObject new];
        [obj sayMyObjectAddress];
        ^{
            [obj sayMyObjectAddress];
        }();
    

    输出为:


    image

    可以看到,当Block对NSObject做const copy时,仅是做了浅拷贝,并没有复制指针所指向的内容,仅仅是const copy了指针。因此,这里的self指针地址是改变了,而self指针所指向的地址都是同一个。

    就像上面Block类型变量的例子,是同一个道理。

    而对于NSObject类型,同样需要两个copy helper函数:

    void __block_copy_foo(struct __block_literal_5 *dst, struct __block_literal_5 *src) {
         _Block_object_assign(&dst->objectPointer, src-> objectPointer, BLOCK_FIELD_IS_OBJECT);
    }
    
    void __block_dispose_foo(struct __block_literal_5 *src) {
         _Block_object_dispose(src->objectPointer, BLOCK_FIELD_IS_OBJECT);
    }
    

    截取__block修饰的变量

    鉴于我们上面所说的都是const copy,因此对于在Block中对于其截取变量的任何改变,都是不被允许的。如果我们要修改Block内部的值,编译器就会提示如下错误:


    image

    那如何在Block中修改截取变量的值呢?我们自然会想到对外部变量加上__block修饰符。我们将上面代码改成下面的形式,则会顺利编译通过:

        __block int b = 13;
        NSLog(@"Outside Block, address of __block int b is %p, b = %d", &b, b);
        blockType1 blk = ^{
            b++;
            NSLog(@"Inside Block, address of __block int b is %p, b = %d", &b, b);
        };
        blk();
        NSLog(@"After Block, address of __block int b is %p, b = %d", &b, b);
    

    输出为:


    image

    这里会发现一个有意思的现象,虽然在进入Block前后,b的地址并不一样!** 也就是在进入Block前后,其实会有两个不同的b **。

    之所以会这样,与Clang对于__block类型变量的处理有关。

    当变量被标记为__block类型时,Clang会对变量b进行改写成一个如下格式的struct:

    struct _block_byref_foo {
        void *isa;   // 设置为NULL
        struct Block_byref *forwarding;   // Block外部变量的地址
        int flags;   //refcount;
        int size;  // size of _block_byref_foo
        typeof(marked_variable) marked_variable;  // copy of Block 外部变量
    };
    

    比如:

    int __block i = 10;
    i = 11;
    

    会被Clang改写做:

    struct _block_byref_i {
      void *isa;
      struct _block_byref_i *forwarding;
      int flags;   //refcount;
      int size;
      int captured_i;
    } i = { NULL, &i, 0, sizeof(struct _block_byref_i), 10 };
    
    i.forwarding->captured_i = 11;
    

    可以看到,int __block i 被改写为了struct _block_byref_i 结构体。这里需要明确一点:
    添加了__block关键字后的int b,实质类型并不是int类型,而是一个struct _block的结构体类型了

    这里有个关键的属性变量,forwardingforwarding指向一个__block结构体。
    当__block在栈上时,forwarding会指向__block自身。而当__block在堆上生成一份copy时,这时候栈上的forwarding会指向堆上的那一份拷贝。而在堆上的那个__block的__forwarding 指针,则指向自己的首地址。

    也就是说,只要通过forwarding来操作__block结构体捕获的外部变量,实质上是操作的同一个变量。
    我们用图片可以更清楚的弄懂其中的原理:

    image

    这也就是为什么,即使Block外和Block内部b分别是两个变量,而b的值却可以被改变的原因。因为在栈上的__block结构体中,通过forwarding指针指向了堆上的Block的地址。那么当在Block内部修改b的值,也就是改变堆上的int b的值的时候,在Block外部再访问b的值的时候,其实在栈上的__block int b通过__forwarding 指针,访问到了堆上的__block int b,这让我们感觉在栈上的变量也被修改了。

    这也就是为什么,在测试代码中,在执行完Block后,再输出b的地址,发现是和Block内部的地址一致,而不是进入Block之前的地址的原因。(以为进入Block后,再次访问b,实际上会指向堆上的那个b,而不是之前栈上的那个b)

    当我们将__block的变量导入Block中时,Clang会作如下改写:
    例如,

    int __block i = 2;
    functioncall(^{ i = 10; });
    

    会被Clang做如下改写:

    struct _block_byref_i {
        void *isa;  // set to NULL
        struct _block_byref_voidBlock *forwarding;
        int flags;   //refcount;
        int size;
        void (*byref_keep)(struct _block_byref_i *dst, struct _block_byref_i *src);
        void (*byref_dispose)(struct _block_byref_i *);
        int captured_i;
    };
    
    
    struct __block_literal_5 {
        void *isa;
        int flags;
        int reserved;
        void (*invoke)(struct __block_literal_5 *);
        struct __block_descriptor_5 *descriptor;
        struct _block_byref_i *i_holder;
    };
    
    void __block_invoke_5(struct __block_literal_5 *_block) {
        _block_byref_i * i_holder = _block->i_holder;
       i_holder->forwarding->captured_i = 10;
    }
    
    void __block_copy_5(struct __block_literal_5 *dst, struct __block_literal_5 *src) {
         _Block_object_assign(&dst->i_holder, src->i_holder, BLOCK_FIELD_IS_BYREF | BLOCK_BYREF_CALLER);
    }
    
    void __block_dispose_5(struct __block_literal_5 *src) {
         _Block_object_dispose(src->i_holder, BLOCK_FIELD_IS_BYREF | BLOCK_BYREF_CALLER);
    }
    
    static struct __block_descriptor_5 {
        unsigned long int reserved;
        unsigned long int Block_size;
        void (*copy_helper)(struct __block_literal_5 *dst, struct __block_literal_5 *src);
        void (*dispose_helper)(struct __block_literal_5 *);
    } __block_descriptor_5 = { 0, sizeof(struct __block_literal_5) __block_copy_5, __block_dispose_5 };
    

    上面的数据结构会做如下初始化

    struct _block_byref_i i_holder = {( .isa=NULL, .forwarding=&i, .flags=0, .size=sizeof(struct _block_byref_i), .captured_i=2 )};
    struct __block_literal_5 _block_literal = {
          &_NSConcreteStackBlock,
          (1<<25)|(1<<29), <uninitialized>,
          __block_invoke_5,
          &__block_descriptor_5,
          & i_holder,
    };
    

    是否只有__block类型才能够在Block中被修改?

    这里插入一个小测试,对于静态变量a,是否可以在Block中作出改变呢?

    static int a = 13;
    - (void)viewDidLoad {
           [super viewDidLoad];
             NSLog(@"Outside Block, static int a address is %p", &a);
       ^{
            NSLog(@"Inside Block, static int a address is %p", &a);
           a++;
        }();
        NSLog(@"Now a is %d", a);
    }
    

    答案是可以的,在Block之后,a的值变为14。这是因为对于全局/静态变量而言,Block会直接引用变量,而不会做const copy。

    所以,我们这一节讨论的,是除去全局、静态变量外,被Block const copy的其他的类型变量。

    小测试

    题目一. 下面代码会输出什么?

    typedef void(^blockType)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int i = 13;
            blockType blk = ^{
                NSLog(@"In block i = %d", i);
            };
            i += 2;
            
            blk();
            NSLog(@"Now i = %d", i);
            
        }
        return 0;
    }
    

    这里考察对于auto类型变量,Block的截取方式。因为auto变量会在Block中做一份const copy,因此在Block内外,实质上应该存在两个i
    这里的输出为:

    image

    题目二. 下面的代码会 正常输出/编译错误/runtime crash

    typedef void(^blockType)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
           NSString *str = @"Hello";
            blockType blk = ^{
                str = @"World";
            };
            blk();
            NSLog(@"Now str is %@", str);
        }
        return 0;
    }
    

    因为对于NSObject类型,在Block中会当做NSObject *const obj处理,此时是一个指针常量。对于指针常量,是不能够更改其指针所指向的位置的,因此,这里会出现编译错误。

    题目三. 下面的代码会 正常输出/编译错误/runtime crash

    typedef void(^blockType)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block NSString *str = @"Hello";
            blockType blk = ^{
                str = @"World";
            };
            blk();
            NSLog(@"Now str is %@", str);
        }
        return 0;
    }
    

    因为str变量用了__block修饰,因此__block NSString *str 实质上一个__block struct 类型变量:

    struct _block_byref_str {
            void *isa;
            struct _block_byref_str  *forwarding;
            int flags;
            int size;
            NSString *captureStr;
    }
    

    当创建__block 类型变量时,在Block结构体中,会存储__block结构体指针:

    struct __block_literal {
            void *isa;
            int flags;
            int reserved;
            void (*invoke)(struct __block_literal * _cself);
            struct __block_descriptor *descriptor;
             struct _block_byref_str *str_holder;    // __block结构体指针
    }
    

    当调用invoke方法时,会是这样的:

    void invoke(struct __block_literal * _cself) {
            _block_byref_str *str_holder = _cself->str_holder;
            str_holder->forwarding->captureStr = @"World";
    }
    

    由于通过forwarding指针,确保了Block外部和内部的str都是一个指针,因此,当Block内部的str指向新的地址时(str = @"World"),在Block外部的str也指向了新的地址。(因为它们是同一个东西)。
    这个过程用图表示为:

    1. __block str = @"World";


      image
    1. 当在Block中操作str=@"World"时,相应的__block结构体会拷贝到heap上,同时,stack上的__block结构体的forwarding指针也会指向heap上的那份copy:


      image
    2. 因此,在Block外面再次输出str的内容时,由于这时候stack上__block结构体的forwarding指针已经指向了heap上的__block结构体,因此也会输出heap上的captured_str指针所指向的内容:@“World”

    为了验证我们的猜测,我们可以用如下代码:


    image

    在进入Block前,Block中,进入Block后分别设置断点,并打印aR指针的地址&aR,会得到如下结果:

    image

    可以看到,在Block中和进入Block后,aR的地址是一样的,而在进入Block之前,则是另一个地址。这是因为在stack上的__block结构变量,将其forwarding指针指向了heap地址所导致的。

    题目四. 下面的代码会 正常输出/编译错误/runtime crash

    typedef void(^blockType)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSString *aStr = @"Hello";
            __block NSString *str = aStr;
            blockType blk = ^{
                str = @"World";
            };
            blk();
            NSLog(@"Now a aStr is %@", aStr);
            NSLog(@"Now str is %@", str);
        }
        return 0;
    }
    

    这个题目和题目三类似,只不过对于str的赋值由__block NSString *str = @"Hello"变成了__block NSString *str = aStr

    上面这段代码会正常输出,其结果为:


    image

    至于str为什么会由@"Hello"变成@“World”,其原因见题目三。

    这里aStr是没有任何变化的,这是因为在将str在Block中赋值为@"World"时,仅仅是将str指向了新的地址,而没有更改原地址的内容。而aStr一直指向旧的地址,也就是值为@"World"的地址。

    题目五. 下面的代码会 正常输出/编译错误/runtime crash

            NSMutableString *str = [NSMutableString stringWithString:@"Hello"];
            blockType blk = ^{
                [str appendString:@" World"];
            };
            blk();
            NSLog(@"Now str is %@", str);
    

    答案是会正常输出。因为对于NSObject类型来说,Block会copy一份指针常量来保存NSObject的地址。所谓指针常量,是指指针指向的地址是不可用更改的。而这里在Block中,并没有更改指针指向的地址,而仅仅是改变了指针指向地址中的值,这个操作是允许的。
    其输出结果为:


    image

    同样的,类似还有下面代码,也是可以正常运行,并输出名字Tim:

            MyRetaion *aR = [MyRetaion new];
            aR.name = @"Jack";
            blockType blk = ^{
                aR.name = @"Tim";
            };
            blk();
            NSLog(@"Now name is %@", aR.name);
    

    总结

    在本篇文章中,我们根据Clang的官方文档,分析总结了Clang为了支持Block,其背后所使用的数据结构。同时,我们重点分析了Block对于不同类型的外部变量的截取方式。按照Block不同的处理方式,Block截取的变量类型可以分为:

    • 全局/静态类型
    • auto类型
    • Block类型
    • NSObject类型
    • __block类型

    不同的类型,Block都有不同的截取处理方式。

    通过深入了解Block的机制,相信对大家编程中正确高效的使用Block,是很有帮助的。

    现在来回答我们文章最开始的部分,代码的输出结果为:

    a = 14, b = 13, str = HelloWorld
    

    至于原因,相信大家都会知道了:)

    相关文章

      网友评论

        本文标题:Block的本质及变量截取

        本文链接:https://www.haomeiwen.com/subject/puzcuqtx.html