面试专题

block:block捕获变量

2019-10-02  本文已影响0人  意一ineyee
一、block捕获变量根儿上的东西
 1、block会捕获局部变量
 2、block不会捕获全局变量
二、block捕获变量衍生出来的东西
 1、block会捕获局部对象类型的指针变量,强指针(__strong)则持有对象、弱指针(__weak)则不持有对象
 2、block会捕获self,强指针(__strong)则持有对象、弱指针(__weak)则不持有对象

block捕获变量是指,如果block的执行体里使用了外界的局部变量,那么block内部就会生成一个与局部变量同名的成员变量,并且局部变量还会把值传递给这个成员变量,当然可能是值传递,也有可能是指针传递。那么接下来block执行体里使用的这个变量就不是外界的局部变量了,而是block体内的成员变量。而如果block的执行体里使用了外界的全局变量,那block是不会捕获它们的,会直接使用它们。

那为什么系统要给block添加捕获变量机制呢?又为什么只捕获局部变量而不捕获全局变量呢?实际开发中,我们难免要在block的执行体里使用外界的局部变量,我们知道block其实是把block的参数、返回值、执行体封装成一个函数,而这个函数在调用时却仅仅接收了block本身作为参数,并没有接收额外的参数,所以一个函数怎么可能无缘无故就访问到函数外部的变量呢。于是系统就为block设计了捕获变量机制,把局部变量捕获到block体内,以便函数仅仅接收block本身作为参数就能正常使用外界的局部变量。而全局变量存储在全局区,block能直接访问到,所以不需要捕获。

一、block捕获变量根儿上的东西


  • block会捕获局部变量
  • block不会捕获全局变量

所以要想知道一个变量会不会被block捕获,你只需要搞清它是个局部变量还是个全局变量就行了,别去管它是什么类型的block。当然上面这个精简版的结论,是由下面这个比较复杂的结论精简出来的,我们没必要记这个复杂版的结论,有点绕而且在实际分析问题中没多大意义:

1、block会捕获局部变量

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 普通局部变量
        int age = 25;
        
        void (^block)(void) = ^{// ARC下这是个堆block,因为block赋值给强指针,系统会自动复制一份到堆区
            
            NSLog(@"%d", age);// 25
        };
        
        age = 26;
        
        block();
    }
    return 0;
}

按正常逻辑来说,上面的代码应该打印“26”,因为在block调用之前age被改成“26”了,但实际上却打印“25”,为什么?我们看看这段代码的C/C++实现(伪代码)。

// block对应的结构体
struct __block_impl_0 {
    struct __block_impl impl;
    struct __block_desc_0* Desc;
    
    int age;// 多了一个成员变量
    
    // : age(_age),C++的语法,意思是直接把_age参数的值赋值给age成员变量,相当于下面又多了一句赋值语句
    __block_impl_0(void *fp, struct __block_desc_0 *desc, int _age, int flags=0) : age(_age) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
        
//      age = _age;// 相当于这样
    }
};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        
        int age = 25;
        
        // 创建block
        void (*block)(void) = &__block_impl_0(
                                              __block_func_0,
                                              &__block_desc_0_DATA,
                                              age// 多了一个参数
                                              );
        
        age = 26;
        
        // 调用block
        block->impl.FuncPtr)(block);
    }
    return 0;
}

void __block_func_0(struct __block_impl_0 *__cself) {
    
    int age = __cself->age;// 获取age成员变量的值
    
    NSLog(age);
}

我们看到block内部多了一个成员变量age

也看到在创建block的时候,block构造函数多了一个age参数,直接把变量的值“25”给传进去了,并赋值给block的成员变量age

然后外界把变量age的值改为“26”。

调用block时,系统读取的是block内部那个成员变量的值,所以打印了“25”。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 静态局部变量
        static int height = 25;
        
        void (^block)(void) = ^{// 这是个全局block
            
            NSLog(@"%d", height);// 26
        };
        
        height = 26;
        
        block();
    }
    return 0;
}

打印“25”红还是“26”😄?直接看C/C++实现吧(伪代码)。

// block对应的结构体
struct __block_impl_0 {
    struct __block_impl impl;
    struct __block_desc_0* Desc;
    
    int *height;// 多了一个成员变量
    
    // : height(_height),C++的语法,意思是直接把_height参数的值赋值给height成员变量,相当于下面又多了一句赋值语句
    __block_impl_0(void *fp, struct __block_desc_0 *desc, int *_height, int flags=0) : height(_height) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
        
//      height = _height;// 相当于这样
    }
};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        
        static int height = 25;
        
        // 创建block
        void (*block)(void) = &__block_impl_0(
                                              __block_func_0,
                                              &__block_desc_0_DATA,
                                              &height// 多了一个参数
                                              );
        
        height = 26;
        
        // 调用block
        block->impl.FuncPtr)(block);
    }
    return 0;
}

void __block_func_0(struct __block_impl_0 *__cself) {
    
    int *height = __cself->height;// 获取height成员变量的值
    
    NSLog(*height);
}

没问题,我们看到block内部多了一个成员变量height,但要注意它是个指针类型

也看到在创建block的时候,block构造函数多了一个height参数,但这里不是直接把变量的值传进去,而是把变量的地址给传进去了,并赋值给block的成员变量height

然后外界把变量height的值改为“26”。

调用block时,系统读取的是block内部那个成员变量的值没问题,但因为它是个指针,指向外界的那个变量,所以打印了“26”。

1、再加深一下印象:block会捕获局部变量

  • block会捕获普通局部变量,局部变量与成员变量之间是值传递
  • block会捕获静态局部变量,局部变量与成员变量之间是指针传递

2、那系统为什么要这样设计呢?同样都是局部变量,为什么普通局部变量是值传递,而静态局部变量是指针传递?

void (^block)(void);
void test() {
    
    // 普通局部变量
    int age = 25;
    // 静态局部变量
    static int height = 25;
    
    block = ^{// ARC下这是个堆block,因为block赋值给强指针,系统会自动复制一份到堆区
        
        NSLog(@"%d %d", age, height);
    };
        
    age = 26;
    height = 26;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        test();
        block();
    }
    return 0;
}

一看上面这段代码,你就明白了。

  • test函数执行完,也就是说出了test函数的作用域,
  • 普通局部变量age就释放了,也就是说它对应的那块栈内存就释放了,有可能被别人征用,里面填充别的数据,那内存释放后你再去访问这块内存,访问到不一定是原来的数据,所以普通局部变量采用指针传递根本没有意义,因为它对应的那块内存说不定什么时候(即有可能在我们使用它之前)就释放掉了,所以还不如趁早把局部变量的值给存下来。
  • 而静态局部变量就不一样了,出了test函数的作用域,height变量虽然也被释放掉了,但这仅仅是表明在代码层我们无法再继续通过height变量去访问它对应的那块内存而已,并不代表那块内存也释放了,因为这块内存是静态全局区的一块内存,所以我们只要用一个指针变量来记住这块内存的地址,那height变量释放后,我们依旧可以通过自己的指针变量去访问那块内存。

2、block不会捕获全局变量

// 普通全局变量
int age = 25;
// 静态全局变量
static int height = 25;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        void (^block)(void) = ^{// 这是个全局block
            
            NSLog(@"%d %d", age, height);// 26, 26
        };
        
        age = 26;
        height = 26;
        
        block();
    }
    return 0;
}

C/C++实现(伪代码)。

// block对应的结构体
struct __block_impl_0 {
    struct __block_impl impl;
    struct __block_desc_0* Desc;

    __block_impl_0(void *fp, struct __block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

int age = 25;
static int height = 25;

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        
        // 创建block
        void (*block)(void) = &__block_impl_0(
                                              __block_func_0,
                                              &__block_desc_0_DATA,
                                              );
        
        age = 26;
        height = 26;
        
        // 调用block
        block->impl.FuncPtr)(block);
    }
    return 0;
}

void __block_func_0(struct __block_impl_0 *__cself) {
    NSLog(age, height);// 直接访问全局变量
}

我们看到block内部并不会多出成员变量,而且调用block时,是直接通过全局变量访问对应内存里的数据。

二、block捕获变量衍生出来的东西


1、block会捕获局部对象类型的指针变量,强指针(__strong)则持有对象、弱指针(__weak)则不持有对象

block会捕获局部对象类型的指针变量,而且捕获后如果发现它是个强指针(即__strong修饰),block还会强引用(即持有)它指向的对象,如果发现它是个弱指针(即__weak修饰),block则会弱引用(即不持有)它指向的对象。(如果更严谨一点的话,栈block永远只是弱引用对象,只不过因为我们是ARC下,用的基本上都是堆block,所以就故意忽略掉了这一点,免得大家混淆)

创建一个Person类,简单实现一下,来验证上面这条结论。

// INEPerson.h
@interface INEPerson : NSObject

@property (nonatomic, assign) NSInteger age;

@end


// INEPerson.m
@implementation INEPerson

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

@end
// main.m
typedef void (^INEBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool
    {// 作用域2起点
        
        INEBlock block;
        
        {// 作用域1起点
            
            INEPerson *person;
            
            person = [[INEPerson alloc] init];
            person.age = 25;
            
            block = ^{
                
                NSLog(@"%ld", person.age);
            };
        }// 作用域1终点
        
        NSLog(@"11");
    }// 作用域2终点
    
    return 0;
}

控制台打印:

11
INEPerson dealloc

block的C/C++实现(伪代码):

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
    
  INEPerson *__strong person;// 确实捕获了,是个强指针
    
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, INEPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

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

我们知道person指针变量是个局部变量,所以它肯定会被block捕获,而且person指针变量默认是个强指针,所以block内部生成的同名成员变量也是一个强指针,于是block就通过它内部的那个强指针强引用了person指针变量指向的Person对象。

所以出了作用域1后,虽然person指针变量销毁了,但此时block还没销毁,它还强引用着Person对象,所以这个时候就不会走Person对象dealloc方法,而是继续往下走,打印完“11”、出了作用域2后,block销毁,同时也就释放了对Person对象的强引用,所以此时才走Person对象的dealloc方法打印了“INEPerson dealloc”。

// main.m
typedef void (^INEBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool
    {// 作用域2起点
        
        INEBlock block;
        
        {// 作用域1起点
            
            __weak INEPerson *person;
            
            person = [[INEPerson alloc] init];
            person.age = 25;
            
            block = ^{
                
                NSLog(@"%ld", person.age);
            };
        }// 作用域1终点
        
        NSLog(@"11");
    }// 作用域2终点
    
    return 0;
}

控制台打印:

INEPerson dealloc
11

block的C/C++实现(伪代码):

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
    
  INEPerson *__weak person;// 确实捕获了,是个弱指针
    
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, INEPerson *__weak _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

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

block确实会捕获person指针变量,但因为它是个弱指针,所以block就通过它内部的那个弱指针弱引用了person指针变量指向的Person对象。

所以出了作用域1后,person指针变量销毁,Person对象身上就没有强引用了,所以这个时候就走Person对象的dealloc方法打印了“INEPerson dealloc”,然后继续往下走,打印完“11”、出了作用域2后,block销毁。

此时,你可能会问:block捕获指针倒是没问题,但你凭什么说捕获到强指针就持有对象,捕获到弱指针就不持有对象,上面虽然通过代码验证了,但这底层是怎么实现的?

从上面的代码中,我们可以看到只要是block捕获了对象类型的指针变量,那它结构体内第二个成员变量里就会多出两个函数,copy函数和dispose函数,这两个函数是专门用来负责对象的内存管理的,这也是为什么block捕获基本数据类型的变量时,它内部不会生成这两个函数。

持有不持有主要靠的是block内部的copy函数和dispose函数,当我们把block从栈区copy到堆区时,系统就会自动调用block内部的copy函数,该函数内部会根据捕获到的是个强指针还是弱指针来决定要不要把对象的引用计数加1,而当block销毁的时候,系统又会自动调用内部的dispose函数,来解除对对象的引用。

2、block会捕获self(指针变量),强指针(__strong)则持有对象、弱指针(__weak)则不持有对象

创建一个Person类,简单实现一下,来验证上面这条结论。

// INEPerson.m
@implementation INEPerson

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

@end

block的C/C++实现(伪代码)。

struct __INEPerson__test_block_impl_0 {
  struct __block_impl impl;
  struct __INEPerson__test_block_desc_0* Desc;
    
  INEPerson *__strong self;
    
  __INEPerson__test_block_impl_0(void *fp, struct __INEPerson__test_block_desc_0 *desc, INEPerson *const __strong _self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可见self被block捕获了,那为什么会捕获self呢?这是因为所有的OC方法其实都有两个默认的参数:self指针变量和_cmd selecotr,即该方法调用者和该方法的selector,而方法的参数也是一种局部变量,所以self会被block捕获。上面的test方法其实就是这样(伪代码):

- (void)test(id self, SEL _cmd) {
    
    void (^block)(void) = ^{
        
        NSLog(@"%@", self);
    };
    block();
}

self指针默认也是个强指针,所以block会持有它指向的对象,而如果把self指针变成弱指针,block就不会持有它指向的对象了。

// INEPerson.m
@implementation INEPerson

- (void)test {
    
    __weak INEPerson *weakSelf = self;
    void (^block)(void) = ^{
        
        NSLog(@"%@", weakSelf);
    };
    block();
}

@end

block的C/C++实现(伪代码)。

struct __INEPerson__test_block_impl_0 {
  struct __block_impl impl;
  struct __INEPerson__test_block_desc_0* Desc;
    
  INEPerson *__weak weakSelf;
    
  __INEPerson__test_block_impl_0(void *fp, struct __INEPerson__test_block_desc_0 *desc, INEPerson *__weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
上一篇下一篇

猜你喜欢

热点阅读