iOS底层原理

小码哥底层原理笔记:Block变量捕获

2020-07-09  本文已影响0人  chilim

前面我们看到Block是会将捕获到的变量保存在__main_block_impl_0结构体中,那么是不是所有变量都会被捕获呢?肯定不是的。接下来将变量分为两类去讨论。

局部变量

在局部变量中又有默认的auto变量和Static变量。
我们看下面这段代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;//auto变量,auto关键字通常省略
        static int height = 10;//Static变量
        void (^block)(void) = ^(){
            NSLog(@"age is %d, height is %d",age, height);
        };

        age = 20;
        height = 20;

        block();
}
    return 0;
}
//执行block打印: age is 10, height is 20

我们将其翻译成.cpp源码可以看到Block捕获到了这两个变量:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;//捕获的是age值
  int *height;//捕获的是height的内存地址
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

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



        int age = 10;
        static int height = 10;
        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));//age直接把值传进去,height是取它的地址传过去

        age = 20;
        height = 20;

        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

我们通过代码可以看到虽然age和height都捕获到了block里面,但是当我们执行block()的时候打印出来的age依旧是10,而height是20。那是因为我们auto局部变量捕获的是它的值,从main函数可以看出来定义block的时候是直接把age值传进去的。

而height不一样,因为height是static修饰的,它是一直保存在内存中的,所以block捕获的时候直接捕获的是它的内存地址,因为它只要一直存在内存中,那么它的内存地址是不会变的,从main函数也可以看到定义block的时候是直接把height的内存地址传进去的。所以当我们改变了height的值时,我们执行block后依旧能打印出最新的值。

以上就是局部变量的捕获原理。

那么为什么我们要捕获局部变量呢?

因为我们执行block的时候实际上是调用了这个函数

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

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_y0_thwqbdb11zq5wklyb8jyy4km0000gn_T_main_c44a07_mi_0,age, (*height));
        }

而我们的局部变量时定义在main函数中的,我们从一个函数中要去访问另一个函数的局部变量,这种跨函数访问,如果我们定义block的时候不把变量捕获到的话,那执行block的时候就无法访问到局部变量了,因为局部变量作用域只在本函数内。

全局变量

全局变量同样也分为默认的auto全局变量和static全局变量
我们看以下代码

int age_ = 10;//默认auto全局变量
static int height_ = 10;//static全局变量

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        void (^block)(void) = ^(){
            NSLog(@"age is %d, height is %d",age_, height_);
        };
        age_ = 20;
        height_ = 20;
        block();//打印,age is 20,height is 20
    }
    return 0;
}

我们同样将其翻译成.cpp源码,如下:

int age_ = 10;
static int height_ = 10;

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

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

        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        age_ = 20;
        height_ = 20;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

我们看到__main_block_impl_0里面并没有捕获到的变量,全局变量依旧是全局变量放在外面,是一直都在内存中的。所以我们不需要捕获进来,因为一直都在内存中,block需要访问的时候直接访问就是了。当我们执行block的时候取的就是age和height的最新值。

以上就是block变量捕获的原理。

这里还有一种情况,是我们经常遇到的。我们新建一个Person类

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;

- (void)test;

@end

#import "Person.h"

@implementation Person

//所有OC的方法翻译成C语言函数,前面两个参数都是(Person * self, SEL _cmd)
- (void)test{
    void (^block)(void) = ^{
        NSLog(@"----------%p",self);
    };
    block();
}

//如果test函数要访问name属性,那么肯定是先捕获到self,然后访问self->_name
//- (void)test{
//    void (^block)(void) = ^{
//        NSLog(@"----------%p",_name);
//    };
//    block();
//}

@end

我们在Person类里面定义了一个test函数,里面定义一个block,这个block访问self。这个self是局部变量还是全局变量呢?我们看一下.cpp源码就知道了
self被捕获到block里面

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

test函数最终是转换成这样

static void _I_Person_test(Person * self, SEL _cmd) {
    void (*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));//把self传进去了
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

我们看到self是做为参数传进去的,应该是作为局部变量来看。
如果我们这个test函数访问的是name属性呢?像这样

- (void)test{
    void (^block)(void) = ^{
        NSLog(@"----------%p",_name);
    };
    block();
}

这样其实是先捕获到self,然后再访问self里面的name属性的,相当于这样

- (void)test{
    void (^block)(void) = ^{
        NSLog(@"----------%p",self->name);
    };
    block();
}

最后总结如下:


block变量捕获总结
上一篇下一篇

猜你喜欢

热点阅读