iOS Block(1)-底层原理探索、block的类型和copy

1. block的本质

我们通过一个简单的demo,解析一下block的底层原理.
定义一个简单的block并调用:

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ^(){
            NSLog(@"Hello world, I'm block!");
        }();
    }
    return 0;
}

将OC代码转换成C++代码

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;
  __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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_a2e15e_mi_0);

        }

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

        ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))();
        
    }
    return 0;
}
//我们OC下的block相关代码
 ^(){
        NSLog(@"Hello world, I'm block!");
    }();

装换成的C++代码就是:
   ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))();

block的调用实际上就是__main_block_impl_0这个结构体,结构体实现如下:

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

__main_block_impl_0结构体内部有一个与结构体同名的__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)函数,这是C++结构体的写法,该函数为结构体的构造函数,相当于OC类中的- (instancetype)init;方法。__main_block_impl_0函数携带三个参数,最后一个参数为可选的,默认值为0。再看结构体__main_block_impl_0,发现其第一个成员imp也是个结构体,结构体类型为__block_impl

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

通过结构体__block_impl实现代码中的isa指针,显而易见这是个对象,因此可以准确地说block的本质是一个OC对象。结构体的第二个成员仍然是个__main_block_desc_0类型的结构体.

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}

该结构体两个成员一个是系统的保留值reserved = 0,另一个Block_size则代表了该block的大小。
接下来回到block的调用函数__main_block_impl_0,((void (*)())&__main_block_impl_0((void*)__main_block_func_0,&__main_block_desc_0_DATA))();该函数就是结构体__main_block_impl_0的构造函数__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0),它有两个必传的参数,一个是函数指针fp ,一个是结构体指针desc,关于结构体指针所指向的结构体就是上面分析到的__main_block_desc_0,那么第一个参数函数指针fp到底是什么?在这个demo的C++实现代码中,fp指向的函数为__main_block_func_0__main_block_func_0的函数实现代码如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) 
{
            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_a2e15e_mi_0);
}

由于在OC代码中,我们block体内打印了一个字符串,与这个__main_block_func_0函数内的代码完全一致。研究发现__main_block_func_0这个函数的作用就是将block体内的代码封装成一个函数,也就是说block体内的所有OC代码被封装成__main_block_func_0这个函数。与我们OC中的代码NSLog(@"Hello world, I'm block!");相对应的就是NSLog((NSString*)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_a2e15e_mi_0);.
通过上面的分析,可以肯定的是block本质上也是一个OC对象,它内部也有一个isa指针,block还是一个封装了函数的调用的OC对象。

2. block的变量捕获(capture)

一. 局部变量之auto变量

什么是auto变量?局部变量有哪几种?
所谓的auto变量就是非静态的局部变量,离开作用于就会销毁。例如下面这个函数:

- (void)example{
   int a = 5;  //等价于auto int a = 5;
   NSString *name = @"Block"; //等价于 auto NSString *name = @"Block";
   static int b = 10;  //这个b就不是auto变量
}

常识小结:通常情况下我们定义的局部非static变量都是auto变量,系统会默认在前面加上auto关键字的;但是静态局部变量就不会有auto前缀,加了也会由于报错而编译不通过。
为了保证block内部能够正常访问外部的变量,block有个变量捕获机制,这个变量捕获机制之后block变成什么样子?:

//demoA
#import <Foundation/Foundation.h>

typedef void(^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int a = 10;
        Block block = ^(){
            NSLog(@"你好世界!a = : %d ;",a);
        };
        
        a = 20;
        block();
//        2018-08-28 21:34:33.276996+0800 Interview03-block[99340:9151961] 你好世界!a = : 10 ;
        
    }
    return 0;
}

上面demo,block内部访问局部变量a的值,后面在调用block之前修改了a的值,但是打印出来的a的结果仍然为修改之前的值,将上边的代码转换成C++代码:

typedef void(*Block)(void);


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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

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

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

        int a = 10;

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

        a = 20;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    }
    return 0;
}

对比之前没有访问任何变量的block结构体,此时的block所对应的结构体__main_block_impl_0里面多了一个成员int a,并且结构体的构造函数__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a)多出了一个参数_a,(知识点:后面的: a(_a)为C++的语法,意为将参数_a赋值给成员a)。
在实现block的时候,对应的C++代码为Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0,&__main_block_desc_0_DATA, a));可见,系统将a作为函数__main_block_impl_0的参数传递进去,所以block所对应的结构体中int a;这个成员所对应的值a = 10;后面我们修改了a的值为20,并使用block();调用block 打印a的值,这个时候调用了函数__main_block_func_0(struct __main_block_impl_0 *__cself),实现如下:

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

其内部访问变量a的方式为:int a = __cself->a;__cself为block所对应的结构体对象,所以这个a也就是之前结构体__main_block_impl_0中保存的成员变量a的值,即为10,而不是后面修改的20。针对这个问题,我的看法是block在调用的时候,其实此时main()函数中的a变量相对于block来说是个外部的变量,因为block对应的结构体内部有自己的变量a,外面怎么修改不会影响到block结构体内部成员a的值。

二. 局部变量之static变量

根据demoA,我们在demoB中中block内部增加访问静态的局部变量static int b以及修改a、b变量的值后,调用block打印的结果:

//demoB
#import <Foundation/Foundation.h>

typedef void(^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        auto NSString *name = @"Block";
        int a = 10;
        static int b = 10;
        Block block = ^(){
            NSLog(@"你好世界!a = : %d ;",a);
            NSLog(@"你好世界!b = : %d ;",b);
        };
        
        a = 20;
        b = 20;
        block();
//        2018-08-28 23:16:53.244791+0800 Interview03-block[861:9731638] 你好世界!a = : 10 ;
//        2018-08-28 23:16:53.245153+0800 Interview03-block[861:9731638] 你好世界!b = : 20 ;
        
    }
    return 0;
}

发现局部静态变量b修改之后,block内部打印的结果也变了!
局部变量a的访问过程demoA已经分析过了,接下来仍旧通过C++代码研究局部静态变量b的捕获过程:

typedef void(*Block)(void);

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__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_0b7d13_mi_0,a);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_0b7d13_mi_1,(*b));
        }

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

        int a = 10;
        static int b = 10;
        Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &b));

        a = 20;
        b = 20;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    }
    return 0;
}

通过C++代码发现,局部自动变量a与静态变量b的捕获方式不同,block结构体中,aint变量,bint *变量,也就是指针。在定义block的时候,Block block = ((void (*)())&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, a, &b)),传递的也是b变量的指针,调用block的时候,__main_block_func_0中获取b也是通过block的结构体__main_block_impl_0访问内部成员变量b,与结构体外部变量b指向的是同一块内存地址,所以只要有地方修改b,结构体内部也会跟随变化,这样就解释了为啥“同样修改了局部auto变量与局部static变量,block访问的结果不同”。
总而言之:在block内部访问的auto变量为值传递,局部静态变量为引用传递(也就是传递变量的指针)。

三. 全局变量

\\demoB
#import <Foundation/Foundation.h>

typedef void(^Block)(void);

int age_ = 25;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int a = 10;
        static int b = 10;
        Block block = ^(){
            NSLog(@"你好世界!a = : %d ;",a);
            NSLog(@"你好世界!b = : %d ;",b);
            NSLog(@"你好世界!age_ = : %d ;",age_);
        };
        
        a = 20;
        b = 20;
        age_ = 26;
        block();
//        2018-08-29 00:54:13.318712+0800 Interview03-block[2155:10283110] 你好世界!a = : 10 ;
//        2018-08-29 00:54:13.319099+0800 Interview03-block[2155:10283110] 你好世界!b = : 20 ;
//        2018-08-29 00:54:13.319130+0800 Interview03-block[2155:10283110] 你好世界!age_ = : 26 ;

    }
    return 0;
}

block内部访问全局变量age_,其变化同静态局部变量一样。同样转换成C++代码分析:

typedef void(*Block)(void);

int age_ = 25;

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__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_d8c19f_mi_0,a);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_d8c19f_mi_1,(*b));
            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_d8c19f_mi_2,age_);
        }

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

        int a = 10;
        static int b = 10;
        Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &b));

        a = 20;
        b = 20;
        age_ = 26;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

较demoA、demoB不同的是,block结构体内部没有定义age_变量,block内部访问age_变量的时候,传入的也是全局的age_,因此在任何地方改变这个全局变量,block访问的时候都是这个全局变量的最新值。
通过demoA\B\C,可以肯定对于局部auto变量、static变量、全局变量,block的变量捕获情况如下:

block变量捕获机制.png

分析了block对自动变量,static变量与全局变量的捕获方式的不同,我认为合理的解释是:自动变量,内存可能会销毁,将来执行block的时候,访问变量的内存,可能会因为不存在引发坏内存访问。
静态局部变量:static变量内存一直会保存在内存中,所以可以取它的最新值,也就是通过指针去取。

3. block的类型

block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型.
__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )
__NSMallocBlock__ ( _NSConcreteMallocBlock )

我们通过关键字可以知道这三种类型block分别存放在内存的全局区、栈区、堆区,在内存中对应的区域图示如下:

三种block在应用程序内存分配.png

程序区域存放的就是我们写的代码,比如一个Person类里面的代码。
数据区也就全局区,存放着程序中使用到的全局变量。
堆存放的就是我们新建的对象。如[[Person alloc] init]出来的,这部分内存需要我们手动释放。
栈区存放的就是自动变量,一般在函数调用之后,这些自动变量所占用内存也就被系统回收了。
补充的知识
堆是动态分配内存的,是程序员管理的,自己申请内存,自己管理内存.
栈是系统会自动分配内存,自己释放内存.

由于在ARC环境下,编译器为我们做了很多额外的工作,比如将栈区的block copy到堆区,我们在ARC下也就不容易捕获到block初始状态的位置。所以暂时将开发环境切换至MRC下:

把开发环境切换至MRC.jpg

在MRC下,定义两个block,一个访问auto变量,一个不访问auto变量,最后对访问auto变量的block调用copy方法,依次查看三种情况下block所对应的类型如下:

#import <Foundation/Foundation.h>

typedef void(^Block)(void);

int age_ = 25;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int a = 10;
        static int b = 10;
        Block block = ^(){
            NSLog(@"你好世界!a = : %d ;",a);
        };
        
        Block block1 = ^(){
            NSLog(@"你好世界");
        };
        NSLog(@"%@  %@  %@",[block class],[block1 class],[[block copy] class]);
        
//         __NSStackBlock__  __NSGlobalBlock__  __NSMallocBlock__

    }
    return 0;
}

访问了auto变量的block在栈区,不访问auto变量的block在全局区。对栈区的block调用copy方法,block居然移到了堆区!后面我们对全局区的block调用copy,发现全局区域的block仍旧在全局区。

block类型 环境
NSGlobalBlock 没有访问auto变量
NSStackBlock 访问了auto变量
NSMallocBlock NSStackBlock调用了copy

stackBlock为什么要copy到堆上,因为我们想把block存储的东西保存下来.栈上的block会在函数结束的时候释放,block保存的东西不确定,我们一般都是将block保存下来,在恰当的时候调用.

每一种类型的block调用copy后的结果.png

4. block的copy

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,有以下情况:

  1. block作为函数返回值的时.
  2. 将block赋值给_strong指针时.
  3. block作为Cocoa API中方法名含有usingBlock的方法参数时.(例如:[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { }];)
  4. block作为GCD API的方法参数时
  • MRC下block属性的建议写法
    @property (copy, nonatomic) void (^block)(void);
  • ARC下block属性的建议写法
    @property (strong, nonatomic) void (^block)(void);
    @property (copy, nonatomic) void (^block)(void);

那么问题来了,为什么要对block进行copy操作?
假如在MRC环境下,在某个函数内定义了一个block变量,并在block中访问了局部变量,但是并没有立即调用该block。后面等到调用该函数的时候,再调用block,看下面的demo:

MRC下未使用copy.png

调用block后,block内部访问的局部变量打印的结果很糟糕,程序倒是没奔溃,但是结果不如人所愿。
出现这种情况的原因很好理解:由于这个block访问了auto变量,因此是一个NSStackBlock类型的block,该block对应的结构体分配在栈内存上,等到test()函数调用完毕,栈内存会被回收,所以block被调用的时候,访问block结构体内部的变量a,a所对应的内存区域随时可能被系统回收,其内存上的数据也是不确定的。
这种情况该如何保证我们调用block的时候,还能正常访问局部变量呢?正如前面列出的,调用copy方法将block从栈区copy到堆区,事情就解决了。【当然,换成ARC环境,我们通常在声明block属性的时候,使用copystrong关键词修饰,系统也会自动帮我们将block从栈区拷贝到堆区。也就无需我们动手调用block的copy方法了。但是系统底层还是帮我们对block做了copy操作。

MRC下未使用copy.png

"copy"这个操作在ARC下是没有必要的。由于我们的block赋值给了void(^block)(void),这个变量默认是__strong修饰的,满足编译器会根据情况自动将栈上的block复制到堆上的条件2,即"将block赋值给__strong指针时"。

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

推荐阅读更多精彩内容